12 KiB
Sklik (Seznam)
Proxy over the Sklik Drak JSON API
(https://api.sklik.cz/drak/json/v5/{method}).
Protocol (verified against seznam/api-examples)
- HTTP
POSTto the base URL with the method name appended to the path. - Body is a JSON array of positional arguments.
client.loginByTokentakes the API token and returns{"status":200,"session":"...","statusMessage":"OK"}.- Every authenticated method takes the user struct
{"session": ...}(optionally"userId") as its first argument, followed by the method's own arguments. - Every response is an object with
status(HTTP-style),statusMessage, a refreshedsession, and method-specific data.200,206and301are treated as success.
The proxy performs client.loginByToken per request from X-Sklik-Token and
injects the session — callers never handle the session.
Credentials
| Header | Required | Meaning |
|---|---|---|
X-Sklik-Token |
yes | API token from Sklik → account settings → API. |
X-Sklik-User-Id |
no | Managed account userId (agency/MCC access). |
Kde získat token (návod pro klienta)
- Přihlaste se na sklik.cz.
- Vpravo nahoře uživatelské jméno → Nastavení.
- Sekce Přístup k API Drak → Zobrazit token.
- Token → hlavička
X-Sklik-Token.
Nový token zneplatní ten předchozí. Token je vázaný na účet, pod kterým jste přihlášeni. Pro správu cizích účtů (agentura/MCC) použijte
X-Sklik-User-Id.
Missing token → 401 missing_credentials. Sklik business errors (invalid token,
access denied, bad arguments) are surfaced as upstream_error with the Sklik
status and full body.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /sklik/login |
Verify the token. Returns {valid, status, statusMessage} (no session). |
| GET | /sklik/limits |
api.limits — quotas and the statsDataLimit. |
| GET | /sklik/write-limits |
Guard rails applied to writes. No credentials needed. |
| GET | /sklik/campaigns |
campaigns.list, all pages collected. |
| GET | /sklik/groups |
groups.list, filterable by campaign_ids. |
| GET | /sklik/ads |
ads.list, filterable by campaign_ids / group_ids. |
| POST | /sklik/campaigns /sklik/groups /sklik/ads |
Create — always paused. |
| PUT | /sklik/campaigns /sklik/groups /sklik/ads |
Update by id (partial). |
| DELETE | /sklik/campaigns /sklik/groups /sklik/ads |
Remove (?ids=1,2,3) — reversible. |
| POST | /sklik/{entity}/restore |
Restore removed entities (?ids=1,2,3). |
| POST | /sklik/report/{entity} |
createReport + paged readReport for an entity. |
| POST | /sklik/rpc/{method} |
Generic authenticated call to any method. |
The proxy never returns the Sklik
session— it is a credential, and AGENTS.md forbids returning secrets from ordinary endpoints. The session is managed internally and callers have no use for it.
Listing entities
GET /sklik/campaigns, /sklik/groups, /sklik/ads wrap {entity}.list and
page through the whole result set (offset/limit, SKLIK_LIST_PAGE_LIMIT rows
per page, capped by SKLIK_LIST_MAX_PAGES):
{ "totalCount": 42, "returnedCount": 42, "truncated": false, "campaigns": [ ... ] }
Query parameters:
| Parameter | Endpoints | Meaning |
|---|---|---|
ids |
all | Comma-separated ids of the entity itself. |
campaign_ids |
groups, ads | Restrict to these campaigns. |
group_ids |
ads | Restrict to these groups. |
is_deleted |
all | true/false. Omit to get both. |
display_columns |
all | Comma-separated columns; defaults to a useful subset. |
Sklik's
campaigns.listfilter supports onlyidsandisDeleted— there is no status filter upstream, so filter onstatusin the returned rows.groups.listandads.listdo support parent filters (campaign.ids,group.ids), which is whatcampaign_ids/group_idsmap to.
Writes — full CRUD over campaigns / groups / ads
| Operation | Endpoint | Upstream | Body / params |
|---|---|---|---|
| Create | POST /sklik/{entity} |
{entity}.create |
JSON array of structs |
| Update | PUT /sklik/{entity} |
{entity}.update |
JSON array of structs, id required |
| Remove | DELETE /sklik/{entity}?ids=1,2 |
{entity}.remove |
ids in the query |
| Restore | POST /sklik/{entity}/restore?ids=1,2 |
{entity}.restore |
ids in the query |
{entity} ∈ campaigns, groups, ads. The structs are exactly the ones Sklik
documents (campaigns.create,
campaigns.update, and the
groups.* / ads.* equivalents). Sklik batches are all-or-nothing: if one
item fails, nothing is applied.
Create — everything is paused, not configurable
Sklik's status field defaults to active, so an omitted status would
create a live, spending campaign. The proxy therefore forces
status: "suspend" on every created entity and ignores any other value you
send (the override is logged).
Update — ordinary CRUD, status is not forced
PUT changes only the fields you send; id is required per item. Unlike
create, status is passed through as given — setting suspend is how you pause
a running campaign and active is how you resume one, so forcing a value here
would break half the use cases.
That does mean update can start spending. If you want activation to stay a
manual action in the Sklik UI, set SKLIK_BLOCK_ACTIVATION=true: status: "active" is then refused with 403 while pausing still works. Default is
false (both directions allowed). Every activation is logged at WARNING either
way.
Ads: changing the creative replaces the ad. Sklik cannot edit an existing ad's headlines, description or URLs — it deletes the old ad and creates a new one, so the ad gets a new id. Re-read the group's ads after such an update. Changing only
statuskeeps the id.
type cannot be changed on a campaign.
Remove and restore — reversible
Sklik's removal is a soft delete: "the campaign is not really removed; it is
only marked as removed". Every DELETE therefore has a matching restore
endpoint, and removed entities still show up in listings unless you filter with
is_deleted=false.
curl -X DELETE ".../sklik/campaigns?ids=123456" -H "X-Sklik-Token: <TOKEN>"
curl -X POST ".../sklik/campaigns/restore?ids=123456" -H "X-Sklik-Token: <TOKEN>"
Optional guard rails
Both are off by default — the proxy does not second-guess your numbers unless you ask it to:
| Guard | How to enable | Effect |
|---|---|---|
| Budget ceilings | Set SKLIK_MAX_DAY_BUDGET_HALERS, SKLIK_MAX_TOTAL_BUDGET_HALERS, SKLIK_MAX_CPC_HALERS to a non-zero value |
A create or update above the ceiling is rejected with 400 before Sklik is called. |
| Idempotency | Send an X-Idempotency-Key header |
A retry with the same key returns the original result instead of repeating the write. Works on every write endpoint. |
| No activation via API | Set SKLIK_BLOCK_ACTIVATION=true |
PUT refuses status: "active" with 403; pausing still works. |
Amounts are in halers (100 halers = 1 Kč), matching the Sklik API — a
ceiling mainly protects against a misplaced decimal point. GET /sklik/write-limits reports what is currently enforced (null = no limit).
Basic shape validation always applies (required fields present, money fields integer and non-negative), so you get a clear message instead of a generic upstream rejection.
Idempotency
Recommended for writes: if a create times out on the network you cannot tell whether the campaign was created, and a blind retry creates a second one.
- Send
X-Idempotency-Key: <unique string per logical operation>(e.g. a UUID). - A retry with the same key returns the stored result plus
"idempotentReplay": true— Sklik is not called again. - A failed create releases the key, so you can retry it.
- A concurrent duplicate (same key still in flight) gets
409.
Limitations, stated plainly: the store is in-memory and per-container. It does not survive a restart and is not shared between replicas, so with more than one container a retry can land somewhere that has never seen the key. It removes the common failure (an immediate retry after a timeout); it is not a distributed guarantee. Moving it to Redis should be a conscious decision, not a surprise.
Example — create a paused campaign
curl -X POST "https://services.csbot.cz/apps/analytics/sklik/campaigns" \
-H "X-Sklik-Token: <TOKEN>" \
-H "X-Idempotency-Key: 8f3a1c02-0f1e-4c3a-9d6b-2b7e5f0a1c44" \
-H "Content-Type: application/json" \
-d '[{"name":"Léto 2026","type":"fulltext","dayBudget":20000,"totalBudget":200000}]'
dayBudget: 20000 = 200 Kč/day. Response:
{
"status": 200,
"statusMessage": "OK",
"campaignIds": [123456],
"createdCount": 1,
"createdStatus": "suspend",
"idempotentReplay": false,
"note": "Created paused. Activate manually in the Sklik UI ..."
}
Then a group and an ad in it:
curl -X POST ".../sklik/groups" -H "X-Sklik-Token: <TOKEN>" \
-H "Content-Type: application/json" \
-d '[{"campaignId":123456,"name":"Sestava A","cpc":300}]'
curl -X POST ".../sklik/ads" -H "X-Sklik-Token: <TOKEN>" \
-H "Content-Type: application/json" \
-d '[{"groupId":654321,"adType":"eta","headline1":"Nadpis jedna","headline2":"Nadpis dva","description":"Popis inzerátu.","finalUrl":"https://example.com/"}]'
Report helper
entity ∈ campaigns, groups, ads, keywords, queries, sitelinks, productSets, banners. Body = the arguments for {entity}.createReport (restriction filter +
optional display options). The proxy creates the report then pages through
{entity}.readReport (100 rows/page) and returns:
{ "reportId": "...", "totalCount": 1234, "returnedCount": 1234, "truncated": false, "report": [ ... ] }
Example body for POST /sklik/report/campaigns:
[
{ "dateFrom": "2026-06-01", "dateTo": "2026-06-18", "statGranularity": "daily" },
{ "statGranularity": "daily" }
]
Generic RPC
POST /sklik/rpc/{method} with a JSON-array body of the arguments after the
session struct (which the proxy injects). Reaches any method, including
mutating ones — that is the long-standing behaviour and it is unchanged.
Calls made this way bypass the typed write endpoints' guard rails: nothing forces
status: "suspend", no budget ceiling applies and there is no idempotency. For creating campaigns preferPOST /sklik/campaigns. An operator who wants to enforce that can setSKLIK_RPC_ALLOW_MUTATIONS=false, which makes this endpoint refuse.create/.update/.remove/.delete/.restore/.setStatuswith403. Default istrue(everything allowed).
Examples:
# List campaigns
curl -X POST ".../apps/analytics/sklik/rpc/campaigns.list" \
-H "X-Sklik-Token: <TOKEN>" -H "Content-Type: application/json" \
-d '[{"statuses":["active"]}, {"displayColumns":["id","name","status"]}]'
# Account info
curl -X POST ".../apps/analytics/sklik/rpc/client.get" \
-H "X-Sklik-Token: <TOKEN>" -H "Content-Type: application/json" -d '[]'
client.loginByToken cannot be called via /sklik/rpc — the proxy manages the
session (returns 400).
Method reference
Full method list: https://api.sklik.cz/drak/. Common ones: client.get,
api.limits, campaigns.list, groups.list, ads.list, keywords.list,
*.createReport / *.readReport.