Skip to main content

File Variables

Process instances hold data in variables. POST /v1/instances/{id}/variables — the endpoint you'll typically use to read and write them by hand — enforces a payload cap of 5 MB serialized (MAX_VARIABLES_PAYLOAD_BYTES) on the instance's full variables blob (existing variables merged with the patch you send), not just the patch itself. Embedding a binary file as base64 in a variable counts fully against that budget and inflates every payload that touches the instance through this endpoint — the variables blob, audit snapshots, webhook deliveries.

For anything beyond a small embedded value, upload the file directly to object storage instead. The client uploads straight to a pre-signed URL — the file body never passes through the Stateway API — and the instance variable ends up holding a small reference object (a file_ref) instead of the bytes themselves.

Base64 is still available for small values

If you just need a small blob (a signature, a thumbnail) inline with the rest of your process data, embedding base64 in a regular variable still works and is documented in Inline Types → Binary and File Data. This guide covers the Files API, which is the right tool once the payload is a real file.

The Upload Flow

Uploading a file is a three-step handshake: request a slot, upload to storage, confirm.

1. Request an upload slot

curl -X POST https://api.stateway.io/v1/instances/{instance_id}/files \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"variable_name": "contract_pdf",
"filename": "contract.pdf",
"mime_type": "application/pdf",
"size_bytes": 154213
}'

variable_name is the instance variable that will hold the file_ref once you confirm the upload — there is no BPMN-level declaration involved; see Where files stand in the type system below. mime_type must be a concrete type such as application/pdf; wildcards like application/* are rejected. filename may not contain slashes, backslashes, or control characters, and is capped at 255 characters.

Response:

{
"data": {
"ref_id": "fr_5f2c9a1e8b3d4c6f9a0b1c2d3e4f5061",
"upload_url": "https://storage.example.com/stateway-prod/tenants/.../contract.pdf?X-Amz-Signature=...",
"expires_at": "2026-08-02T10:35:00.000Z",
"method": "PUT",
"headers": {
"Content-Type": "application/pdf",
"Content-Length": "154213"
}
}
}

This response also carries X-Stateway-Storage-Used and X-Stateway-Storage-Quota headers — see Limits and quota.

upload_url is valid until expires_at (5 minutes by default). headers are the exact headers the client must send on the PUT.

2. Upload directly to storage

curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
-H "Content-Length: 154213" \
--data-binary @contract.pdf

The file body goes straight from the client to the storage backend. The Stateway API never sees it, and it never counts toward any API request size limit.

3. Confirm the upload

curl -X POST https://api.stateway.io/v1/instances/{instance_id}/files/fr_5f2c9a1e8b3d4c6f9a0b1c2d3e4f5061/confirm \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"metadata": { "source": "customer-portal" }
}'

metadata is optional, free-form business data merged into the file_ref once confirmed. Confirmation verifies the uploaded object in storage (existence and size) before it populates the variable — until this call succeeds, contract_pdf does not exist as an instance variable at all.

Response:

{
"data": {
"variable_name": "contract_pdf",
"file_ref": {
"$type": "file_ref",
"ref_id": "fr_5f2c9a1e8b3d4c6f9a0b1c2d3e4f5061",
"filename": "contract.pdf",
"mime_type": "application/pdf",
"size_bytes": 154213,
"storage": "s3",
"uploaded_at": "2026-08-02T10:31:04.000Z",
"checksum_sha256": null,
"metadata": { "source": "customer-portal" }
}
}
}

The file_ref Variable Value

Once confirmed, the variable holds a file_ref object with these fields:

FieldDescription
$typeAlways "file_ref"
ref_idStable identifier for the file reference
filenameOriginal filename as declared at slot creation
mime_typeMIME type as declared at slot creation
size_bytesMeasured size in storage (not just the declared value)
storageStorage backend identifier (currently always "s3")
uploaded_atISO 8601 timestamp of confirmation
checksum_sha256SHA-256 checksum if the storage backend returned one, otherwise null
metadataThe free-form object passed to confirm, only present if non-empty
object_key and bucket are internal storage details

object_key and bucket are internal storage details. They're stripped everywhere a variable's current value is returned or delivered as a managed representation — including nested file_ref values inside other variables: REST responses for an instance's or task's current variables, WebSocket events, webhook notification payloads (e.g. service_task.completed), the webhook delivery-history endpoint (including deliveries recorded before this protection existed), the outbound request body a serviceTask/sendTask sends to the external URL configured via its I/O mapping, and the decision evaluation history endpoints (inputVariables, outputVariables, and matchedRules).

Two narrow exceptions remain. The audit history endpoint only strips variable snapshots recorded after this protection was added — snapshots recorded earlier retain the fields. And when a businessRuleTask evaluates a DMN decision table, the raw, unprojected variable value is what the decision table actually sees and computes with while evaluating — stripping it beforehand would silently change what the table's conditions and outputs can see, which would be a correctness bug rather than a privacy improvement. This only affects that live evaluation input itself: everything the evaluation produces or is later exposed as — the decision evaluation history endpoints and the webhook notification sent afterward — strips the fields the same as everywhere else.

Separately, the pre-signed download URL and the redirect Location necessarily carry the storage path in the URL itself — that's how a pre-signed URL grants access. The key always belongs to the calling tenant's own storage layout, so this isn't a cross-tenant leak; it just means the storage path isn't secret once you have a valid download link.

Downloading a File

GET /v1/instances/{id}/variables/{name}/download redirects (302) to a pre-signed, temporary download URL:

curl -i https://api.stateway.io/v1/instances/{instance_id}/variables/contract_pdf/download \
-H "X-API-Key: sw_live_your_key"
HTTP/1.1 302 Found
location: https://storage.example.com/stateway-prod/tenants/.../contract.pdf?X-Amz-Signature=...
x-stateway-file-ref: fr_5f2c9a1e8b3d4c6f9a0b1c2d3e4f5061
x-stateway-filename: contract.pdf
x-stateway-mime-type: application/pdf

x-stateway-filename is percent-encoded UTF-8 (RFC 3986) — decode it before using it, since raw HTTP headers cannot carry non-ASCII bytes. The redirect target is valid for 5 minutes by default. This response does not carry the storage-quota headers described below — a download does not change how much storage the tenant is using.

GET /v1/instances/{id}/variables?include_download_url=true attaches a download_url field to every variable that is a confirmed file_ref:

curl "https://api.stateway.io/v1/instances/{instance_id}/variables?include_download_url=true" \
-H "X-API-Key: sw_live_your_key"
{
"data": {
"contract_pdf": {
"$type": "file_ref",
"ref_id": "fr_5f2c9a1e8b3d4c6f9a0b1c2d3e4f5061",
"filename": "contract.pdf",
"mime_type": "application/pdf",
"size_bytes": 154213,
"storage": "s3",
"uploaded_at": "2026-08-02T10:31:04.000Z",
"checksum_sha256": null,
"download_url": "https://storage.example.com/stateway-prod/tenants/.../contract.pdf?X-Amz-Signature=..."
}
}
}

A variable whose upload has not been confirmed yet, or that has already been deleted, simply has no download_url field in this listing — it is not an error, since one pending or deleted file among many variables shouldn't fail the whole listing. Use the dedicated download endpoint above if you need the precise reason.

Deleting a File (LGPD/GDPR)

DELETE /v1/instances/{id}/variables/{name} removes the object from storage and replaces the variable with a tombstone:

curl -X DELETE https://api.stateway.io/v1/instances/{instance_id}/variables/contract_pdf \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "reason": "customer requested erasure" }'

reason is optional free text (1–255 characters); it defaults to "unspecified" when omitted.

Response:

{
"data": {
"variable_name": "contract_pdf",
"status": "deleted",
"tombstone": {
"$type": "file_ref_deleted",
"ref_id": "fr_5f2c9a1e8b3d4c6f9a0b1c2d3e4f5061",
"deleted_at": "2026-08-02T11:00:00.000Z",
"deleted_by": {
"actor_type": "api_key",
"actor_id": "...",
"tenant_id": "..."
},
"reason": "customer requested erasure"
}
}
}

The deletion is idempotent: repeating the same DELETE call returns the same tombstone rather than an error. After deletion, the download endpoint responds 410 for that variable, with the tombstone in the error details.

Limits and Quota

LimitDefaultNotes
Max file size20 MBMAX_FILE_SIZE_BYTES; enforced when requesting the upload slot
Upload URL validity5 minutesFILE_UPLOAD_URL_TTL_SECONDS; the deployment may configure 60–600 seconds
Confirmation grace period60 seconds after the upload URL expiresConfirming after this window returns 410 UPLOAD_EXPIRED
Download URL validity5 minutesFILE_DOWNLOAD_URL_TTL_SECONDS; the deployment may configure 60–600 seconds
Declared vs. measured size tolerance1 byteLarger mismatches fail confirmation with 422
Storage quota per tenant5 GBShared across all instances of the tenant; contact support to increase it

The three mutating endpoints — request slot, confirm, delete — return X-Stateway-Storage-Used and X-Stateway-Storage-Quota response headers (bytes), reflecting the tenant's storage usage immediately after the operation. The download redirect does not return these headers.

Exceeding the storage quota fails the operation with 402 STORAGE_QUOTA_EXCEEDED, whether at slot creation or — if another upload consumed the remaining quota in the meantime — at confirmation.

Errors

StatuscodeEndpoint(s)Meaning
400VALIDATION_ERRORRequest slot, Confirm, DeleteMalformed body, size_bytes exceeds the max file size, mime_type is a wildcard, or reason is empty/too long
402STORAGE_QUOTA_EXCEEDEDRequest slot, ConfirmThe tenant's storage quota is exhausted
404NOT_FOUNDRequest slot, Confirm, Download, DeleteInstance, tenant, variable, or file reference not found
409CONFLICTRequest slot, Confirm, DeleteInstance not running/suspended (slot); upload already confirmed (confirm); or the variable pointed at a different file reference while the deletion was in flight — retry (delete)
410FILE_REF_DELETEDConfirm, DownloadThe file was already deleted (tombstone in details, or just the ref_id if the variable itself is stale)
410UPLOAD_EXPIREDConfirmThe upload slot expired past its grace period
422PROCESS_ERRORConfirmThe uploaded object is missing from storage, or its measured size doesn't match size_bytes beyond the 1-byte tolerance
422FILE_REF_PENDINGDownload, DeleteThe upload hasn't been confirmed yet
422NOT_A_FILE_VARIABLEDeleteThe named variable exists but isn't a file_ref
422VARIABLE_TYPE_MISMATCHRequest slotThe target variable has a declared type that isn't a stateway:file scalar (a different type, or a stateway:file collection)

Where Files Stand in the Type System

You can, optionally, declare a variable's BPMN type as stateway:file:

<bpmn:itemDefinition id="ContractDocumentType" structureRef="stateway:file" />
<bpmn:dataObject id="ContractDoc" name="contract_document" itemSubjectRef="ContractDocumentType" />

This doesn't change how the upload flow works — the link between a file and the process is still made purely by the variable_name you pass when requesting the upload slot, whether or not that variable has a declared type. What declaring the type adds:

  • Shape validation. The variable only accepts a file_ref (see above) or its deletion tombstone — any other value is rejected the same way any other typed variable's value would be.
  • A guard at the upload slot. Requesting a slot for a variable declared with a different type (or a stateway:file collection — the Files API doesn't support appending to collections yet) fails with 422 VARIABLE_TYPE_MISMATCH before anything is uploaded.

A variable with no declared type still accepts uploads exactly as before — declaring stateway:file is optional, not a prerequisite for using this API.

See Inline Types → Binary and File Data for how this compares to embedding base64 data in a typed variable.

Retention

Files that belong to instances that have already completed or been terminated are automatically removed from storage, with a tombstone left in their place, after a retention period of approximately 5 years — the exact cutoff includes a short additional grace window, so a file may be retained a little past the 5-year mark before removal. This is separate from on-demand deletion above — it exists as a backstop so storage isn't retained indefinitely for instances no one is acting on anymore.