Errors
Every refusal is application/problem+json (RFC 9457) with a status, a title and a detail sentence a person could read out.
The shape
{
"type": "https://api.thawed.app/problems/insufficient_scope",
"title": "Not allowed",
"status": 403,
"detail": "This token can read but not change. Make one with \"Change things\" in Settings > Developers.",
"scope": "write"
}
type is about:blank unless the problem has a name worth matching on (unauthorized, invalid_token, insufficient_scope), in which case it is a URL under the API host. title is the status in words. detail is written for a person: show it as it is. A refusal for want of a scope adds scope; a body that could not be taken adds errors.
The Problem shape is the same on every host and every version.
The statuses
| Status | Title | When |
|---|---|---|
| 400 | Bad request | The request could not be read: a cursor that is not one, a body that is not JSON. |
| 401 | Not signed in | No token, a token that has ended, or one issued for the other door (an assistant’s MCP token on the API, or the other way round). WWW-Authenticate names the resource metadata document. |
| 403 | Not allowed | The token, the role or the plan does not allow it. A missing scope is named in scope and in WWW-Authenticate as insufficient_scope; a plan that does not come with it says which one does. |
| 404 | Not found | Nothing of that id in this family or business. An id from another family is a 404 here, never a 403. |
| 409 | Conflict | Not now: Thaw is still working on the document, or the same idempotent call is still running. |
| 413 | Too large | A file past the plain limit; send it in parts. |
| 422 | Cannot be taken as sent | A field was wrong. errors names each one, as lists of sentences keyed by field. |
| 429 | Too many requests | Past the pace. Retry-After is seconds. |
| 503 | Not available right now | A door that is not set up on this server (Sign in with Apple without its keys). |
| 500 | Something went wrong | Nothing of the cause is in it. Try again in a moment. |
401 and the challenge
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
WWW-Authenticate: Bearer realm="Thaw", resource_metadata="https://api.thawed.app/.well-known/oauth-protected-resource", error="invalid_token", error_description="The token is not valid or has expired."
The challenge is RFC 6750’s, so an OAuth client that gets one knows where to learn about the authorization server and start the flow. A personal token that is revoked or a connection that has ended both come back this way.
403 and which wall
A change can be refused by three walls, and detail names the one that stopped it, in order:
- The role. “Your role here is read-only, so a token of yours can read but not change anything.” Nothing to step up to.
- The plan. “A token can look things up on the Free plan. Changing things comes with Personal: https://go.thawed.app/settings/plan”. The way up is the Plan page.
- The scope. “This token can read but not change. Make one with “Change things” in Settings > Developers.” Only this one adds
scope, because only this one can be fixed by asking again with a wider token (or, for a connection, consenting again with “Read and change”).
422 and the fields
{
"type": "about:blank",
"title": "Cannot be taken as sent",
"status": 422,
"detail": "Check the fields.",
"errors": {
"until": ["The until must be a date after today."]
}
}
A change that cannot be made as asked - a document still being read, a quarantined file, a crop on something that is not a photo - is also a problem, with the status the page would have shown and the sentence the page would have said.
Reading one in code
Match on status first and type second; show detail to the person; on 403 with scope, ask for a wider token; on 429, wait Retry-After. Never parse detail - it is written to be read, and its words will change.