Skip to content
Browse documentation

Rate limits & errors

The per-key rate limit and its headers, the JSON error shape, every error code with its HTTP status, and how to retry safely.

Last updated 3 min read

On this page

Rate limit#

Each API key may make 600 requests per minute, counted in fixed one-minute windows. The MCP server shares the same budget. Every response reports where you stand:

HeaderMeaning
X-RateLimit-LimitRequests allowed per window (600).
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetSeconds until the window resets.
X-Request-IdUnique ID of this request — include it when you contact support.

Over the limit the API answers 429 with Retry-After (seconds):

Text
HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 17

{"error":{"code":"rate_limited","message":"Rate limit of 600 requests per minute exceeded."}}

To stay under the limit:

  • Prefer one report (GET /reports, up to 10,000 rows per page) over many small requests.
  • Use format=csv on /reports to stream up to 200,000 rows in one request.
  • Page lists with the largest limit the endpoint allows (clicks 1,000, conversions 500, others 200).
  • Use separate keys for separate systems: limits are per key.

Error shape#

Every error has the same JSON body and a meaningful HTTP status:

JSON
{
  "error": {
    "code": "validation_failed",
    "message": "The request body failed validation.",
    "details": {
      "fields": {
        "payout": "Enter a decimal amount with at most 4 decimals, e.g. \"12.50\".",
        "bogus": "Unknown field."
      }
    }
  }
}

code is stable and machine-readable; message is for humans and may change; details is present when there is more to say (details.fields for body validation, details.param for a bad query parameter, details.field for a conflict).

Error codes#

StatuscodeMeaning
400invalid_parameterA query parameter is invalid; details.param names it. Also returned when a key's role may not use a requested metric or dimension.
400invalid_cursorThe cursor is malformed or belongs to another list.
400invalid_jsonThe body is empty or not valid JSON.
400validation_failedThe body failed validation; details.fields maps field paths to messages (unknown fields included).
400invalid_requestThe request is not possible as sent; the message explains why.
401missing_api_keyNo key was sent.
401invalid_api_keyMalformed, unknown or revoked key.
403ip_not_allowedThe key's IP allowlist does not include the caller.
403forbiddenThe key's role may not use this endpoint or object.
403read_only_keyAn analyst key tried to change something.
403workspace_inactiveThe workspace is not active.
403affiliate_inactivePartner key of a partner that is not active.
403advertiser_inactiveAdvertiser key of an inactive advertiser.
403not_approvedPartner key: not approved to run the offer yet (tracking links).
404not_foundThe object does not exist or is not visible to this key; also unknown /api/v1/ paths.
409conflictA unique value is taken (details.field), e.g. an offer code or an adv_txid.
409in_useThe record is still referenced (e.g. an event with conversions).
409no_tracking_domainThe workspace has no active tracking domain to build links with.
413payload_too_largeBody over 512 KB.
415unsupported_media_typeBody not sent as application/json.
429rate_limitedToo many requests — see above.
500internal_errorUnexpected error; the message contains the request ID.
503unavailableTemporary; for example a new offer or affiliate code could not be allocated. Retry.

The tracking domain's own endpoints (/pb, /px, /lp, click links) have different answers; see Error and reason codes and Postback & tracking URLs.

Retrying safely#

SituationRetry?
429Yes, after Retry-After seconds.
500, 503, network errorsYes, with exponential back-off.
4xx other than 429No — fix the request.

Reads (GET) are always safe to retry. For writes, avoid duplicates:

  • POST /conversions with an adv_txid is idempotent in effect: a second conversion with the same adv_txid on the same offer is refused with 409 conflict.
  • PATCH requests set absolute values, so repeating them is harmless.
  • POST /offers, /affiliates, /advertisers and /postbacks create a new record each time; on a timeout, list first to check whether the first attempt succeeded.

Example back-off in JavaScript:

JavaScript
async function api(path, init = {}, attempt = 0) {
  const res = await fetch(`https://app.trackcrown.com/api/v1${path}`, {
    ...init,
    headers: { Authorization: `Bearer ${process.env.ATK}`, "Content-Type": "application/json", ...init.headers },
  });
  if ((res.status === 429 || res.status >= 500) && attempt < 5) {
    const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt;
    await new Promise((r) => setTimeout(r, wait * 1000));
    return api(path, init, attempt + 1);
  }
  const body = res.status === 204 ? null : await res.json();
  if (!res.ok) throw Object.assign(new Error(body.error.message), { code: body.error.code, requestId: res.headers.get("X-Request-Id") });
  return body;
}

See also: Pagination · Authentication & API keys.

Something unclear or missing in this article?Tell us