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
tokenisedtoken — the server holds the key, so it decrypts the value and returns it, alongside the collection, field and record; - a
client_encryptedtoken — the server holds no key, so it returns only the sealed parts (ciphertext,nonceandwrapped_record_key) for a device to unwrap locally. It never returns, and never computes, a value for one. - a
filetoken — aclient_encryptedfield whose type isfile— 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.