Record files
Write and read a file field's sealed ciphertext over HTTP — one file per request, on its own routes.
A file field holds a file's bytes. Those bytes
do not travel in a record body — they move over their own
two routes, one file per request, so a record write that touches other fields never
carries a whole document and a batch token resolution never
drags one per entry.
A file field is always client_encrypted. Your device seals the bytes; the server
holds ciphertext it cannot read, and neither route returns a decrypted byte.
Before you start, the collection needs a file field — declare one in the
schema first. Both routes need bytes your own
machine produces on an enrolled device: the sealed ciphertext and
its nonce, the object_key under which the bytes are stored, and the token the
device derives from that object key. This page shows where each belongs; it does not
restate how to produce them.
PUT /v1/collections/:name/records/:id/files/:field — write a file
Writes one file field's sealed ciphertext to one record. You need the
write grant on the field. The record is named in the path
by its record_id; the field by its name.
The body carries the sealed parts, the object key your device minted, the token, and the size of the ciphertext:
PUT /v1/collections/policyholders/records/018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70/files/passport_scan
Authorization: Bearer <token>
Content-Type: application/json
{
"ciphertext": "<base64>",
"nonce": "<base64>",
"object_key": "MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U",
"token": "tok_AOWVT36VNVCIESCL",
"byte_count": 20480
}ciphertextandnonceare the sealed file and its nonce, base64-encoded.object_keyis the object-key suffix your device mints from a random source: exactly 32 characters of RFC 4648 base32 (uppercaseA–Zand2–7, no padding). The server prepends the vault's own prefix to store it; the stored key is theobject_keythe response returns.tokenis the token your device derived from the object key. The server stores it verbatim — it holds a token it cannot recompute. See Tokens for why a file's token comes from its object key and not its bytes.byte_countis the size, in bytes, of the ciphertext this write carries, and is judged against the deployment-wide maximum (below). It must be a non-negative integer.
An object key names where the ciphertext is stored, and nothing else: it is the vault's identifier and a device-minted random value, so it "discloses nothing about the file, the record or the person it concerns".
On success the response is 200 OK and names what was stored — the field, the stored
object key (the vault prefix and your suffix), the recorded byte count and the token
stored verbatim:
{
"field": "passport_scan",
"object_key": "42/MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U",
"byte_count": 20480,
"token": "tok_AOWVT36VNVCIESCL"
}Replacing a file writes a new object under a fresh key rather than overwriting the old
one, so a replacement write carries a new object_key.
The refusals
-
No
writegrant on the field (403) — "you hold no write grant on field "passport_scan", so you may not write its file." -
Not a file field (
422) — "field "policy_number" is not a file field (it is string/plain); only a file field's ciphertext is carried on this route." -
Over the size limit (
413) — a write that declares, or sends, more thanCUSTODIAN_FILE_MAX_BYTES(see Deployment) is refused and nothing is stored. A write that declares too much is refused on the declaration, before the ciphertext is decoded; one that declares less and sends more is refused as the bytes are read. Both carrymaximum_bytesandoffered_bytes:{ "error": "this file declares 12000000 bytes, above the maximum of 10485760 bytes a single write may store; nothing is stored.", "details": { "maximum_bytes": 10485760, "offered_bytes": 12000000 } } -
Malformed object key (
422) — a suffix of the wrong width or alphabet is refused, naming which; nothing is stored:{ "error": "object key suffix must be 32 characters, but \"MFRGG\" is 5." } -
Object key already in use (
409) — a suffix already stored in the vault (a collision or a replayed write) is refused, and the file already at that key is left untouched:{ "error": "object key \"42/MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U\" is already stored in this vault; a collision or a replayed write is refused rather than overwriting the file already there." }
A missing ciphertext, nonce, token or byte_count is a plain 422 naming the
missing part.
GET /v1/collections/:name/records/:id/files/:field — read a file
Returns one file field's sealed parts, for a device to unwrap locally. You need the
read_plaintext grant on the field. On a client_encrypted
field, read_plaintext is what lets the caller resolve the sealed value on an
enrolled device.
GET /v1/collections/policyholders/records/018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70/files/passport_scan
Authorization: Bearer <token>{
"collection": "policyholders",
"field": "passport_scan",
"record_id": "018f4e2a-7c1b-7a3e-b9d4-2f6c8a1e5b70",
"sensitivity": "client_encrypted",
"ciphertext": "<base64>",
"nonce": "<base64>",
"wrapped_record_key": "<base64>"
}The ciphertext is fetched from storage and returned untouched: the server holds no
key for it and adds none, so the response never contains a decrypted byte. The
wrapped_record_key is "one record's random key sealed under a key derived from the
master key, inert to anyone without the master key the server has never held" — your
device unwraps it and, with it, the file.
Reading a file discloses a plaintext, so this route appends a read_plaintext
audit row before it responds.
The refusals
-
No
read_plaintextgrant on the field, or no such field — both return the uniformnot_found, so the route discloses neither which fields exist nor which a principal is denied, and writes no audit row for either. A record that carries no file at this field takes the samenot_found. -
An
agentprincipal is refused withagent_forbidden_on_v1before any work, like every other/v1route (see Using the HTTP API). Aread_plaintextgrant on aclient_encryptedfield is not grantable to an agent in the first place. -
The record's wrapped key was discarded (
409) — a deleted record discards its wrapped key, so its sealed value can no longer be unwrapped, and the read is refused before the bytes are fetched:{ "error": "the record this token names holds no wrapped key, so its sealed value can no longer be unwrapped by any device." } -
The vault's keys are destroyed — refused with
vault_keys_destroyed, like every other read that would disclose a plaintext (see Keys).
A file field is refused in a record body
A file field is client_encrypted, but its ciphertext still may not be supplied
in a record create or patch — naming one there is refused, pointing at the write route
above. See Records.