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:
Over the limit the API answers 429 with Retry-After (seconds):
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=csvon/reportsto stream up to 200,000 rows in one request. - Page lists with the largest
limitthe 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:
{
"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#
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#
Reads (GET) are always safe to retry. For writes, avoid duplicates:
POST /conversionswith anadv_txidis idempotent in effect: a second conversion with the sameadv_txidon the same offer is refused with409 conflict.PATCHrequests set absolute values, so repeating them is harmless.POST /offers,/affiliates,/advertisersand/postbackscreate a new record each time; on a timeout, list first to check whether the first attempt succeeded.
Example back-off in 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.