Errors
Every failure carries a machine-readable code, a message, and usually a hint about what to do.
Branch on code, not on the message. Messages get reworded; codes do not.
{
"error": {
"code": "no_coverage",
"message": "Nobody is reporting within 2 cells of that location in the last 60 minutes.",
"hint": "Coverage is a mosaic, not a grid. Widen `window`, raise `max_rings`, or call /v1/cells to see where data exists.",
"docs": "https://openaqi.net/docs/errors",
"request_id": "req_ce3715fc8042498fa21b"
}
}| Code | HTTP | Retry? | What to do |
|---|---|---|---|
unauthorized | 401 | no | Check the key. Read keys start with oaq_; a sensor key will not work here. |
forbidden_origin | 403 | no | The key is origin-locked. Add the origin, or use an unlocked key from a server. |
invalid_request | 400 | no | A parameter was missing or out of range. The message says which. |
no_coverage | 404 | no | Nobody is reporting nearby. Widen window or max_rings, or show an empty state. |
rate_limited | 429 | yes | Wait Retry-After. Caching is usually the real fix. |
internal | 500 | yes | Ours. Retry with backoff and quote request_id if it persists. |
no_coverage is normal
It is the most common error and it is not a failure. Roughly half the planet has no contributor within 12 km, and the honest answer to "what is the air like here" is sometimes "nobody knows".
Treat it as an empty state rather than an exception:
try {
const { data } = await openaqi.air({ lat, lon });
render(data);
} catch (err) {
if (err instanceof OpenaqiError && err.code === "no_coverage") {
return renderNoDataNearby(); // not an error screen
}
throw err;
}Request IDs
Every response, success or failure, carries X-Request-Id. Quote it and we can
find the exact request rather than asking you to reproduce it.
Retrying
Only rate_limited and internal are worth retrying. Retrying a 401 three
times is three wasted round trips and a slower error message.
The SDK does this for you, with exponential backoff, jitter, and
Retry-After honoured exactly.