Attach files to a session
A governed action can record the files it worked with, such as the invoice it checked, a screenshot it acted on, or the report it produced. Each file becomes its own step in the action’s session, so reviewers open it from the session timeline and evidence exports list it with its hash.
Call attach on the action context while the protected action runs. The SDK chooses how to send the file and builds the session step for you. Attaching follows your application’s content capture setting: when content capture is off, attach sends nothing and fails with attachment_content_capture_disabled.
How the SDK sends a file
| File | How it travels |
|---|---|
Image, audio, PDF, or application/octet-stream, up to 1 MiB | Inline, as a base64 data URL inside the session step |
| Any other type, or anything larger than 1 MiB | Uploaded first, then referenced from the session step |
A file can be up to 25 MiB. Empty files, names with quotes or control characters, names longer than 255 characters, and MIME types with parameters (such as text/plain; charset=utf-8) are rejected before anything is sent. The SDK keeps only the last segment of a file path as the name and guesses the MIME type from the extension when you don’t pass one.
VeriProof stores a file exactly as you send it. The sensitive data policy masks captured text, not the contents of files. Remove or mask protected data before you attach a file.
Attach a file from a governed action
TypeScript
import { Attachment } from '@veriproof/sdk-core';
const result = await veriproof.govern('invoice.review', invoice, async (context) => {
await context.attach(await Attachment.fromPath('invoices/INV-1042.pdf'));
await context.attach(new TextEncoder().encode(notes), {
fileName: 'review-notes.txt',
mimeType: 'text/plain',
});
return decide(context.input);
});Each call returns a receipt with the file name, MIME type, size, SHA-256 hash, delivery (inline or staged), the upload reference for a staged file, and the ID of the session step that holds the file. Keep the hash if you want to match the file against an export later.
attach works only while the protected action runs. A call after the action returns fails, and so does a call after VeriProof stops the run.
When an attachment fails
A failed attachment raises an error inside your action. Catch it if the action can continue without the file; otherwise let it propagate and the governed call ends as failed.
| Reason code | What happened |
|---|---|
attachment_upload_failed | The staged upload didn’t reach VeriProof or was refused. |
attachment_upload_invalid | The upload response didn’t match the file that was sent. |
attachment_evidence_failed | The session step that refers to the file wasn’t recorded. |
attachment_evidence_invalid | The response for the session step didn’t match this run. |
attachment_content_capture_disabled | Content capture is off for this application, so no file was sent. |
The error type is AttachmentError in TypeScript, Python, Go, and Rust, and AttachmentException in .NET and Java. The original transport error is kept as its cause.
Send attachments without an SDK
Without an SDK, send a file in two steps: upload it, then send a session step that points to it. Small image, audio, and PDF files can skip the upload and travel inline.
Both requests use the application key split across two headers, as described in Runtime authentication. Upload with the same application key that sends the session.
1. Upload the file
curl -X POST "https://ingest.veriproof.app/v1/ingest/upload" \
-H "x-functions-key: <functions-key>" \
-H "X-API-Key: <application-key>" \
-F "purpose=session_attachment" \
-F "[email protected];type=application/pdf"Send exactly one file field and put purpose before it. The file can be up to 25 MiB. The response names the reference to use:
{
"refId": "3f1c9a0e5b7d4c2a8e6f1b3d5c7a9e0f",
"fileName": "INV-1042.pdf",
"mimeType": "application/pdf",
"expiresUtc": "2026-10-31T14:05:12.000Z"
}An upload reference lasts 24 hours and can be used in one session step.
2. Send the session step
Post the session to /v1/ingest/otlp with Content-Type: application/json. The step that holds the file sets four fields next to its attributes:
| Field | Value |
|---|---|
inputMimeType | The upload’s mimeType, exactly |
inputValue | A JSON string: [{"file_url":{"url":"veriproof-ref://<refId>/<fileName>"}}], with the file name percent-encoded |
attachmentRefId | The upload’s refId |
attachmentFileName | The upload’s fileName, exactly |
{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"tenantId": "",
"applicationId": "",
"spanCount": 2,
"merkleRootHex": "<64 lowercase hex characters>",
"idempotencyKey": "invoice-review-INV-1042-attachment",
"timestampUtcMs": 1793360715000,
"spans": [
{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"operationName": "invoice.review",
"startTimeUnixNs": 1793360712000000000,
"durationNs": 3000000000,
"statusCode": "OK",
"attributes": {
"veriproof.session.id": "invoice-review-INV-1042",
"veriproof.session.name": "invoice.review"
}
},
{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "53995c3f42cd8ad8",
"parentSpanId": "00f067aa0ba902b7",
"operationName": "INV-1042.pdf",
"startTimeUnixNs": 1793360714000000000,
"durationNs": 0,
"statusCode": "OK",
"attributes": { "veriproof.step.outcome": "success" },
"inputMimeType": "application/pdf",
"inputValue": "[{\"file_url\":{\"url\":\"veriproof-ref://3f1c9a0e5b7d4c2a8e6f1b3d5c7a9e0f/INV-1042.pdf\"}}]",
"attachmentRefId": "3f1c9a0e5b7d4c2a8e6f1b3d5c7a9e0f",
"attachmentFileName": "INV-1042.pdf"
}
]
}Each step holds one file. To attach several files, send one step per file.
If you send OTLP protobuf instead of this JSON envelope, set the same values as span attributes: input.mime_type, input.value, app.attachment.ref_id, and app.attachment.file_name.
Send a small file inline
For an image, audio file, or PDF small enough to fit the 10 MB request limit after base64 encoding, skip the upload. Put a data URL in inputValue, set inputMimeType and attachmentFileName, and leave out attachmentRefId:
"inputMimeType": "image/png",
"inputValue": "[{\"file_url\":{\"url\":\"data:image/png;base64,iVBORw0KGgo...\"}}]",
"attachmentFileName": "screen.png"Send other types, such as text, CSV, or Office documents, through the upload. Inline, they would be read as captured text rather than stored as a file.
Compute merkleRootHex
SDKs compute this value for you. Over plain HTTP:
- Serialize each span object as compact JSON with its keys sorted, encoded as UTF-8.
- Hash each serialized span with SHA-256. These are the leaves, in the order of
spans. - Hash each pair of adjacent leaves together (the 32 bytes of the left followed by the 32 bytes of the right). When a level has an odd count, pair the last hash with itself.
- Repeat until one hash remains, and send it as lowercase hex.
Add a file to a session that already exists
To add a step to an existing session, repeat its root span: the same traceId, root spanId, startTimeUnixNs, and veriproof.session.id. Leave out veriproof.session.outcome so the recorded outcome doesn’t change. Use a new idempotencyKey for each request.