Errors
One body shape for every failure: a JSON object with a single detail key. The status code says what class of problem it is; the string says which instance.
{ "detail": "Over the per-minute limit of 60 requests." }detail is written for a developer reading a log, and it names the fix wherever there is one. It is not a stable identifier — treat the status code as the thing to branch on and the string as the thing to print.
Status codes
- 401Unauthorized
- No key, an unknown key, a revoked key or an expired one. Carries
WWW-Authenticate: Bearer. Not retryable — the credential has to change. Which is which. - 403Forbidden
- The key is valid and the request is not allowed. Three causes: the account is pending, rejected or suspended; the key lacks the scope; or a trial key named a location outside its ZIP. All three are decisions about access, so none is retryable.
- 404Not Found
- No such event, or one that is not public. Also what a trial key gets for a real event outside its ZIP — deliberately indistinguishable, because a
403there would confirm the id was real. - 422Unprocessable Content
- A parameter is wrong and
detailnames which: a lonelatwithoutlng, two conflicting centres, an unknown category, a ZIP with no centroid on file, or a cursor that is not one of ours. Fix the request; retrying it unchanged returns the same thing. - 429Too Many Requests
- Over the per-minute or the daily window, and
detailsays which. Back off onRetry-Afterrather than on a fixed interval. - 5xxServer error
- Ours. Retry with backoff; nothing about the request needs to change.
What to retry
Two of the six are worth retrying and four are not, which is the whole decision:
- Retry429, 5xx
429on theRetry-Afterit gives you;5xxon exponential backoff. Cap both — a daily429can be hours out, and sleeping through it is worse than failing the job.- Do not retry401, 403, 404, 422
- Nothing on loso's side will change between attempts. Retrying these is how a rate limit gets spent discovering that a parameter is still wrong.
Not an error
An empty page is a 200, not a 404: { "data": [], "next_cursor": null } means the filters matched nothing. Common and legitimate — a quiet ZIP on a Tuesday, a category with nothing upcoming, a starts_before in the past.
One case deliberately does not degrade to an empty page: a trial key querying outside its ZIP gets a 403. Answering it with zero rows would be a false statement about loso’s coverage made to the one reader deciding whether to pay for it.