Errors
The single shape every refusal takes, and the closed set of reason codes a /v1 refusal can carry.
Every refusal on every /v1 route returns the same error envelope: a
machine-readable code, a human-readable message, and — where the refusal carries
specifics — a details object.
{
"code": "duplicate_external_ref",
"message": "A live record (018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70) in this collection already holds external_ref \"policy-4471\"; a re-run import stops here rather than overwriting it.",
"details": { "existing_record_id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70", "external_ref": "policy-4471" }
}The keys appear in that order — code, then message, then details when present.
A refusal with no specifics omits details entirely. For equal inputs the body is
byte-identical.
Branch on the code, never on the message. The code is a stable identifier;
the message is prose that may be improved without notice. A refusal that carries
specifics puts them in details as fields — the existing record's identifier, the
two version numbers, the batch limit and the count supplied — so you read them as
data rather than parsing them out of the sentence.
The closed set of codes
A /v1 refusal carries one of these and no other. Each links to the page where it
is shown in context.
code | Refusal |
|---|---|
duplicate_external_ref | An external_ref already exists in the collection. |
schema_version_mismatch | A record write names a schema version that is not the current one. |
stale_schema_document | A schema apply's document version is not the current one. |
destructive_change_requires_flag | A schema apply makes a destructive change without the flag that authorises it. |
sensitivity_change_refused | A schema apply asks to change a field's sensitivity, which is refused. |
incomplete_client_encrypted_patch | A client_encrypted field is patched without all of its parts. |
plaintext_grant_to_agent | A read_plaintext grant is made to an agent on a client_encrypted field. |
collection_read_plaintext_to_agent | A collection-wide read_plaintext grant is made to an agent. |
grant_to_revoked_principal | A grant names a principal that has been revoked. |
grant_expiry_in_past | A grant's expiry is already in the past. |
duplicate_live_grant | A grant duplicates one that is already live. |
field_not_in_collection | A grant or search names a field that is not in the named collection. |
caller_not_operator | A route reserved for operator principals is called by another kind. |
agent_forbidden_on_v1 | An agent principal calls the /v1 surface, which is closed to it. |
audit_context_too_large | An audit context exceeds its size limit. |
resolve_batch_too_large | A token-resolve request names more tokens than the batch limit. |
search_value_where_index_required | A search supplies a value on a field that requires a blind index. |
search_index_where_value_required | A search supplies a blind index on a field that requires a value. |
not_found | See Indistinguishable refusals below. |
unauthenticated | See Indistinguishable refusals below. |
rotation_already_in_progress | A rotation is begun while one is already in progress. |
rotation_not_in_progress | A rotation route is called with no rotation in progress. |
rotation_records_rewrapped | A rotation is discarded after records have already been re-wrapped under the new master key. |
vault_keys_destroyed | An operation is attempted on a vault whose keys have been destroyed. Raised per-route by the vault-destruction latch across every /v1 route and the MCP tools, carrying destroyed_at. |
vault_rotation_in_progress | A request is refused because the vault is rotating its master key. Raised by the rotation latch across every /v1 route and the MCP tools, carrying started_at. |
device_revoked | The wrapped master key of a revoked device is asked to be replaced. |
rotation_field_values_immutable | A re-wrap batch would change a value's ciphertext or nonce, which a rotation never rewrites. |
records_under_old_wrapping | A rotation is completed while records still carry the old wrapping. |
rotation_index_on_non_searchable | A re-wrap batch carries a blind index on a value that is not searchable. |
devices_under_old_wrapping | A rotation is completed while a live device still lacks a copy of the master key under the new wrapping. |
recovery_envelope_exists | A recovery envelope is written to a vault that already holds one, without an explicit request to replace it. |
unexpected_recovery_parameter | A recovery-envelope write carries a parameter beyond blob, salt and params. |
recovery_params_invalid | A recovery envelope's params omit a cost parameter or the algorithm version, or carry a cost that is not a positive integer. |
recovery_blob_out_of_bounds | A recovery blob is empty or far larger than a wrapped master key. |
vault_slug_mismatch | A POST /v1/keys/destroy does not name the caller's own vault slug as confirmation. |
invalid_principal_slug | A POST /v1/principals names a slug outside the permitted characters. |
invalid_principal_kind | A POST /v1/principals names a kind that is not one of agent, service or operator. |
last_operator_irremovable | A DELETE /v1/principals/:slug would revoke a vault's last live operator. |
recovery_refused_live_device | A GET /v1/recovery is made while the vault still holds a live device. |
recovery_refused_keys_destroyed | A GET /v1/recovery is made on a vault whose keys have been destroyed. |
recovery_refused_not_operator | A GET /v1/recovery is made by a principal that is not an operator. |
recovery_rate_limited | A GET /v1/recovery exceeds the vault's recovery-fetch rate limit. |
That is the whole set — 42 codes. A refusal outside them is a plain HTTP error with
no error code (for example a malformed request the API names as a 422 with an
"error" string, or a duplicate slug returned as a bare 409).
Indistinguishable refusals
Two codes each stand for a set of distinct reasons that are, by design, a single response the caller cannot pick apart. Each carries one fixed message, and a route may not vary it — varying it would leak which reason fired.
not_found covers: a record you may not read, another vault's record, a tombstoned
record, and a nonexistent identifier. Its body is always:
{ "code": "not_found", "message": "No record with that identifier is visible to you." }unauthenticated covers: an unknown selector, a failed verification, a revoked
principal, and a missing authentication header. Its body is always:
{ "code": "unauthenticated", "message": "The request could not be authenticated." }