Using the API
Errors
The error envelope and every error code, with its status and what to do about it.
One shape, everywhere
Every error — including a 404 for a path that does not exist — is JSON in the same envelope, with a status code that means what it says.
{
"error": {
"code": "plan_required",
"message": "'history' requires the pro plan or above.",
"request_id": "req_…",
"details": {
"capability": "history",
"required_plan": "pro"
}
}
}- Branch on
code. Codes never change; messages may be reworded. - Plan refusals name
details.required_plan, the cheapest plan that would have answered, anddetails.capability, so a client can show the right upgrade prompt without a second request. - Codes with more to say carry
details: the comparability classes found, the newest data available. - Quote
request_idwhen you write to us: it finds the request in our logs.
No route fetches from an operator at request time: every answer is served from stored data, so a request here cannot fail because an operator’s website is down. When a source fails to import, its last published data keeps serving and its health says so; source_not_available is only for a source that has never published or has been switched off.
Request
| Code | Status | When | What to do |
|---|---|---|---|
| invalid_parameter | 400 | A parameter is malformed, out of range, not accepted by the route, a sort key not on its allow-list, or a limit above your plan's page size. | The message names the parameter and what was expected. Fix the request; retrying it unchanged fails the same way. |
| invalid_cursor | 400 | The cursor is not one the API issued, or was issued for a different query. | Start again without a cursor. The API refuses rather than silently restarting at page one. |
| route_not_found | 404 | No endpoint exists at this path. The answer is still JSON in the error envelope. | Check the path against /v1/openapi.json. |
Key and plan
| Code | Status | When | What to do |
|---|---|---|---|
| unauthorized | 401 | The key is missing, malformed or revoked, on a route that needs one. | Send Authorization: Bearer with a live or test key from the dashboard. |
| plan_required | 403 | The route or parameter needs something your plan does not include: history, the change feed, as_of, exports, webhooks, or a value above the plan's limit such as a nearby radius. | details.capability names what was missing and details.required_plan the cheapest plan that would answer. Upgrade, or drop the parameter. |
Data
| Code | Status | When | What to do |
|---|---|---|---|
| country_not_supported | 404 | The country code is valid but the API has nothing registered for it. | GET /v1/coverage lists what is covered. |
| operator_not_supported | 404 | The operator is known, but none of its publications is registered. | See its page for what it publishes; GET /v1/operators?covered=true lists covered operators. |
| operator_not_found | 404 | No operator has this id or slug. | GET /v1/operators lists them. |
| asset_not_found | 404 | No asset has this id. A merged asset's id keeps resolving, so this means it never existed. | Search with GET /v1/substations. |
| source_not_found | 404 | No source has this id. | GET /v1/sources lists them. |
| capacity_not_available | 404 | The asset (or postcode) exists, but no capacity record is published for it. | Nothing to retry: the operator publishes no figure there. /v1/substations?has_capacity=true lists assets that have one. |
| source_not_available | 409 | The source you named is disabled, registry-only, or has never published. | GET /v1/sources/{id}/status says which. Last published data, where there is any, keeps serving. |
| source_data_stale | 409 | Nothing satisfies fresh_after. details.newest is the newest data available. | Loosen fresh_after, or treat it as the answer you asked for: no data rather than old data. |
| capacity_semantics_not_comparable | 422 | comparability=strict and the matching records span more than one comparability class. details.classes lists them. | Narrow the query to one class (by source, country or methodology), or use best_effort and read meta.warnings. |
| source_rights_restricted | 451 | The request names only sources whose rights do not permit serving values. | Their records are still listed in queries that include other sources, with the value withheld and a link to the operator. |
Limits
| Code | Status | When | What to do |
|---|---|---|---|
| rate_limit_exceeded | 429 | Too many requests per second for your plan. | Wait Retry-After seconds. Page with cursors one page at a time rather than in parallel. |
| quota_exceeded | 429 | This month's request quota is used up. | Retry-After points at the start of the next UTC month. RateLimit-Remaining shows it coming. |
Exports and webhooks
| Code | Status | When | What to do |
|---|---|---|---|
| export_limit_exceeded | 429 | The account already has three exports queued or running. | Wait for one to finish (GET /v1/exports/{id}), then create the next. details.limit is the ceiling. |
| download_limit_exceeded | 429 | Every export download slot on the service is in use. | Retry after the seconds in Retry-After. The file is still there. |
| export_not_ready | 409 | The export is queued, running or failed, so there is no file to download. | Poll GET /v1/exports/{id} until status is succeeded. details.status says where it is. |
| export_expired | 410 | The export's file has expired and was deleted: seven days after it was built, or earlier when the rights of a source changed after it was created. | Create the export again. |
| export_not_found | 404 | No export of yours has this id. | GET /v1/exports lists yours. |
| webhook_not_found | 404 | No webhook endpoint of yours has this id. | GET /v1/webhooks lists yours. |
Server
| Code | Status | When | What to do |
|---|---|---|---|
| internal_error | 500 | Something failed on our side. The detail is logged, not sent. | Retry with backoff. If it persists, send us the request_id. |