Skip to main content
Every status code the /v1 API returns is below, with what causes it and what you do about it.

Error shape

Every error is JSON with an error string:
/ads adds a stable code. /commerce-metrics and the cost-import routes add a stable reason. Branch on code or reason rather than the error text, which we reword.

Status codes

Never treat 503 from /ads or /commerce-metrics as zero spend or zero revenue. These endpoints fail loudly on purpose, so that a read you could not complete never lands in a report as a real number.

Rate limit

Write endpoints spend a second, smaller budget on top of this one: 20 write requests per key per minute, including dry runs. A write therefore needs room in both budgets. The limit counts requests, not rows. One /events request for 1000 rows costs the same as one /workspace request. Every /v1 response carries your current budget, so you do not have to be rejected to find it. The first two headers are present on success and on error responses.

Per-key limits

The workspace owner sets a named key’s limit in Settings → API. See API keys. The plan caps it. GET /v1/usage and GET /v1/context return the limit in effect as rate_limit.

When the limit store is unreachable

Reads and writes behave differently on purpose. A read outage is a wasted query, so a store outage must not take every read down with it. Reads fall back to a standby limit of 30 requests per 60 seconds. That limit is per server, not per key, so your effective budget during an outage is smaller than 100 and is not exactly predictable. Treat X-RateLimit-Degraded: 1 as a signal to slow down. A write that lands twice costs a duplicate conversion rule or a duplicate postback, so writes stop rather than guess. Send an Idempotency-Key on every write, and a retry after a store outage stays safe.

Key requests and token exchange

These routes create credentials, so their limits fail closed. When the limit store is unreachable they return 503.

Parameter limits

We clamp a value above the maximum instead of returning 400, so an oversized limit still gives you a page of data.

Pagination

/events and /users page with limit and offset, and both return has_more. Walk a page at a time, adding limit to offset, and stop when has_more is false. Do not stop on a short page instead, because a full range can still end on one. Keep every other parameter identical across the walk, and keep the date range fixed, or the pages will overlap. On /users, offset plus limit must stay at 5000 or less, and a larger window returns 400 rather than a truncated page. Narrow the date range or pass search when you need to go deeper. On /events, total_count counts the whole range, not the page. With property_filters and no include_total, it is exact on the last page and a lower bound before it, flagged by total_count_is_lower_bound: true. Send include_total=true for the exact count on every page. That count reads the range a second time, so the request takes longer.

Ingest limits

Event ingest runs on a different host, https://ingest.datalyr.com, and has its own limits. The two ingest transports carry different limits. /track takes one server event per request. The SDK batch transport takes an array. The batch limit counts events, not requests. A batch of 50 events costs 50. The /track limit counts requests, which is the same thing there, because each request carries one event. See Ingest API for the full /track contract.

Verify a limit

Send 101 /v1/workspace requests inside one minute with a key at the default limit. Request 101 returns 429 and the header Retry-After: 60.

Next