Custodian
ReferenceHTTP API

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.

codeRefusal
duplicate_external_refAn external_ref already exists in the collection.
schema_version_mismatchA record write names a schema version that is not the current one.
stale_schema_documentA schema apply's document version is not the current one.
destructive_change_requires_flagA schema apply makes a destructive change without the flag that authorises it.
sensitivity_change_refusedA schema apply asks to change a field's sensitivity, which is refused.
incomplete_client_encrypted_patchA client_encrypted field is patched without all of its parts.
plaintext_grant_to_agentA read_plaintext grant is made to an agent on a client_encrypted field.
collection_read_plaintext_to_agentA collection-wide read_plaintext grant is made to an agent.
grant_to_revoked_principalA grant names a principal that has been revoked.
grant_expiry_in_pastA grant's expiry is already in the past.
duplicate_live_grantA grant duplicates one that is already live.
field_not_in_collectionA grant or search names a field that is not in the named collection.
caller_not_operatorA route reserved for operator principals is called by another kind.
agent_forbidden_on_v1An agent principal calls the /v1 surface, which is closed to it.
audit_context_too_largeAn audit context exceeds its size limit.
resolve_batch_too_largeA token-resolve request names more tokens than the batch limit.
search_value_where_index_requiredA search supplies a value on a field that requires a blind index.
search_index_where_value_requiredA search supplies a blind index on a field that requires a value.
not_foundSee Indistinguishable refusals below.
unauthenticatedSee Indistinguishable refusals below.
rotation_already_in_progressA rotation is begun while one is already in progress.
rotation_not_in_progressA rotation route is called with no rotation in progress.
rotation_records_rewrappedA rotation is discarded after records have already been re-wrapped under the new master key.
vault_keys_destroyedAn 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_progressA 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_revokedThe wrapped master key of a revoked device is asked to be replaced.
rotation_field_values_immutableA re-wrap batch would change a value's ciphertext or nonce, which a rotation never rewrites.
records_under_old_wrappingA rotation is completed while records still carry the old wrapping.
rotation_index_on_non_searchableA re-wrap batch carries a blind index on a value that is not searchable.
devices_under_old_wrappingA rotation is completed while a live device still lacks a copy of the master key under the new wrapping.
recovery_envelope_existsA recovery envelope is written to a vault that already holds one, without an explicit request to replace it.
unexpected_recovery_parameterA recovery-envelope write carries a parameter beyond blob, salt and params.
recovery_params_invalidA 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_boundsA recovery blob is empty or far larger than a wrapped master key.
vault_slug_mismatchA POST /v1/keys/destroy does not name the caller's own vault slug as confirmation.
invalid_principal_slugA POST /v1/principals names a slug outside the permitted characters.
invalid_principal_kindA POST /v1/principals names a kind that is not one of agent, service or operator.
last_operator_irremovableA DELETE /v1/principals/:slug would revoke a vault's last live operator.
recovery_refused_live_deviceA GET /v1/recovery is made while the vault still holds a live device.
recovery_refused_keys_destroyedA GET /v1/recovery is made on a vault whose keys have been destroyed.
recovery_refused_not_operatorA GET /v1/recovery is made by a principal that is not an operator.
recovery_rate_limitedA 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." }

On this page