Error semantics

The governing rule is: 401 means "I do not know who you are"; 403 means "I know
who you are, and no."

Condition Status Body
No credential, malformed, or revoked 401
Key expired 401 {"error": "key expired"}
Session expired 401 {"error": "session expired"}
Bad password or locked account 401 {"error": "invalid credentials"}
Insufficient role 403 {"error": "insufficient role", "required": "admin_only", "have": "read_write"}
Source IP not allowlisted 403 {"error": "key not authorized from this IP"}
Path outside key scope 403 {"error": "key not authorized for this endpoint"}
CSRF missing or mismatched (cookie path only) 403
Rate limit exceeded 429 {"error": "rate_limit_exceeded", "group": …, "retry_after": …} plus a Retry-After header
Controller unreachable 502 {"error": {"code": "CONTROLLER_UNREACHABLE", "message": …}}
Controller returned a non-JSON body varies {"error": {"code": "BAD_UPSTREAM", "message": …}}

The direct client guidance follows from the table. Retry a 401 only after
re-minting a credential — never in a loop.
Never retry a 403; it is a
policy decision and will not change on repetition. Honour Retry-After on
429.
Treat 502 as a transient dependency failure and back off.

Note that a locked account and a wrong password return an identical 401 with an
identical body. The distinction is recorded in the audit log but is deliberately
not disclosed on the wire, because disclosing it enumerates valid usernames.