Laboratory › Developers › Errors
Errors
The taxonomy is fixed now, while nothing depends on it, so its shape cannot be invented differently later under pressure to ship.
Unknown paths are the host's, not ours
Requesting /api/does-not-exist.json is not a Tiffany endpoint failing. No Tiffany endpoint is
involved: the hosting layer resolves the request before anything of ours would run, and answers with its own native
404. The body is not our envelope and may not be JSON at all.
This is a boundary, not an unimplemented feature. Normalising it would mean adding a request-time function whose only job is to re-dress someone else's 404 — the first executable surface on a site whose integrity rests on having none. That trade has been considered and declined; the decision is recorded in the deferred ledger.
| Request | Who answers | Governed by this contract |
|---|---|---|
/api/claims.json | A Tiffany endpoint | Yes — including any error it returns |
/api/does-not-exist.json | The hosting layer | No — native host response |
/anything/unrouted | The hosting layer | No — native host response |
The contract
When an endpoint does return a structured error, it returns exactly this, validated against
error.schema.json:
{
"error": {
"code": "not_found",
"message": "No claim version at this address.",
"retryable": false,
"documentation": "https://tiffany.systems/developers/api/errors#not_found",
"request_id": "…"
}
}
Codes
| Code | HTTP | Retry | Meaning | Emitted by an implemented endpoint today |
|---|---|---|---|---|
not_found |
404 | no | The endpoint exists but the object it was asked for does not. Not to be confused with a request to an unknown path, which never reaches a Tiffany endpoint and is answered by the hosting layer — see error_contract_scope. |
no |
invalid_request |
400 | no | The request was malformed. | no |
validation_failed |
422 | no | The request was well formed but an input failed validation. Names the offending field. | no |
unsupported_version |
400 | no | The requested API contract version is not served. | no |
withdrawn_object |
200 | no | Not an error condition on this surface. A withdrawn record returns 200 with its original bytes and lifecycle WITHDRAWN; the code exists for future endpoints that must refuse an operation on a withdrawn object. | no |
superseded_object |
200 | no | As above. A superseded version is still served, still citable, and carries superseded_by. | no |
unauthorized |
401 | no | No valid credential was presented. | no |
forbidden |
403 | no | The credential is valid but its scope does not permit this. | no |
rate_limited |
429 | yes, with backoff | Request budget exhausted. Retry after the interval in Retry-After. | no |
conflict |
409 | no | The operation conflicts with the current state of the object. | no |
unavailable |
503 | yes, with backoff | Temporarily unable to serve. Retry with backoff. | no |
internal_error |
500 | yes, with backoff | An unexpected failure. Carries a request id and nothing else — no stack frame, no path, no internal identifier. | no |
Withdrawn and superseded are not errors
A withdrawn claim version returns 200 with its original bytes. So does a superseded one. The
lifecycle field carries the scientific state.
This is deliberate and will not change. A citation must resolve to what its author actually read; answering 404
or 410 for withdrawn science would erase the record rather than correct it. The two error codes named
withdrawn_object and superseded_object exist for future endpoints that must refuse an
operation on such an object — never for reading one.
What an error will never contain
- A stack frame or an exception class.
- A filesystem path, a repository path or any internal identifier.
- Any part of a credential, including a prefix or a length.
- The existence or name of a private object. A caller who may not know a thing exists gets the same answer as a caller asking about something that does not exist.
- Infrastructure topology, region, or upstream service names.
PreviousSchemas
NextVersioning