Documentation › API

The Cookies správně API

An HTTP API for reading the consent records your cookie bar has collected. Bearer token authentication, JSON responses, cursor pagination. The whole reference is on this one page.

On this page

1. What the API is for

The API has two endpoints:

  • GET /api/v2/licenses returns the licenses your token can reach and their id.
  • GET /api/v2/consents returns a paginated export of one license's consents.

The base address is https://cookies-spravne.cz/api/v2.

In the data, a consent is a state, not an event. One row per license and visitor pair, overwritten on every change of choice. The export therefore returns each visitor's current decision, not a time series of their clicks. Details are in GET /api/v2/consents.

The API has no writing, deleting or webhooks. Consents are created only in the bar.

Conventions

Base addresshttps://cookies-spravne.cz/api/v2
MethodsGET
Response Content-Typeapplication/json
Cacheboth endpoints respond with Cache-Control: no-store
TimesUTC, ISO 8601 with the Z suffix, accurate to the second
Consent idUUID as a string
license_idinteger
Booleanstrue and false

A successful response contains data, meta and links. An error response contains error and never data.

Legacy API

Integrations built on the original system can keep using the /api/consent-report and /api/consent-configs endpoints. They are described at the end of this page. Build new integrations on v2.

2. Authentication

You generate the token in the dashboard under Account → API access and send it in the Authorization header as a bearer token. An account always has at most one token.

How to send the token

GET /api/v2/licenses HTTP/1.1
Host: cookies-spravne.cz
Authorization: Bearer cs_2f9Ktz...

Token properties

Issued inthe dashboard, Account → API access
Formatcs_ followed by 48 characters, 51 in total
Numberone token per account
Scopelicenses owned by the account and licenses shared with it
Transportonly the Authorization header; a ?token= parameter is not read

The token is shown only once, when it is generated. After that the dashboard shows only the last four characters, the creation date and the date of last use. The token is meant for server-to-server communication; never put it in code that runs in a browser.

Managing the token

  • Generate token creates a new token and shows it once.
  • Regenerate token immediately replaces the existing one. An integration using the old token stops working.
  • Revoke token closes access immediately.

The Last used field is your warning light. If you do not use the API and it still shows recent use, the token has probably leaked. Generate a new one.

Invalid or missing token

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="cookiesspravne", error="invalid_token"
Cache-Control: no-store
Content-Type: application/json

{"error":{"code":"invalid_token","message":"The API token is missing, malformed or no longer valid."}}

3. GET /api/v2/licenses

Returns all licenses your token can reach, sorted by id. No parameters and no pagination. This is where you get the id you then use as license_id when exporting consents.

Request

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://cookies-spravne.cz/api/v2/licenses"

Response

{
  "data": [
    { "id": 1, "domain": "example.cz", "is_active": true,  "created_at": "2025-04-01T09:12:00Z" },
    { "id": 7, "domain": "eshop.cz",   "is_active": false, "created_at": "2026-01-20T14:03:00Z" }
  ]
}

Record fields

FieldTypeMeaning
idintThe value of the license_id parameter for the /consents endpoint.
domainstringThe domain the license is valid for.
is_activeboolWhether the license is running. An inactive license remains readable, including its whole consent history.
created_atstring or nullWhen the license was created.

The license key the bar authenticates with is not shown in the response. Neither is the plan or the billing status.

4. GET /api/v2/consents

A paginated export of one license's consents. The only required parameter is license_id; the rest have sensible defaults.

Parameters

ParameterRequiredValuesDefault
license_idyesint–
limitno1 to 10000100
order_bynocreated_at, updated_atcreated_at
ordernoasc, descasc
fromnoY-m-d or ISO 8601–
tonoY-m-d or ISO 8601–
cursornoopaque string–

How parameters are validated

  • license_id must be a number. Missing, empty or non-numeric returns 400 invalid_request. Syntactically valid but without access to the license returns 404 license_not_found. The API does not distinguish between “does not exist” and “you have no access”.
  • limit that is non-numeric returns 400. A numeric value is clamped to the range 1 to 10000: limit=50000 returns 10000 rows, limit=0 returns one. The value used is always in the response in the meta.limit field.
  • order_by and order outside the allowed values, for example order_by=id, return 400 invalid_request.
  • from and to accept either a bare date Y-m-d or a full ISO 8601 value, which is used exactly. from=2026-06-01 means 2026-06-01T00:00:00Z, to=2026-06-30 means 2026-06-30T23:59:59Z. Both bounds are inclusive. A non-existent date such as 2026-02-31 returns 400 invalid_date. from later than to returns an empty page.
  • The date range applies to the column chosen in order_by. With order_by=updated_at, from therefore means “changed since”, not “created since”.

Response

curl -sS -H "Authorization: Bearer $TOKEN" \
  "https://cookies-spravne.cz/api/v2/consents?license_id=1&limit=100"
{
  "data": [
    {
      "id": "9c1f7a2e-5b3d-4e18-9a6c-2f0d81b7e4aa",
      "uid": "aB3xY7kQ",
      "license_id": 1,
      "created_at": "2026-06-01T10:00:00Z",
      "updated_at": "2026-06-04T08:31:00Z",
      "necessary": true,
      "analytics": false,
      "marketing": false,
      "ad_user_data": false,
      "ad_personalization": false,
      "was_shown": true,
      "was_dismissed": false
    }
  ],
  "meta": {
    "count": 100,
    "limit": 100,
    "order": "asc",
    "order_by": "created_at",
    "has_more": true,
    "next_cursor": "v1.eyJ0IjoiMjAyNi0wNi0wMSAxMDowMDowMCJ9"
  },
  "links": {
    "next": "https://cookies-spravne.cz/api/v2/consents?license_id=1&limit=100&cursor=v1.eyJ0IjoiMjAyNi0wNi0wMSAxMDowMDowMCJ9"
  }
}

Record fields

FieldTypeMeaning
idstring (UUID)The record's primary key. It does not change when the choice is overwritten.
uidstring or nullThe user identifier. Used to look up a consent; the visitor sees it in the bar and can report it to you.
license_idintThe license the record belongs to.
created_atstring or nullThe first record for the license.
updated_atstring or nullThe last write for the license.
necessaryboolNecessary cookies.
analyticsboolAnalytics cookies.
marketingboolMarketing cookies.
ad_user_databoolGoogle Consent Mode, the ad_user_data signal.
ad_personalizationboolGoogle Consent Mode, the ad_personalization signal.
was_shownboolThe bar was shown to the visitor.
was_dismissedboolThe bar was closed with the cross, without choosing categories.

The internal reference to the configuration version the visitor saw and the flag for imports from the older system are not exported. If you need them, write to us at podpora@cookies-spravne.cz.

The meta and links fields

FieldTypeMeaning
meta.countintThe number of records in data.
meta.limitintThe limit after clamping to the allowed range.
meta.orderstringThe sort direction.
meta.order_bystringThe sort column used.
meta.has_moreboolAnother page exists.
meta.next_cursorstring or nullThe cursor for the next page.
links.nextstring or nullThe original query with all parameters and the cursor added.

links.next is built from the query string of the incoming request, with cursor added. It therefore also keeps the limit, order_by, from and to the pagination started with.

The consent data model

The bar cares about the visitor's current decision, not their history. A write is therefore an upsert on the license_id and uid pair: one row per visitor, overwritten on every change of choice.

The was_shown and was_dismissed flags are in the export because without them three different situations look the same, with all categories false:

was_shownwas_dismissedWhat happened
truefalseThe visitor saw the bar and rejected all categories.
truetrueThe visitor closed the bar without setting categories.
falsefalseThe bar was not rendered for the visitor.

6. Limits and errors

With a valid token you have 120 requests per minute. Every error response has the same shape: an error object with code and message fields.

Limits

KeyLimit
Account with a valid token120 requests per minute
IP address without a valid token10 requests per minute

With a valid token, the limit is keyed to the account, not the IP. Servers behind one NAT address therefore do not share a budget, but two machines of the same account do. With limit=10000 the ceiling corresponds to 1,200,000 rows per minute.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{"error":{"code":"rate_limited","message":"Too many requests."}}

The 429 response carries no Retry-After header and no X-RateLimit-* headers. The window is a fixed minute, so it is enough to wait and repeat the request.

Error response shape

All errors, including 500, have the same shape:

{
  "error": {
    "code": "invalid_token",
    "message": "The API token is missing, malformed or no longer valid."
  }
}

Error codes

HTTPcodeWhen it occurs
400invalid_requestMissing or invalid license_id, order_by, order or limit.
400invalid_datefrom or to is neither Y-m-d nor ISO 8601, or is a non-existent date.
400invalid_cursorThe cursor cannot be decoded.
400cursor_mismatchThe cursor was created with different filters than the current query carries.
401invalid_tokenThe token is missing, invalid or has been revoked.
404license_not_foundThe license does not exist, or you have no access to it.
429rate_limitedThe request limit has been exceeded.
500server_errorAn error on the API side.

Example

GET /api/v2/consents?license_id=abc

HTTP/1.1 400 Bad Request

{"error":{"code":"invalid_request","message":"A numeric license_id is required."}}

7. Example calls

Three ready-to-use samples: a full export to JSONL, incremental reading of changes and loading a single page.

Full export to JSONL

Bash with curl and jq. It goes through all pages via links.next as long as another one exists.

#!/usr/bin/env bash
set -euo pipefail

TOKEN="cs_…"
URL="https://cookies-spravne.cz/api/v2/consents?license_id=1&limit=1000"

while [ -n "$URL" ]; do
  BODY=$(curl -sS -H "Authorization: Bearer $TOKEN" "$URL")
  echo "$BODY" | jq -c '.data[]' >> consents.jsonl
  URL=$(echo "$BODY" | jq -r '.links.next // ""')
done

Incremental reading of changes

JavaScript. It reads records changed since a given moment, sorts by updated_at and can wait when the limit is exceeded.

const TOKEN = process.env.COOKIES_SPRAVNE_TOKEN;
const BASE = 'https://cookies-spravne.cz/api/v2/consents';

async function* changes(licenseId, since) {
  let url = `${BASE}?license_id=${licenseId}&order_by=updated_at&order=asc&limit=1000`
    + (since ? `&from=${encodeURIComponent(since)}` : '');

  while (url) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });

    if (res.status === 429) {
      await new Promise(resolve => setTimeout(resolve, 60_000));
      continue;
    }

    if (!res.ok) {
      const { error } = await res.json();
      throw new Error(`${res.status} ${error.code}: ${error.message}`);
    }

    const { data, links } = await res.json();

    yield* data;

    url = links.next;
  }
}

A single page in PHP

<?php

$token = getenv('COOKIES_SPRAVNE_TOKEN');

$context = stream_context_create([
    'http' => [
        'method' => 'GET',
        'header' => "Authorization: Bearer {$token}\r\n",
    ],
]);

$url = 'https://cookies-spravne.cz/api/v2/consents?' . http_build_query([
    'license_id' => 1,
    'order_by' => 'updated_at',
    'order' => 'asc',
    'limit' => 1000,
]);

$page = json_decode(file_get_contents($url, false, $context), true);

For regular imports, store the time of the last successful run and pass it next time in the from parameter together with order_by=updated_at. That way you download only what has changed.

8. Legacy API (v1)

The original system's endpoints remain available at the same addresses and return data in the same format, so existing integrations keep working without changes.

Build new integrations on API v2 described above. This part is here only for backward compatibility.

Legacy API authentication

The legacy API is not authenticated with the API token but with the legacy API key you received when access was activated in the original system. If we hold it for your account, you will find it in the dashboard under Account → API access. If you do not see it there, write to us at podpora@cookies-spravne.cz.

The v2 API token does not work on the legacy endpoints, and the legacy API key does not work on v2.

The key is sent in the Authorization header. For backward compatibility the Authentication header, which the original documentation gave, is also accepted:

Authorization: Bearer xxxxxxxxx
Authentication: Bearer xxxxxxxxx

Responses are always application/json. Errors have the same shape and codes as v2, and the same limits apply.

GET /api/consent-report

Returns users' consents. Supports pagination and filtering.

ParameterRequiredMeaning
keyyesThe license key of the domain you want the report for. The license must belong to your account or be shared with it, otherwise 404 license_not_found.
fromnoA Y-m-d date. On its own it forms an interval from that day until today; the given day is part of the interval.
tonoA Y-m-d date. On its own it forms an interval from the beginning until that day; the given day is not part of the interval.
limitnoThe number of records per page. Default 1000, maximum 10000.
pagenoThe page number; the first is 1.

Combining from and to creates an interval that includes from and excludes to. For example ?from=2022-01-01&to=2022-02-01 returns the whole of January 2022. Filtering is by the date consent was first given, date_of_consent, and records are sorted by it in ascending order.

Response headers

x-document-countThe total number of records matching the key, from and to filters.
x-total-pagesThe total number of pages. Depends on the limit parameter.

Example

curl -sS -H "Authorization: Bearer $LEGACY_KEY" \
  "https://cookies-spravne.cz/api/consent-report?key=bstl7sn95ns7sno97s1q&from=2022-01-01&to=2022-02-01&page=1"
[
  {
    "_id": { "$oid": "9c1f7a2e-5b3d-4e18-9a6c-2f0d81b7e4aa" },
    "uid": "CxQ25iVbozc2o82J4RcuX1Lv5DoXpO",
    "key": "bstl7sn95ns7sno97s1q",
    "necessary": "1",
    "analytics": "0",
    "marketing": "1",
    "config": { "$oid": "4812" },
    "date_of_consent": "1644842294"
  }
]
FieldTypeMeaning
_id.$oidstringThe record identifier.
uidstring or nullA unique user ID by which the user can be identified.
keystringThe domain's license key.
necessarystringAlways "1".
analyticsstring"1" or "0" according to the consent given.
marketingstring"1" or "0" according to the consent given.
configobject or nullThe $oid of the configuration from /api/consent-configs that the user saw.
date_of_consentstringA Unix timestamp of when the user first gave consent.

GET /api/consent-configs

Returns all versions of the bar's settings and texts that were shown to visitors. Consents may be tied to a different wording of the bar, so each consent has a reference to a specific version in the config field. The only parameter is the required key, with the same meaning as above.

[
  {
    "_id": { "$oid": "4812" },
    "modified": 1644236417,
    "texts": {
      "cs": {
        "reject_all": "Odmítnout",
        "cookie_policy": "Více o cookies",
        "iframe_no_cookies": "Pro zobrazení tohoto obsahu povolte prosím ukládání cookies."
      }
    }
  }
]

_id.$oid matches the config.$oid value in the consent listing, modified is a Unix timestamp of when this version was first shown, and texts contains the bar's texts by language.

Differences from the original system

  • A consent is one record per visitor. When a visitor changes their choice, the record is overwritten and date_of_consent remains the date of the first consent. To track changes, use API v2 with order_by=updated_at.
  • Errors return JSON with an error object. For example, a missing key returns 400 invalid_request.