Errors & rate limits
Every Ranktracker API error comes back in a predictable shape, with an HTTP
status code you can branch on and a machine-readable code you can log. This
guide covers the error envelope, what each status code means, and how to handle
throttling.
If you haven't made a request yet, start with the Quickstart and Authentication guides.
The error envelope
Errors use a JSON:API-style envelope: a top-level errors array, where each
entry has a code, a numeric status, and a human-readable detail.
{
"errors": [
{
"code": "over_quota",
"status": 402,
"detail": "You have reached your plan's keyword limit."
}
]
}
Key points:
- The HTTP response status matches the
statusfield on the error. - A single response can carry multiple entries in
errors— for example, a422validation failure may return one entry per invalid field. Iterate the array rather than reading onlyerrors[0]. - Some errors include extra fields. SERP errors (see
/rest-api/serp), for instance, add anidalongsidecode/status/detail. Treat unknown fields as additive and don't rely on their presence.
A handful of failures — notably a disabled or missing API key — are returned as
a plain 403 Forbidden HTML body rather than the JSON envelope. Always branch
on the HTTP status code first, then parse the JSON body when one is present.
Status codes
| Status | Meaning | When it happens |
|---|---|---|
400 | Bad request | A required parameter is missing or malformed (for example, a create call sent without its wrapping resource object). |
402 | Payment required | The request would exceed your plan's quota — for example, adding keywords beyond your keyword or data-row allowance. Nothing is created. |
403 | Forbidden | Your API key is missing/invalid, or your plan does not have API access enabled. |
404 | Not found | The resource doesn't exist, or it belongs to another account. Lookups are account-scoped, so a foreign UUID looks identical to one that was never created. |
422 | Unprocessable entity | The request was well-formed but failed validation — an invalid attribute, an empty required array, or (for SERP) data that isn't ready yet. |
503 | Service unavailable | The request was throttled (rate-limited). Retry with backoff — see Rate limiting. |
403 — plan access and authentication
A 403 means one of two things:
- Authentication failed — the
Authorizationheader is missing or the key is invalid. See Authentication for how keys are sent. - API access is disabled for your plan — the key is valid but your plan isn't entitled to use the REST API.
You can check whether API access is enabled for your account without triggering
an error: GET /v1/account/usage returns an apiEnabled flag in its
attributes.
curl https://api.ranktracker.com/v1/account/usage \
-H "Authorization: tkn_usr_your_api_key_here"
{
"data": {
"id": "…",
"type": "usage",
"attributes": {
"apiEnabled": true,
"keywords": { "usage": 842, "limit": 1000, "remaining": 158 },
"dataRows": { "usage": 12050, "limit": 20000, "remaining": 7950 }
}
}
}
If apiEnabled is false, upgrade or enable API access in the Ranktracker app
at https://app.ranktracker.com. See
/rest-api/account for the full response shape.
402 — over quota
Adding keywords tracks the cartesian product of words and search_engines,
so a single create call can consume many keyword and data-row credits. If the
request would push you past your plan's keyword or data-row allowance, the API
responds 402 and links nothing — the operation is all-or-nothing, so you
never end up with a partially-applied batch.
{
"errors": [
{
"code": "over_quota",
"status": 402,
"detail": "Tracking these keywords would exceed your plan's data-row limit."
}
]
}
To avoid 402s, check remaining allowance before a large add — the remaining
values from GET /v1/account/usage tell you how much headroom you have. See the
Usage & quota guide for the full workflow, and
Bulk operations for batching keywords safely.
422 — validation
A 422 means the request reached the resource but failed validation. Read every
entry in the errors array to surface each problem — creating a domain with a
bad match_type, bulk-tagging with an empty keyword_uuids array, or
requesting a SERP that hasn't been captured yet all return 422.
{
"errors": [
{
"code": "invalid",
"status": 422,
"detail": "match_type is not included in the list"
}
]
}
Rate limiting
The API throttles bursts of traffic. When you're throttled, the request is rejected with:
HTTP/1.1 503 Service Unavailable
:::warning No rate-limit headers yet
There are currently no RateLimit-* headers on responses — you can't read a
remaining-request count or a reset timestamp from the API. Treat a 503 itself
as the signal to slow down.
:::
Handling 503 with exponential backoff
When you receive a 503, wait and retry, increasing the delay after each
consecutive failure (add a little random jitter so parallel clients don't retry
in lockstep). Cap the number of attempts so a persistent outage doesn't loop
forever, and only retry idempotent-safe work — a GET is always safe to retry;
for writes, be aware a 503 after a partial send is rare but possible.
import time, random, requests
BASE_URL = "https://api.ranktracker.com"
HEADERS = {"Authorization": "tkn_usr_your_api_key_here"}
def get_with_backoff(path, max_retries=5):
for attempt in range(max_retries):
resp = requests.get(f"{BASE_URL}{path}", headers=HEADERS)
if resp.status_code != 503:
resp.raise_for_status()
return resp.json()
# Throttled: back off 1s, 2s, 4s, 8s… plus jitter.
delay = (2 ** attempt) + random.random()
time.sleep(delay)
raise RuntimeError(f"Still throttled after {max_retries} attempts")
domains = get_with_backoff("/v1/domains")
const BASE_URL = "https://api.ranktracker.com";
const HEADERS = { Authorization: "tkn_usr_your_api_key_here" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function getWithBackoff(path, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const resp = await fetch(`${BASE_URL}${path}`, { headers: HEADERS });
if (resp.status !== 503) {
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
// Throttled: back off 1s, 2s, 4s, 8s… plus jitter.
const delay = 2 ** attempt * 1000 + Math.random() * 1000;
await sleep(delay);
}
throw new Error(`Still throttled after ${maxRetries} attempts`);
}
const domains = await getWithBackoff("/v1/domains");
:::tip Stay well under the limit
The cheapest way to avoid 503s is to not hit them: page through large result
sets with a sensible per_page (see Pagination), batch
writes where an endpoint supports it (see
Bulk operations), and cache responses that don't
change on every request.
:::
Quick reference
- Branch on the HTTP status first, then parse the JSON
errorsarray. 400/422are your bug — fix the request; they won't succeed on retry.402is a quota wall — check Usage & quota before large adds.403is auth or plan access — verify your key andapiEnabled.404is account-scoped — a foreign UUID looks the same as a missing one.503is transient — retry with exponential backoff and jitter.