14 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. |
| GET | /sklik/keywords |
keywords.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, keywords. 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.
The bodies are typed in Swagger
Create and update bodies are modelled (app/sklik_models.py), so /docs shows
every field with its type, whether it is required, the allowed enum values
(campaign type, status, adType, adSelection, paymentMethod,
premiseMode, videoFormat) and a description with the unit. "Try it out" is
therefore fillable without reading the Sklik docs first.
Two properties of the models matter:
- They do not restrict you. Every model accepts undeclared fields and forwards them to Sklik untouched, so the less common parts of the API (retargeting, product sets, anything Sklik adds later) keep working without a code change here. The schema documents the common path; it is not a whitelist.
- Update is genuinely partial. Only the fields you actually send are
forwarded, so omitting a field leaves it alone rather than resetting it. An
explicit
nullis still sent (that is how you cleartotalBudget).
Schema violations (missing required field, bad enum value) come back as 422
with the offending field in detail[].loc — before any Sklik call. The business
guard rails (budget ceilings, blocked activation) return 400 / 403.
Create — campaigns, groups and ads are 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 campaign, group and ad, and ignores any
other value you send (the override is logged).
Keywords are the exception and are not forced. A keyword cannot spend anything on its own — the campaign, group and ad above it are all created paused, and those gate the spending. Forcing keywords paused as well would only create a trap: you activate the campaign in the Sklik UI, nothing happens, and the cause is a fourth paused level nobody expected. Sklik's own default (
active) applies; sendstatusexplicitly if you want otherwise.GET /sklik/write-limitslists which entities are force-paused.
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.
Immutable fields, so PUT does not offer them at all:
| Entity | Cannot be changed |
|---|---|
| campaign | type |
| keyword | name, matchType — remove it and create a new one |
| ad | the creative (see the note above) |
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.