Errors
One shape, always, including on 500.
code is stable and meant to be matched on. message is for a human reading a
log and is not a contract: it can be reworded at any time.
{
"error": {
"code": "insufficient_scope",
"message": "This key does not hold library.write.",
"scope": "library.write"
}
}| Code | Status | Means |
|---|---|---|
unauthorized | 401 | The request carried no identity: no key, and no session from this origin. |
forbidden | 403 | The identity is fine and a rule refused anyway: a block, a private account, a closed comment section. When the refusal is one of those, the message says which. |
invalid_key | 401 | Unknown, revoked or expired. The three are not told apart. |
insufficient_scope | 403 | The key does not hold the scope, which is named in the body. |
not_found | 404 | No such resource, or none the owner can see. |
invalid_request | 400 | A field is missing or not allowed. The message says which. |
conflict | 409 | That already exists. |
rate_limited | 429 | Carries retry_after in seconds. |
internal | 500 | Something failed here. Nothing about the schema is said. |
unavailable | 503 | Only GET /api/v1/arcade/jwks today, with reason identity_unavailable: the server does not have the keys that sign Arcade identities yet. Try again later. |
Other members
Beside code and message, a refusal can carry:
reason: a word to act on, finer than thecode, on Arcade refusals and on some account ones, such asreauthentication_requiredandmfa_required. A client that meets one it does not know acts on thecode.field: the field refused, withinvalid_request.detail: a finer cause, such astoo_largeon amedia_invalid.retry_after: how many seconds to wait, withrate_limited.scope: the scope that was missing, withinsufficient_scope.check: the URL check result, when submitting an Arcade release or making it live is refused.second_factor: withreauthentication_required, on the routes that take a code instead of a new sign-in, whether a two-factor code would do.
Arcade's refusals, with each reason, are in the
Arcade refusal table.
Where a refusal comes from
Most refusals are the database's, not the handler's. A check constraint, a not-null column, a rate-limit trigger or a row-level policy raises, and that raise is translated into the shape above.
Only refusals raised deliberately carry their message outward. Anything else
answers internal with nothing else said, because an unplanned error message
is a description of the schema.
404 not_found covers two cases on purpose: the resource does not exist, and
the resource exists but the key's owner cannot see it. Telling them apart
would be a way to ask whether somebody's private list exists.