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.
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).
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.
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 |
