POST an upload, get a file_id back immediately, poll until the job is
ready, then download the redacted file in the same format you sent./mask there is no envelope
and no recovery; detected entities are removed from the document in
place. It is the file analogue of /redact.404 (never 403), so file ids can't be
probed.| Method | Path | Purpose |
POST | /api/v1/files/redact | Upload (multipart) and enqueue a redaction job. Returns 201. |
GET | /api/v1/files | List your files, newest first (keyset pagination). |
GET | /api/v1/files/{file_id} | Poll one job's state and metadata. |
GET | /api/v1/files/{file_id}/download | Stream the redacted file (ready only). |
POST | /api/v1/files/{file_id}/pin | Keep the file beyond the TTL. |
POST | /api/v1/files/{file_id}/unpin | Restore the TTL. |
DELETE | /api/v1/files/{file_id} | Delete the row and stored bytes now. Returns 204. |
POST /api/v1/files/redact, multipart/form-data:| Form field | Required | Description |
file | yes | The document. The part's declared content type must be one of the supported formats below. |
glossary_id | — | UUID of a persisted glossary you own; 404 if not found or not yours. |
glossary | — | Inline custom terms as a single string field: either a JSON array of strings (["Al-Noor, Riyadh Branch", "Project X"], lossless, terms may contain commas) or a plain comma-separated list. |
| Kind | Formats |
| Documents | PDF, DOCX, XLSX, PPTX, CSV |
| Images | PNG, JPEG, TIFF (OCR'd) |
201 with:| Field | Type | Description |
file_id | UUID | The job id. |
state | string | Always "queued" on upload. |
created_at | datetime | Upload time (UTC). |
expires_at | datetime | When the redacted output's TTL elapses (24 h after upload by default). |
poll_url | string | Relative path to poll (/api/v1/files/{file_id}). |
123queued → processing → ready ↘ failed ready ─(TTL elapses, unpinned)→ expired
state | Meaning |
queued | Durably enqueued; a worker will claim it. |
processing | A worker is extracting, masking, and reassembling. |
ready | Redacted and independently re-verified; download_url is set. |
failed | Terminal; error_code says why. No output is ever returned. |
expired | The TTL elapsed on an unpinned file: the redacted bytes are destroyed, the row is kept for a while so you get 410 instead of a bare 404. |
ready; a file that cannot be proven clean fails closed
(redaction_incomplete / verify_failed) rather than being returned.GET /api/v1/files/{file_id} (and each row of the list response):| Field | Type | Description |
file_id | UUID | Job id. |
state | string | See the state table. |
original_filename | string | As uploaded ("untitled" if the part had no filename). |
content_type | string | The declared upload content type. |
size_bytes_in | integer | Uploaded size. |
size_bytes_out | integer | null | Redacted output size; null until ready. |
word_count | integer | null | Words extracted and masked; the billing basis. |
entity_count | integer | null | Entities redacted. |
created_at | datetime | Upload time. |
processed_at | datetime | null | When processing finished. |
expires_at | datetime | TTL boundary. |
is_persisted | boolean | true while pinned (exempt from the TTL sweep). |
error_code | string | null | Set when state is failed; see the error table. |
download_url | string | null | /api/v1/files/{file_id}/download when ready; null otherwise. |
poll_url | string | Self link. |
GET /api/v1/files?limit=50&cursor=<file_id> returns
{ "files": [...], "next_cursor": "<id>" | null }. limit is clamped
to 1–50. Pass next_cursor back as cursor for strictly older rows;
an unknown or malformed cursor returns 400 invalid_cursor.| Status | detail | Trigger |
400 | unsupported_format | Content type not supported, or magic bytes don't match (corrupt, legacy binary Office, or password-protected file). |
402 | insufficient_tokens | Read-only balance pre-check failed: no free quota and no token balance. Nothing is enqueued or charged. |
404 | Glossary not found | The glossary_id isn't yours or doesn't exist. |
413 | file_too_large | Over the 100 MB cap (rejected up front from Content-Length where possible, and mid-stream otherwise). |
429 | too_many_concurrent_jobs | More than 2 jobs queued/processing at once. |
429 | quota_active | More than 50 files in queued/processing/ready states. |
429 | too_many_files_today | The 100-uploads-per-day cap (counted per Asia/Riyadh day, any state, resets at Riyadh midnight). |
404 not_ready while the job hasn't reached ready,
410 expired after the TTL destroyed the bytes.state becomes expired). Pin a file (up to 5 pinned) to keep
it; unpin to restore the TTL. DELETE removes it immediately.word_count at the same
per-token rate as the text endpoints, plus a small per-page surcharge
for paged formats (10 tokens per PDF page or PPTX slide; a standalone
image counts as one page). The real debit happens when the worker
processes the job; a failed job nets to zero.