Skip to main content

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 status field on the error.
  • A single response can carry multiple entries in errors — for example, a 422 validation failure may return one entry per invalid field. Iterate the array rather than reading only errors[0].
  • Some errors include extra fields. SERP errors (see /rest-api#reference/tag/serp), for instance, add an id alongside code / status / detail. Treat unknown fields as additive and don't rely on their presence.
note

A handful of failures — a missing or invalid API key (401) and a plan without API access (403) — are returned with an empty 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​

StatusMeaningWhen it happens
400Bad requestA required parameter is missing or malformed (for example, a create call sent without its wrapping resource object).
402Payment requiredThe request would exceed your plan's quota — for example, adding keywords beyond your keyword or data-row allowance. Nothing is created.
401UnauthorizedThe Authorization header is missing or the key is invalid. The body is empty. See Authentication.
403ForbiddenYour plan does not have API access enabled (a missing or invalid key is a 401, see Authentication).
404Not foundThe 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. The body is the envelope with code not_found and a detail naming the resource, e.g. Domain not found.
422Unprocessable entityThe request was well-formed but failed validation — an invalid attribute, an empty required array, or (for SERP) data that isn't ready yet.
429Too many requestsThe request was throttled (rate-limited). Wait for the interval in Retry-After, then retry — see Rate limiting.

403 — plan access​

A 403 means API access is disabled for your plan — the key is valid but your plan isn't entitled to use the REST API. A key that is missing or invalid is a 401 instead; see Authentication for how keys are sent.

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#reference/tag/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"
}
]
}

422 from keyword create — invalid_reference_value​

POST /v1/domains/{domain_uuid}/keywords validates its catalogue fields — frequency and each entry's name, device, location and language — before it creates anything, and reports every failure in one response. Each entry carries the machine-readable code invalid_reference_value, a JSON pointer to the exact field, and a meta block you can act on without parsing prose. This is what sending a place name as the location returns:

{
"errors": [
{
"code": "invalid_reference_value",
"status": 422,
"title": "Unknown location",
"detail": "Unknown location \"Dijon, France\" for search engine \"google\". Send the Google Ads geo-target id as a string (e.g. \"2250\" for France, \"1005850\" for Dijon) or an ISO 3166-1 alpha-2 country code (e.g. \"FR\"). See https://developers.ranktracker.com/guides/reference-values. Look it up with GET /v1/locations?search_engine=google&q=Dijon",
"source": { "pointer": "/search_engines/0/location" },
"meta": {
"field": "location",
"value": "Dijon, France",
"search_engine": "google",
"lookup": "/v1/locations?search_engine=google&q=Dijon",
"docs": "https://developers.ranktracker.com/guides/reference-values"
}
}
]
}
FieldMeaning
titleUnknown <field> when the value isn't recognised (Unknown location, Unknown language, Unknown device, Unknown name, Unknown frequency), or Missing <field> when the key was absent or blank (Missing location).
detailHuman-readable: the value as sent, the engine it was checked against, and how to find a valid value.
source.pointerJSON pointer into your request body: /frequency, or /search_engines/<index>/<field>.
meta.fieldThe field name (frequency, name, device, location, language).
meta.valueThe value you sent, as a string (an integer location id is echoed as its digits; values longer than 100 characters are truncated). null when the key was absent from the request; a blank or whitespace-only string is echoed as sent. In both cases the title is Missing <field> and the detail starts No <field> given.
meta.search_engineThe engine the value was checked against — present only for location and language.
meta.allowedThe full list of accepted values — present only for frequency, name and device, which have short fixed lists.
meta.lookupA GET /v1/locations or GET /v1/languages query that lists the accepted values for that engine — present only for location and language. For a location, the segment of your value before its first comma is pre-filled as q ("Dijon, France" → q=Dijon) unless your whole value is an id or ISO code, or the segment is shorter than two characters (the endpoint's minimum q). A postal-code name such as "21000, France" still pre-fills q=21000, which finds the catalogue's 21000,Bourgogne-Franche-Comte,France row.
meta.docsAlways https://developers.ranktracker.com/guides/reference-values.

Errors are ordered frequency first, then per search_engines entry: name, device, location, language. When an entry's name is unknown, its location and language are not checked (they are engine-scoped), so fix the engine and resend to see any remaining problems with that entry. Sending the Global language 0000 is rejected with its own message (The Global language (0000) cannot be tracked via the API; send a language code such as "fr"). See Reference values for every accepted value and the lookup endpoints.

422 for a malformed request — validation_failed​

If the body has the wrong shape rather than an unknown value — no frequency, words that is not an array of strings or has no non-blank string, or search_engines that is not a non-empty array of objects — the response is a single validation_failed error with the same source.pointer / meta.field envelope, so one decoder handles both:

{
"errors": [
{
"code": "validation_failed",
"status": 422,
"title": "Invalid search_engines",
"detail": "search_engines must be a non-empty array of objects",
"source": { "pointer": "/search_engines" },
"meta": { "field": "search_engines" }
}
]
}

The lookup endpoints use the same two codes for their own query parameters: an unknown search_engine or a malformed country on GET /v1/locations is an invalid_reference_value (source.pointer /search_engine or /country), and a q that is too short, too long or not a string is a validation_failed (q must be at least 2 characters, q must be a string).

Rate limiting​

Requests authenticated with an API key are limited to:

ScopeLimit
Per API key300 requests / minute
Per IP address600 requests / minute

The per-key budget is shared between the REST endpoints and the GraphQL endpoint — they draw on the same 300 requests a minute, so a client using both should count them together.

The per-IP limit is a backstop, deliberately set well above the per-key limit. A single well-behaved key never reaches it; it exists to catch one source spreading traffic across several keys to multiply its ceiling.

The 429 response​

When you exceed a limit, the request is rejected with 429 Too Many Requests:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 37
RateLimit-Limit: 300
RateLimit-Remaining: 0
RateLimit-Reset: 37

The body is the standard error envelope, with the machine-readable code rate_limited:

{
"errors": [
{
"status": 429,
"code": "rate_limited",
"detail": "Rate limit exceeded. Retry after 37 seconds."
}
]
}
HeaderMeaning
Retry-AfterSeconds to wait before retrying.
RateLimit-LimitThe ceiling of the limit you hit.
RateLimit-RemainingRequests left in the window — always 0 on a 429.
RateLimit-ResetSeconds until the window resets (same value as Retry-After).

:::warning These headers appear on 429 responses only RateLimit-* and Retry-After are sent only when you are throttled, not on successful responses. There is no way to poll your remaining quota ahead of time — a successful 200 tells you nothing about how close you are to the limit. Budget your request rate client-side rather than trying to read it back. :::

Handling a 429​

The limit is a fixed window, not a sliding one, and Retry-After tells you exactly how many seconds remain in the current window. So the right response to a 429 is simply to wait that long and try again — guessing with exponential backoff makes you wait longer than necessary, and retrying sooner just burns another rejected request.

Keep a fallback delay for the rare case where the header is missing or unparseable, cap the number of attempts so a persistent problem doesn't loop forever, and add a little jitter so parallel workers don't all resume in lockstep.

import time, random, requests

BASE_URL = "https://api.ranktracker.com"
HEADERS = {"Authorization": "tkn_usr_your_api_key_here"}

def retry_after_seconds(resp, attempt):
"""Seconds to wait, from Retry-After — falling back to 2^attempt."""
try:
return max(0, int(resp.headers["Retry-After"]))
except (KeyError, TypeError, ValueError):
return 2 ** attempt

def get_with_retry(path, max_retries=5):
for attempt in range(max_retries):
resp = requests.get(f"{BASE_URL}{path}", headers=HEADERS)
if resp.status_code != 429:
resp.raise_for_status()
return resp.json()
# Throttled: wait out the window, plus jitter so parallel
# workers don't all resume on the same tick.
time.sleep(retry_after_seconds(resp, attempt) + random.random())
raise RuntimeError(f"Still throttled after {max_retries} attempts")

domains = get_with_retry("/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));

// Seconds to wait, from Retry-After — falling back to 2^attempt.
function retryAfterSeconds(resp, attempt) {
const seconds = Number.parseInt(resp.headers.get("Retry-After"), 10);
return Number.isNaN(seconds) ? 2 ** attempt : Math.max(0, seconds);
}

async function getWithRetry(path, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const resp = await fetch(`${BASE_URL}${path}`, { headers: HEADERS });
if (resp.status !== 429) {
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
return resp.json();
}
// Throttled: wait out the window, plus jitter so parallel
// workers don't all resume on the same tick.
await sleep(retryAfterSeconds(resp, attempt) * 1000 + Math.random() * 1000);
}
throw new Error(`Still throttled after ${maxRetries} attempts`);
}

const domains = await getWithRetry("/v1/domains");

:::tip Stay well under the limit The cheapest way to avoid 429s 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 errors array.
  • 400 / 422 are your bug — fix the request; they won't succeed on retry.
  • 402 is a quota wall — check Usage & quota before large adds.
  • 401 is your key — verify the Authorization header.
  • 403 is plan access — check apiEnabled.
  • 404 is account-scoped — a foreign UUID looks the same as a missing one.
  • 429 is transient — wait the Retry-After interval, then retry.