Files
analytics/documentation/sklik.md
T
2026-07-20 09:02:42 +02:00

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 POST to the base URL with the method name appended to the path.
  • Body is a JSON array of positional arguments.
  • client.loginByToken takes 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 refreshed session, and method-specific data. 200, 206 and 301 are 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)

  1. Přihlaste se na sklik.cz.
  2. Vpravo nahoře uživatelské jméno → Nastavení.
  3. Sekce Přístup k API DrakZobrazit token.
  4. 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.list filter supports only ids and isDeleted — there is no status filter upstream, so filter on status in the returned rows. groups.list and ads.list do support parent filters (campaign.ids, group.ids), which is what campaign_ids / group_ids map 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 null is still sent (that is how you clear totalBudget).

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; send status explicitly if you want otherwise. GET /sklik/write-limits lists 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 status keeps 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

entitycampaigns, 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 prefer POST /sklik/campaigns. An operator who wants to enforce that can set SKLIK_RPC_ALLOW_MUTATIONS=false, which makes this endpoint refuse .create / .update / .remove / .delete / .restore / .setStatus with 403. Default is true (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.