Documentation / Core concepts
Getting started
Reference
Core concepts
Errors
One envelope, one machine-readable code, and a message written for the person reading the log. Branch on the code; show the message.
Every failure replaces the whole body with a single error object. There is no partial success and no mixed response.
The envelope
Every error
code— a stable, machine-readable slug. This is the contract. Switch on it.message— one sentence for a human. Show it, log it, but never parse it: the wording may be improved at any time and is not versioned.
Some codes add fields. The extras are always additive, so a client that reads only code and message keeps working:
402addsrequired,balanceandtop_up_url.429addsretry_after, mirroring theRetry-Afterheader.- A parameter that fails validation adds
details— an array naming the parameter and the rule it broke.
Validation failure · 400
Every code
The full list, grouped by status. If you implement handling for six of these, make them invalid_key, insufficient_credits, display_only, too_many_rows, rate_limited and internal.
400 — the request is malformed
Something in the query string is wrong. These are deterministic: the same request will fail the same way every time, so retrying is never the answer.
| HTTP | Code | Cause | What to do |
|---|---|---|---|
| 400 | bad_request | A query parameter failed validation — wrong type, or out of range. | Read the details array in the body; it names the parameter and the rule it broke. |
| 400 | bad_format | format was neither json nor csv on /v1/data. | Send format=json or format=csv. Nothing else is accepted. |
| 400 | bad_range | from is greater than to on /v1/data. | Swap them. Both bounds are inclusive. |
| 400 | bad_year | year on /v1/rankings was neither latest nor four digits. | Send year=latest or year=2024. |
| 400 | bad_sort | sort on /v1/indicators is not one of the five accepted values. | Use name, coverage, latest, topic or source. |
| 400 | bad_dir | dir on /v1/indicators was not asc or desc. | Use asc or desc — and note dir only applies when sort is also set. |
| 400 | too_many_rows | A /v1/data query matches more than 100,000 rows. | The message carries the real count. Chunk by countries or narrow the year range. This costs 0 credits. |
401, 402, 403 — credentials, credits, licence
Three different refusals that are easy to confuse. 401 is who you are, 402 is what you hold, 403 is what the data permits — and only the second is fixed by money.
| HTTP | Code | Cause | What to do |
|---|---|---|---|
| 401 | unauthorized | No credentials on a route that costs credits. | Send Authorization: Bearer wgd_live_… — or call one of the free endpoints instead. |
| 401 | invalid_key | The key is unknown, malformed, revoked, expired, or its account is closed. | One code covers all of those on purpose. Check the key you deployed; if in doubt, mint a new one. |
| 402 | insufficient_credits | Your balance is below what the request needs. | The body carries required, balance and top_up_url. Top up, then retry the identical request. |
| 403 | display_only | The indicator's licence permits display but not redistribution. | Not a billing problem and topping up will not clear it. Read /docs/licensing. |
| 403 | forbidden | The generic 403, for a refusal that is not one of the two specific codes above. Rare on the data API. | Check the indicator on /v1/indicators/{id}. If it is absent from the catalogue, it is not servable at all. |
| 403 | key_scope | The key lacks the scope a route requires. | Reserved. Every key today carries read, and no public endpoint requires more. |
404, 405, 409 — not there
The route, the resource, or the method does not exist.
| HTTP | Code | Cause | What to do |
|---|---|---|---|
| 404 | indicator_not_found | No enabled, redistributable indicator has that id. | Ids are exact and case-sensitive. Search for it with /v1/indicators?q=… |
| 404 | country_not_found | No enabled country has that ISO3 code. | Use the three-letter ISO 3166-1 alpha-3 code. /v1/countries lists every one we serve. |
| 404 | no_rankings | The indicator exists but has no ranked years at all. | Indicators flagged no_rank are never ranked. Read the values from /v1/data instead. |
| 404 | not_found | The path does not exist. | Check the base URL and the spelling. Every public route is listed on /docs/endpoints. |
| 405 | method_not_allowed | You sent something other than GET. | The whole public API is read-only. Every route is a GET. |
| 409 | conflict | An account operation collided with existing state. | Account API only; the public data API never returns it. |
423, 429 — too many
Both carry Retry-After, and it is authoritative.
| HTTP | Code | Cause | What to do |
|---|---|---|---|
| 423 | account_locked | Too many failed sign-in attempts from one address or one account. | Wait for the period in Retry-After. Sign-in only — a valid key is never locked out this way. |
| 429 | rate_limited | You exceeded the per-minute, per-day or concurrent limit for your tier. | Sleep for Retry-After seconds, then retry. See /docs/rate-limits for a correct backoff. |
500, 503 — our side
Retry these, with backoff. Nothing about your request needs to change.
| HTTP | Code | Cause | What to do |
|---|---|---|---|
| 500 | internal | We broke. Never your fault, never a stack trace in the body. | Retry with backoff. If it persists, send us the X-Request-Id. |
| 503 | accounts_unavailable | The accounts layer is deploying or not yet migrated. | Free endpoints keep working throughout. Retry the metered ones shortly. |
| 503 | payments_unavailable | No payment provider is configured. | Account API only. Credits can still be granted; the data API is unaffected. |
The ones you will actually meet
401 · no key, or a key we do not accept
unauthorized · 401
unauthorized means the header was missing or empty on a route that costs credits. invalid_key means we read a credential and rejected it — unknown, malformed, revoked, expired, or belonging to a closed account. All five collapse into one code deliberately: telling them apart would help someone probing the key space and helps you not at all, because the fix is the same in every case. Check what you deployed. See Authentication.
402 · out of credits
insufficient_credits · 402
Nothing was served and nothing was charged. Do not retry — a 402 is not transient. Stop the loop, record where you stopped, top up, and re-send the identical request. Full handling advice on Credits.
403 · the licence says no
display_only · 403
This is the one refusal money cannot clear. The indicator’s provider permits display but not redistribution, so the value is visible on the portal and not exportable here. Check access_tier on the indicator record before you build a pipeline around a series. Licensing has the detail.
400 · too many rows
too_many_rows · 400
The message carries the real count, which is what makes it actionable — divide it by 100,000 and you know how many chunks you need. Nothing was served and no credits were spent: the count runs before any data is read. Pagination has a complete chunking recipe.
429 · slow down
rate_limited · 429
Sleep for Retry-After seconds, then retry the same request. It costs no credits. A correct backoff, in five languages, is on Rate limits.
5xx · our fault
A 500 never leaks a stack trace, a SQL fragment or an internal hostname; what it does carry is a request id, which also appears in our logs. Retry with backoff. If it persists, send us the X-Request-Id from the response headers and we can find the exact request without you having to reproduce anything.
503 accounts_unavailable is a specific, expected state: the accounts layer is deploying or has not been migrated yet. The free catalogue endpoints keep working throughout, by design — a deploy of the metering layer never takes the data API down.
Handling errors well
- Retry 429, 5xx and transport failures. Nothing else. Every other 4xx is deterministic; retrying is guaranteed not to help and spends your quota proving it.
- Treat 402 as a budget event, not an outage. Page whoever owns the account, not whoever is on call.
- Treat 403 display_only as data, not as a failure. It is a permanent property of that indicator. Record it and skip the series; do not build a retry around it.
- Log the request id. One field, attached to every response, that turns “it failed sometimes last Tuesday” into a single log line we can look up.
- Default unknown codes to non-retryable. New codes may appear. Failing loudly on one you do not recognise is safer than looping on it.