Custodian
ReferenceHTTP API

Tokens

Resolve a bounded batch of tokens back into values or sealed parts over HTTP.

A token stands in for a field value. POST /v1/tokens/resolve turns a batch of tokens back into what the caller may see of them. What comes back depends on the field's sensitivity:

  • a tokenised token — the server holds the key, so it decrypts the value and returns it, alongside the collection, field and record;
  • a client_encrypted token — the server holds no key, so it returns only the sealed parts (ciphertext, nonce and wrapped_record_key) for a device to unwrap locally. It never returns, and never computes, a value for one.
  • a file token — a client_encrypted field whose type is file — resolves to a pointer, not sealed parts: a file's ciphertext can be several megabytes, so it is fetched one at a time from its own route rather than dragged into a batch. See A file token points at the file route.

masked and plain fields carry no token, so they are never resolvable through this route.

Resolving a token requires a read_plaintext grant on the token's field. A token you may not resolve reads exactly like an unknown one, so the route cannot tell you which fields a principal is denied.

POST /v1/tokens/resolve

POST /v1/tokens/resolve
Authorization: Bearer <token>
Content-Type: application/json

{ "tokens": ["tok_AOWVT36VNVCIESCL", "tok_9Xa1..."] }
{
  "resolutions": [
    {
      "token": "tok_AOWVT36VNVCIESCL",
      "status": "resolved",
      "records": [
        {
          "collection": "policies",
          "field": "premium",
          "record_id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70",
          "sensitivity": "tokenised",
          "value": "12000"
        }
      ]
    },
    {
      "token": "tok_9Xa1...",
      "status": "resolved",
      "records": [
        {
          "collection": "policyholders",
          "field": "sa_id_number",
          "record_id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70",
          "sensitivity": "client_encrypted",
          "ciphertext": "<base64>",
          "nonce": "<base64>",
          "wrapped_record_key": "<base64>"
        }
      ]
    }
  ],
  "resolved_count": 2,
  "unresolved_count": 0,
  "file_count": 0
}

The three counts — resolved_count, unresolved_count and file_count — are counted by outcome and together sum to the number of distinct tokens in the batch.

A token that resolves to several records returns every one of them: a token is deterministic, so the same value in the same field shares one token.

A token that matches nothing visible to you is unresolved, never carrying a value:

{ "token": "tok_unknown", "status": "unresolved", "reason": "no value visible to you resolves this token." }

A token whose value lives in another vault reads identically to an unknown one, so the route cannot be used to probe another tenant.

A file token points at the file route

A token that names a file field resolves to a "status": "file" entry. It carries no sealed parts and no value — instead it tells the caller where to fetch the file, with a fetch object naming the method and path of GET …/files/:field:

{
  "token": "tok_5FILEHZ7VNCIESCL",
  "status": "file",
  "detail": "this token names a file field, whose bytes are fetched one at a time from the file route rather than resolved in a batch.",
  "fetch": {
    "method": "GET",
    "path": "/v1/collections/policyholders/records/018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70/files/passport_scan"
  },
  "vault_id": 42,
  "record_identifier": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70",
  "field_id": 71,
  "schema_version": 4
}

Such an entry is counted in file_count. Nothing is disclosed by the pointer, so no audit row is written for a file resolution — the audit row is written when you fetch the file itself.

A file field's token

The determinism above has one exception. A file field's token is derived from its per-object random object key, not from the file's bytes. So two identical files do not share a token, a file's token is not reproducible from its bytes, and the server stores it verbatim — it holds a token it cannot recompute. (When the master key is rotated, the device recomputes the token from the object key, which the rotation listing hands it, not from the file.)

On a destroyed vault

Resolving tokens against a vault whose keys have been destroyed is refused whole, up front — before any token is resolved and before any audit row is written — naming the destruction. It is not reported as a per-token unresolved, and not an unexplained error:

{
  "code": "vault_keys_destroyed",
  "message": "This vault's keys have been destroyed, and it refuses this request.",
  "details": { "destroyed_at": "2026-08-28T10:10:00Z" }
}

The batch is bounded

The batch is capped (by default at 100 tokens per request), so one call cannot resolve a whole vault. A batch above the cap is refused:

{
  "code": "resolve_batch_too_large",
  "message": "This request supplies 250 tokens, above the per-request maximum of 100. Resolve them in batches of at most 100: an unbounded batch would resolve a whole vault in a single call.",
  "details": { "limit": 100, "supplied": 250 }
}

The cap is set on the server. A body that is not a tokens array is a plain 422, with no error code.

On this page