Skip to main content
Any failure, on any surface, comes back in one shape:
The REST API, the MCP server, and the widget all use it. A client that had to branch on which surface it was talking to would sooner or later get the branch wrong.

Branch on code, never on message

code is the contract. It comes from a fixed set, and its meaning never changes. message is written for a human reading a log. We may reword it at any time. Anything that matches on the text of a message is a bug waiting for a copy edit. field appears on validation failures. It is a dotted path into the body you sent.

The codes

Two that surprise people

404 for another workspace’s data

Asking for a ticket that belongs to someone else returns not_found, exactly as if it did not exist. It never returns 403. That is by design. A 403 would tell you the id is real, and being able to probe for which ids exist in other workspaces is itself the leak.

402 rather than 403 when you run out

quota_exhausted is a billing state, not a permissions one. Your credentials are fine, and you are allowed to do this; the account is not paid up. The distinction matters for clients. Something treating 403 as “log in again” would send an integration round an auth loop over an unpaid invoice.

Validation changes nothing

A 400 means the request was rejected before anything acted on it. There is no partial write to undo. Fix the field named in field, then send it again.

Retrying

Retrying a POST without an Idempotency-Key is how you get duplicate tickets. See Idempotency.