Every error is an RFC 9457 problem document, served as
application/problem+json. The set of error kinds is closed — each one is
listed under Errors — so you can write exhaustive
handling rather than pattern-matching on prose.
{
"type": "https://developer.winteriscoming.fyi/problems/insufficient_scope",
"title": "Insufficient scope",
"status": 403,
"code": "insufficient_scope",
"detail": "This API key is missing the \"sites:read\" scope. Granted: services:read.",
"instance": "/v1/sites",
"required_scope": "sites:read"
}
codecode is a Yeti extension to the RFC and it exists for exactly this reason:
type is a URI, and asking every client to parse a URI to find out what went
wrong is a poor trade. code is the stable token. It does not change when the
documentation moves.
problem = response.json()
match problem["code"]:
case "rate_limit_exceeded": back_off(response.headers["Retry-After"])
case "insufficient_scope": alert(f"key needs {problem['required_scope']}")
case _: raise ApiError(problem)
Do not switch on title, which is human-readable text we may reword, or on the
HTTP status alone, which is shared by several kinds — 403 is
insufficient_scope, company_not_granted or plain forbidden, and they need
different reactions.
Some kinds carry extra fields beyond the RFC's own, and they are the actionable part:
| Member | On | What it gives you |
|---|---|---|
required_scope |
insufficient_scope |
Exactly which grant is missing |
errors |
validation_failed, invalid_query_parameter |
Field or parameter → what is wrong with it |
A record belonging to another company also returns not_found. That is
deliberate: a 403 there would confirm the id exists and turn the endpoint into a
way to enumerate another tenant's records.
An unrecognised filter, sort, include or fields parameter fails the
request with invalid_query_parameter. It does not return an unfiltered page that
looks correct — which is the failure that makes an export quietly wrong rather
than loudly broken. The detail lists what the endpoint does accept.