Pagination & errors
The API uses one list envelope, one cursor-pagination scheme, and one error shape everywhere. Fields are snake_case and dates are ISO-8601 UTC.
The list envelope
Every list endpoint responds with the same shape:
{
"data": [ ... ],
"has_more": true,
"next_cursor": "eyJzb3J0X3ZhbHVlIjoi...",
"total": 1284
}data— the page of items.has_more— whether another page exists.next_cursor— opaque cursor for the next page;nullon the last page.total— total item count, included where it is cheap to compute.
Cursor pagination walkthrough
Lists take ?limit= (default 100, max 500) and ?cursor=. Fetch the first page:
curl "https://api.zutrix.com/v1/projects/PROJECT_ID/keywords?limit=200" \
-H "Authorization: Bearer $ZUTRIX_API_KEY"If has_more is true, pass next_cursor back verbatim to get the next page:
curl "https://api.zutrix.com/v1/projects/PROJECT_ID/keywords?limit=200&cursor=eyJzb3J0X3ZhbHVlIjoi..." \
-H "Authorization: Bearer $ZUTRIX_API_KEY"Repeat until has_more is false. Cursors are opaque — do not parse or construct them, and keep the other query parameters identical between pages.
The error envelope
All errors are JSON with the same fields:
{
"error": "Analytics is not connected for this project",
"code": "NO_DATA",
"request_id": "req_01j8f2c9...",
"hint": "Connect Google Analytics in the Zutrix dashboard, then retry.",
"setup_url": "https://zutrix.com/dashboard/project/..."
}error— human-readable message.code— stable machine-readable code (table below). Match on this, not on the message.request_id— unique id for the request, also sent on every response (success or error) as theX-Request-Idheader. Include it when contacting support.hint— optional, what to do about it.setup_url— optional, the dashboard page where the missing setup can be completed.details— optional structured context, e.g. per-field entries onVALIDATION_ERRORor{ current_usage, limit }on quota errors.
Codes are frozen
Error codes are a stable contract: new codes may be added, but existing ones are never renamed or removed. Build your error handling on code.
Error codes
| Code | Status | Meaning |
|---|---|---|
INVALID_INPUT | 400 | A parameter or body field is malformed or unsupported. |
VALIDATION_ERROR | 422 | Request failed schema validation; details[] lists each offending field. |
UNAUTHORIZED | 401 | Missing, invalid, or revoked API key. |
FORBIDDEN | 403 | Your key is valid but lacks permission for this resource or action. |
NOT_FOUND | 404 | The resource does not exist (or is not visible to you). |
CONFLICT | 409 | The request conflicts with current state (e.g. duplicate resource). |
FEATURE_NOT_ENABLED | 400 | The feature is not set up for this project; setup_url points at the dashboard page to enable it. |
NO_DATA | 404 | The feature is available but has no data yet (e.g. analytics not connected, no tracked keywords). |
NO_RESULT | 404 | The upstream data source has no results for this request. |
RATE_LIMITED | 429 | Per-account requests/minute exceeded; retry after Retry-After seconds. |
QUOTA_EXCEEDED | 402 | A plan quota dimension is used up; details holds current_usage and limit. |
PLAN_UPGRADE_REQUIRED | 402 | Your plan does not include this feature; upgrading unlocks it. |
UPSTREAM_TIMEOUT | 504 | A data provider took too long; safe to retry shortly. |
UPSTREAM_RATE_LIMITED | 429 | A data provider is throttling this operation; retry shortly. |
UPSTREAM_ERROR | 502 | A data provider failed; retry shortly. |
INTERNAL | 500 | Unexpected server error; report it with the request_id. |
Validation errors
VALIDATION_ERROR (422) includes a details array naming each invalid field:
{
"error": "Request validation failed",
"code": "VALIDATION_ERROR",
"request_id": "req_01j8f2c9...",
"hint": "Fix the fields listed in details and retry.",
"details": [
{ "path": "limit", "message": "Number must be less than or equal to 500" }
]
}