Errors and limits
HTTP status codes, error response format, rate limits, and limit-reached responses.
Error response format#
All errors use the same envelope shape:
{
"data": null,
"error": "Human-readable error message",
"meta": { /* optional error detail */ }
}Status codes#
Every error response has a meta.code. When an endpoint has no more specific code, the code comes from the status: 400 invalid_request, 401 unauthorized, 402 limit_reached, 403 forbidden, 404 not_found, 405 method_not_allowed, 409 conflict, 410 gone, 422 unprocessable, 429 rate_limit_exceeded, 500 internal_error. The codes in the table below take priority.
| Status | Meaning |
|---|---|
400 | Bad request: invalid query parameters or request body (meta.code: invalid_query, invalid_body, invalid_params) |
401 | Unauthorized: missing, invalid, revoked or expired API key (meta.code: invalid_api_key), or a key that stopped working because of who it acts as or who created it (key_member_removed, key_creator_removed, key_creator_not_admin, key_creator_banned; see Who a key acts as) |
402 | Limit reached: org has hit a count-based plan limit (meta.code: limit_reached), or a date range exceeds tier history retention (retention_exceeded) |
403 | Forbidden: key lacks the required scope (meta.code: insufficient_scope), the member the key acts as isn't an org admin and the action needs one (insufficient_role), a write to another member's personalization document (not_your_document), or the org/user is banned |
404 | Not found: resource doesn't exist or isn't in your org. A path that matches no endpoint at all (any method) returns 404 JSON with meta.code: route_not_found and a pointer to /api/v2/openapi.json. |
409 | Conflict: a duplicate or a state conflict. Adding a handle that's already tracked in the project returns meta.code: already_tracked (see below). An Idempotency-Key reused on a different endpoint returns idempotency_conflict; one whose first request is still running returns idempotency_in_progress with a Retry-After header. A notes replace whose expected_readme_version is out of date returns notes_conflict (see Notes and documents). |
410 | Gone: removed API version. Every /api/v1/* path returns 410 (meta.code: api_version_removed). Use /api/v2 instead. |
422 | Unprocessable: an Idempotency-Key was reused with a different request body (meta.code: idempotency_key_reused). Use a new key for a new request. |
429 | Rate limited: daily API quota or per-key sliding-window exceeded (meta.code: rate_limit_exceeded) |
500 | Server error (meta.code: internal_error) |
402: Limit reached#
Returned when a write operation would exceed a count-based plan limit. The meta field contains details:
{
"data": null,
"error": "Limit reached: max_accounts_per_org",
"meta": {
"code": "limit_reached",
"limitKey": "max_accounts_per_org",
"current": 5,
"max": 5
}
}See Plans and limits for all limit keys.
409: Already tracked#
Returned by POST /api/v2/creators, POST /api/v2/projects/{id}/creators and POST /api/v2/creators/{id}/accounts when one of the handles is already tracked in the project. Nothing is created. error names the handle and the creator that tracks it, and meta carries the ids so you can use the existing creator instead:
{
"data": null,
"error": "@examplecreator (TikTok) is already tracked in this project as creator \"Sam Rivera\" (3f1c…).",
"meta": {
"code": "already_tracked",
"platform": "tiktok",
"handle": "examplecreator",
"account_id": "9a7e…",
"creator_id": "3f1c…"
}
}If the same platform and handle appear twice in one request, you get 400 with meta.code: duplicate_account instead.
Idempotency-Key errors#
Write endpoints that accept an Idempotency-Key header (see Idempotency) can return:
meta.code | Status | Meaning | What to do |
|---|---|---|---|
invalid_idempotency_key | 400 | The key is longer than 255 characters | Use a UUID |
idempotency_in_progress | 409 | A request with this key is still running. Retry-After and meta.retry_after_seconds say how long to wait | Retry with the same key and body after the delay |
idempotency_conflict | 409 | The key was already used on a different endpoint | Use a new key |
idempotency_key_reused | 422 | The key was already used with a different request body | Use a new key for the new request |
A 5xx or 429 response is never replayed: retrying with the same key runs the request again.
404: Unknown endpoint#
A request to a path that no endpoint matches returns JSON, never the website's HTML page, so a typo'd or unsupported call can't look like a success:
{
"data": null,
"error": "No API endpoint matches DELETE /api/v2/goals/3f1c….",
"meta": {
"code": "route_not_found",
"method": "DELETE",
"path": "/api/v2/goals/3f1c…",
"hint": "Check the path and method against the OpenAPI spec at /api/v2/openapi.json (docs: /docs/api-reference/overview).",
"openapi": "/api/v2/openapi.json"
}
}A known path called with a method it doesn't support returns 405.
403: Insufficient scope#
Returned when a write request (POST/PATCH/DELETE) is made with a key that lacks the write scope, or an admin endpoint (org management, PIN reads, hard deletes) is called without the admin scope. A key's admin scope only counts while its owner is an org admin.
{
"data": null,
"error": "This action requires the 'write' scope. Granted: [read]",
"meta": {
"code": "insufficient_scope",
"required": "write",
"granted": ["read"]
}
}See Authentication for the scope model.
403: Insufficient role#
A key acts as one member and can never do more than they can in the app. Endpoints that only org admins can use in the app (payments and payout setup, removing creators, manual scrapes, share pages, comment settings, organization and project documents) return this when the member isn't an org admin. The OpenAPI spec marks each of them "Requires an org admin".
{
"data": null,
"error": "You need to be an organization admin to do this. Ask an org admin to update your role in Organization settings.",
"meta": { "code": "insufficient_role", "required": "org_admin" }
}Use a key that acts as an org admin, or ask an org admin to do it.
429: Rate limited#
Two separate systems can produce 429s, and both use meta.code: "rate_limit_exceeded".
Per-key sliding window: more than 60 requests per minute from a single API key. The response carries a Retry-After header (seconds). Retry after a brief delay.
{
"data": null,
"error": "Rate limit exceeded",
"meta": {
"code": "rate_limit_exceeded",
"bucket": "v1Api",
"retryAfter": 7
}
}Daily quota: your org's daily api_requests allowance was exhausted. Resets at midnight in your org's time zone. Like the per-key limiter, this response carries a Retry-After header (seconds until the reset), so a client can back off uniformly on any 429 without special-casing which limiter fired.
{
"data": null,
"error": "Daily API request limit exceeded",
"meta": {
"code": "rate_limit_exceeded",
"retryAfter": 33421
}
}The public GET /api/v2/changelog has neither: it is limited to 60 requests a minute per IP address (bucket publicChangelog) and never counts toward the daily quota.
Handling errors#
const res = await fetch('https://www.trackagoat.com/api/v2/creators', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
if (!res.ok) {
if (res.status === 429) {
const retryAfter = body.meta?.retryAfter ?? 60;
// wait retryAfter seconds and retry
} else {
throw new Error(body.error ?? `HTTP ${res.status}`);
}
}
const creators = body.data;