Errors
The Veriticity error contract: one envelope, stable machine-readable codes, and what to do about each. Includes the three permanent refusals a client must not retry.
One envelope#
Every failure on the REST API and on MCP uses the same shape. code is stable and safe to branch on. message is written for a person and may be reworded without notice — do not match on it.
The OAuth endpoints are the one exception: /oauth/token and /oauth/revoke answer RFC 6749’s {error, error_description}, because that is what every OAuth library in the world parses.
Validation issues#
issues[] appears on invalid_request and draft_invalid only, and it always carries every problem found rather than the first — so four mistakes are fixed in one round trip instead of four.
Unknown fields are refused rather than ignored, with guidance saying where authority actually comes from. A body containing agentId is a caller who believes they are choosing the agent, and silently dropping it would leave them believing it.
Every error code#
28 codes. This table is generated from the same table the API answers from, so it cannot fall behind.
| Status | Code | What to do |
|---|---|---|
400 | invalid_request | The body was understood and is not acceptable. Read issues[]: it carries every problem found, not just the first. |
400 | malformed_json | The body is not valid JSON. |
400 | webhook_url_not_permitted | The address is not one Veriticity will send to — it must be HTTPS on port 443, a hostname rather than an IP literal, and must resolve to a publicly routable address. Editing your JSON will not fix it. |
401 | api_key_expired | The key expired. Issue a new one in the dashboard. |
401 | api_key_revoked | The key was revoked. Issue a new one in the dashboard. |
401 | invalid_api_key | The key does not exist, or the secret did not match. Both give this same answer deliberately — telling them apart would confirm which keys exist. |
401 | invalid_token | The access token is unknown, malformed, expired or revoked — or the connection behind it was revoked. Obtaining a new one may help; if it was a revoked connection, a person must reconnect. |
401 | unauthorized | No usable Authorization: Bearer header was presented. Check the header name and that the value is not empty. |
401 | wrong_environment | |
403 | api_key_not_bound_to_agent | You presented an organisation-scoped key to an operation that must act as an agent. Use a key bound to one — issue one key per agent. |
403 | api_key_not_organization_scoped | You presented an agent-bound key to an operation that acts for the organisation. Use an organisation-scoped key. |
403 | api_key_not_permitted | No API key of either kind may do this, and none ever will. Policy authoring is a human operation performed in the dashboard. Do not go looking for a different credential. |
403 | connection_unauthorized | The token is valid and the human authority behind it is gone: the person who granted the connection has left the organisation or lost permission. Refreshing will succeed and the next call will fail identically. Stop, and tell them to reconnect. |
403 | insufficient_scope | This connection was not granted the scope this operation needs. Re-authorise with a wider grant — this will work. |
403 | oauth_not_permitted | No OAuth scope reaches this operation, whatever the connection holds. Reconnecting with more scopes will not help. Use an organisation-scoped API key if you have one. |
403 | organization_suspended | The organisation is suspended. Nothing is evaluated for it until that is resolved. |
404 | not_found | No such resource in this organisation. Another organisation's resources answer identically, so this never confirms that an id exists elsewhere. |
409 | budget_reconciliation_inconsistent | Veriticity's own records disagree about a budget, so it refused to activate a spending limit against them. Nothing was changed. Retrying will not help; report it with the counter id in the message. |
409 | idempotency_key_reuse | This Idempotency-Key was already used for a different purchase. A retry must repeat the original request exactly; a new purchase needs a new key. |
409 | policy_lifecycle_conflict | The lifecycle forbids this — activating a non-draft, editing an archived version, discarding what is live. |
409 | policy_name_taken | A live policy in this organisation already has that name. |
409 | webhook_endpoint_revoked | The endpoint was revoked, and revocation is terminal. Create a new one. |
409 | webhook_limit_reached | The account already has as many webhook endpoints as it may have. |
413 | payload_too_large | Over the limit — 64 KB for purchases, policies and simulations, 16 KB for webhooks. |
415 | unsupported_media_type | Send the body as application/json. The one exception is rotate-secret, which accepts no body at all. |
422 | draft_invalid | The request was fine; the stored draft cannot be activated. issues[] says why. Distinct from invalid_request, where the body you just sent is at fault. |
500 | internal_error | Ours. It is logged in full and never described to a caller. Safe to retry — send an Idempotency-Key on a purchase so a retry cannot double-spend. |
503 | webhook_not_configured | This deployment cannot currently hold webhook signing secrets. It is a configuration fault on our side; retrying after it is fixed will succeed. |
Every 401 carries WWW-Authenticate: Bearer realm="compass".
Refusals you must not retry#
Three codes mean “stop”, and a client that treats them as transient will retry forever. They are kept distinct precisely so you can tell them apart.
| Code | Why retrying cannot work |
|---|---|
api_key_not_permitted | No API key of either kind may do this. It is a human operation. |
oauth_not_permitted | No OAuth scope reaches this operation. A wider grant would not help. |
connection_unauthorized | The person behind the grant is gone. Refreshing will succeed and the next call will fail identically. |
Contrast insufficient_scope, which is fixable: re-authorise with the scope named in the message. That distinction is why these are four codes and not one.
There are no rate limits#
Veriticity publishes no rate limits on the REST API, the OAuth endpoints or MCP today. There is no 429 in the error vocabulary and no Retry-After header anywhere.
This is stated plainly because the opposite is easy to assume. Do not write handling for a response that does not exist. If limits are introduced they will be documented here first, with notice.
Reasonable client behaviour still applies: back off on internal_error, and send an Idempotency-Key on purchases so a retry cannot double-spend. See Purchases.