Errors
One shape, always.
{
"error": {
"code": "invalid",
"message": "no value for guest_name",
"hint": "A contract that ships with a placeholder still in it is worse than one that failed to send."
}
}
The message is frequently the database's own sentence, passed
through rather than replaced. It is more specific than anything this layer
could write, and the layer that knows why something was refused is the layer
that refused it.
| Status | Code | Means |
|---|---|---|
| 400 | bad_request | The request could not be read at all. |
| 401 | unauthorized | No key, or a key that is not live. Unknown, revoked and expired all answer the same way — a caller who could tell them apart could enumerate. |
| 403 | forbidden | The key is live and its scopes do not carry this. |
| 404 | not_found | No such thing, in this account. A thing in another account answers the same way, and deliberately: confirming it exists would be the leak. |
| 405 | method_not_allowed | That path exists under another method. Said separately from 404 because it saves an hour. |
| 409 | conflict | An idempotency key reused for a different body, or the first attempt still running. |
| 413 | too_large | Larger than a signing request ever is. |
| 422 | invalid | The body did not validate, or the engine refused it. The message is the engine's own sentence. |
| 429 | rate_limited | Too many attempts. The message names the budget and the hint says when to try again. |
| 500 | internal | Ours. Quote the x-request-id. |
Retrying
Retry 429 and 500, with backoff. Do not retry
422 — the request will be refused the same way every time, and the
message says why. Retrying anything that creates something means sending an
idempotency key; our own SDK mints one for you.
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 25 agreements a month.
Get an API key