Core concepts
Errors
Every failure returns the same machine-readable shape, so you write one error handler rather than one per endpoint.
One shape, every endpoint
Errors follow RFC 9457 Problem Details and are served as application/problem+json. Branch on status, log detail, and show the user something of your own. Do not parse detail: it is written for a human reading a log and its wording can change.
One exception: a 402 whose body carries an x402 offer is served as application/json. That body has the same members, then the offer.
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://dataquoll.io/errors/bad-request",
"title": "Bad Request",
"status": 400,
"detail": "limit must be <= 500 (got 9999)",
"instance": "/api/v1/incidents"
}- type
- A stable URI identifying the error class. This is the field to switch on if you want machine behaviour finer than the status code.
- title
- Short, stable, human-readable label for the class. Safe to display.
- status
- Repeats the HTTP status code, so a logged body stands alone.
- detail
- What went wrong with this specific request, including the offending value where that helps. Wording is not part of the contract.
- instance
- The path that produced it. Present on most errors, absent on a few.
The catalogue
| Status | Title | What it means | Retry? |
|---|---|---|---|
| 400 | Bad Request | A parameter is malformed (an unparseable limit, a bad bounding box, an invalid cursor), a query over a dense area or period ran out of time, or the filters are too large for one query: longer than 24,926 characters (for example, severity, urgency, certainty and warningLevel each set to every value). The detail says which, and how to narrow it. | Fix the request. A query that ran out of time may answer on a second try. For filters too large, send fewer values. |
| 401 | Unauthorized | A key that is not valid, a scheme other than Bearer or Payment, or a HEAD request with no key. With no key at all, every data endpoint answers 402 with a price instead. | No |
| 402 | Payment Required | No key on an endpoint you can pay for per request: the WWW-Authenticate header carries the price. Also a payment that failed its checks or whose price expired, with a fresh price. With x402: a payment we cannot accept as sent (type payment-not-accepted: a signature in a format we do not accept, a signature that does not verify, or a PAYMENT-SIGNATURE we cannot read) gets a 402 with no price. | Pay the price and resend, or send a key. x402 payment-not-accepted: do not send that payment again |
| 403 | Forbidden | The key is valid but not allowed here: an origin-restricted key used from another site, or history deeper than your plan allows. Also a payment that was already used for a request. | No |
| 404 | Not Found | No record with that identifier. | No |
| 409 | Conflict | A second payment for a price that was already paid. It is not used, and it is refunded. With x402: an authorization already used (that request is paid for: do not pay again), or one whose transaction reverted (it moved no money). | MPP: resend without a payment for a new price. x402: after a reverted authorization, pay with a new one; after a used one, do not pay again |
| 429 | Rate Limit Exceeded | Your monthly plan quota is spent. | Yes, after Retry-After |
| 429 | Too Many Requests | Per-IP throttling on an unauthenticated route. Separate from plan quota, and upgrading does not affect it. | Yes, after Retry-After |
| 500 | Internal Server Error | Our fault. If it persists, tell us what you called and when. | Yes, with backoff |
| 503 | Service Unavailable | Pay per request is paused, so no price is issued. With x402: your payment may have gone through (do not pay again: if it lands, it is refunded), or it could not be recorded (send the same PAYMENT-SIGNATURE again). | After Retry-After, or send a key. Never pay again after an x402 503 |
Two 429s that mean different things
Rate Limit Exceeded is your plan quota for the month. Upgrading fixes it, and the reset is the first of the month. Too Many Requests is per-IP throttling on unauthenticated routes such as password reset. Upgrading does nothing about it and the window is short.
Both carry Retry-After. Read it rather than retrying on a timer of your own. See rate limits.
Retry the transient, not the invalid
Retrying a malformed request (a 400 for a bad parameter) or a 401 will never succeed and burns quota you could be spending on data. Retry 429 and 500 with exponential backoff, and honour Retry-After when it is present. One 400 is the exception: when its detail says the query ran out of time, it may answer after a short wait, or narrow it (a smaller radiusKm, or state and event_type on a snapshot).
A 402 is a price, not a failure: pay it and resend, or send a key. On a paid request, anything but a 2xx is refunded within 35 days, so check a request before you pay for it.
A note on 500s specifically: we aggregate feeds run by small teams on varied infrastructure, and when a source goes down mid-poll the failure can surface here. It is worth retrying once before you treat it as real.