Files
Create, upload, scan, download, and request deletion of Workspace Files.
Endpoints under /api/v1/files (API keys) and /api/files (session; same handlers). JSON
success uses api-contracts.v1: { ok: true, data, meta: { contractVersion: "api-contracts.v1" } }.
Failure: { ok: false, error: { code, message } }. GET .../{id}/content is a raw byte
stream, not that envelope.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/files | List files in the key workspace |
| POST | /api/v1/files | Create upload metadata (201) |
| GET | /api/v1/files/{id} | Get serialized metadata |
| DELETE | /api/v1/files/{id} | Request deletion (202) |
| PUT | /api/v1/files/{id}/content | Store bytes (201 { blob }) |
| GET | /api/v1/files/{id}/content | Download bytes (200 / 206) |
| POST | /api/v1/files/{id}/complete | Verify blob, scan (202 { file }) |
POST, PUT content, POST complete, and DELETE require Idempotency-Key.
Metadata and downloads require workspace.restricted.read (session controls can be stricter).
GET .../{id}/content?preview=true verifies raster content and returns an inline PNG, JPEG, WebP
or GIF. The default is an attachment. Both return Cache-Control: private, no-store and
X-Content-Type-Options: nosniff. Preview rejects active formats such as SVG and HTML.
DELETE returns 409 while a table record retains the file. Remove all table associations first;
other owners and the existing GC retention policy remain in force.
List
GET /api/v1/files
| Query | Notes |
|---|---|
cursor | Optional opaque base64url { createdAt, id }. Invalid: 400. |
limit | Optional. Default 50, integer 1–100. Else 400 request_malformed. |
data: { files, nextCursor } (nextCursor null when no further page). Order: createdAt
desc, id desc. No status filter.
Create upload
POST /api/v1/files — 201.
{ "name": "report.pdf", "mimeType": "application/pdf", "sizeBytes": 4 }
| Field | Notes |
|---|---|
name | Required, trim, 1–255 |
mimeType | Required, trim, 1–255 |
sizeBytes | Integer 0–104857600 (100 MiB) |
sha256 | Optional 64 hex; stored lowercased |
data: { file, uploadUrl, completeUrl }. URLs are {collection}/{id}/content|complete plus
session workspaceId query when present.
Write readiness 503 on preview/production when Files is blocked. Replay of a successful create
still 201 even if Files later becomes blocked (idempotency record). A new key is 503.
Get and delete
GET /api/v1/files/{id} — { file }.
DELETE /api/v1/files/{id} — 202 { file } with status: "deletion_requested" (or unchanged
if already requested/deleted).
Deletion is refused while a table row or live image-change operation retains the file. For supported
raster images, including server-validated images uploaded as application/octet-stream, live workflow execution,
saved-run and pinned-output references also block deletion, including after the last table association is cleared.
Generic octet-stream files do not receive this image-specific restriction. A reference whose lease
has expired does not block deletion; remove permanent owning runs or pins to release their references.
Put content
PUT /api/v1/files/{id}/content — raw body required. Content-Length > 100 MiB → 413.
data: { blob } (store stat: key, size, sha256). Only pending files.
Complete and scan
POST /api/v1/files/{id}/complete — body {} or { sizeBytes?, sha256? }. Completes then scans
if pending_scan. 202 { file }.
File object (serialized)
| Field | Notes |
|---|---|
id | UUID (or caller-supplied in service only; HTTP create uses random UUID) |
name | string |
mimeType | string |
sizeBytes | number |
sha256 | 64 hex; "0"×64 until complete |
status | pending | pending_scan | ready | quarantined | deletion_requested | deleted |
createdAt | ISO-8601 |
updatedAt | ISO-8601 |
Storage keys, encryption, and scan rows are not serialized.
Content GET headers
| Header | Value |
|---|---|
accept-ranges | bytes |
content-type | File MIME |
content-disposition | attachment; filename="…" |
etag | "sha256-{hex}" |
content-length | Full or range length |
content-range | On 206: bytes start-end/total |
Range must match bytes={start}-{end} (end optional). Other forms: 400.
If start exists and end exceeds the file length, the response is 206 and ends at
the file's last byte. A start outside the file or an end before the start returns 416.
Error map
| HTTP | error.code | Typical cause |
|---|---|---|
| 400 | idempotency_key_invalid | Missing/invalid Idempotency-Key |
| 400 | validation_failed | Bad create/complete JSON |
| 400 | request_malformed | Bad limit/cursor/range/missing body |
| 401 | authentication_required | v1 bearer |
| 403 | authorization_failed | Non-workspace key |
| 404 | resource_not_found | File or blob missing in this workspace |
| 409 | conflict | Wrong status, checksum/size mismatch, scan lease |
| 409 | idempotency_replay_mismatch | Same key, different body |
| 413 | validation_failed | Over 100 MiB |
| 416 | validation_failed | Invalid byte range |
| 423 | conflict | File not ready (download) |
| 503 | service_unavailable | Write readiness blocked |
| 500 | internal_error | Unclassified |
Use a file in a workflow
Upload and complete the file with this API, wait for ready, then pass its ID in
input.fileInputs to the workflow execute endpoint.
The execution verifies workspace ownership and resolves bytes only for consumers such as extraction
or an HTTP binary/multipart body. Form and Webhook upload tokens cannot list or download Workspace
Files. Inspector downloads use session authorization and the run's workspace.
