Repository navigation
API Reference
An index of the HTTP API: 32 route files under app/api/. The full reference, with every body, response and status per route, is docs/api.md.
There are no accounts. Three kinds of authorization appear, each answering a different question:
| Kind | Established by | Authorizes |
|---|---|---|
| Session | the httpOnly anonify_sid cookie (random id, 30 days, rolling on uploads and rule edits); identity derived in lib/security/fingerprint.ts
|
ownership of your own documents, batches and global rules |
| Signed token | an HMAC token from lib/security/signed-url.ts, passed as ?token=, valid 5 minutes
|
one download: a document artifact (domain download-url|) or a batch archive (domain batch-download-url|) |
CRON_SECRET |
Authorization: Bearer $CRON_SECRET |
the cleanup sweep (required in production) |
A signed token is necessary but not sufficient. The download route still re-derives your session, re-checks ownership, and hashes the bytes as they stream against the checksum recorded at export.
Ownership is not probeable. requireDocument is the gate every document read passes through: no session → 401; not found or not yours → 404 (never 403); expired → 410. Batches follow the same 404 rule.
Patterns travel in bodies, never query strings, because a search term is usually the value being redacted, and URLs end up in logs.
{ "error": "message", "...extra": "..." }A rate-limit refusal adds retryAfterSeconds, resetAt and rateLimited: true, with a Retry-After header. Verification and storage failures surface as 500 with a generic message, and the detail goes to the operator log only.
| Status | Meaning |
|---|---|
200 / 201 / 202
|
success / created / durable run started |
400 |
invalid body or params, or a pattern the compiler refuses |
401 |
no session, or a missing / invalid / expired download token |
403 |
a valid token that is not yours |
404 |
not found or not yours |
409 |
conflicts with current state (not ready, already running, not in a batch, over 200 global rules, …) |
410 |
the document has expired |
413 |
upload over MAX_UPLOAD_BYTES, or a Hush conversation over its size |
415 |
restore: unrecognised file type |
422 |
a search or rule ran out of its budget (nothing applied); restore not possible; a withheld mailbox |
429 |
rate limit or quota refused |
500 |
verification failure, storage failure, or an unexpected error |
503 |
missing configuration, the database, or (Hush) no usable model or the budget reached |
| Method | Path | Rate limit | Does |
|---|---|---|---|
GET |
/api/documents |
none | your documents, newest first ([] with no session) |
POST |
/api/documents |
upload |
reserve one document: { filename, size, contentType?, preset?, ttlSeconds? } → { id, pathname, expiresAt, quota, uploadMode }
|
GET |
/api/documents/:id |
read |
one DocumentSummary
|
PATCH |
/api/documents/:id |
processing |
extend retention, recomputed from creation and capped at 72 h (attachments follow their parent) |
DELETE |
/api/documents/:id |
none | purge every artifact, then the row (the row is kept if storage failed) |
GET |
/api/documents/:id/content |
read |
the normalized model: ?view=outline, ?page=N (one ranged read), or whole |
GET |
/api/documents/:id/source |
read |
the decrypted source, streamed, no-store, private, nosniff
|
POST |
/api/documents/:id/retry |
processing |
re-run a failed document; 409 for non-retryable codes |
GET |
/api/documents/:id/usage |
none | tokens, duration, calls by task, cost if priced, degradation |
| Method | Path | Rate limit | Does |
|---|---|---|---|
POST |
/api/documents/:id/process |
processing |
hand a finished upload to the pipeline: { blobUrl } (a URL, local:… or s3:…) → 202 { runId }, or the existing run { runId, resumed: true }. Queued until admitted. |
GET |
/api/documents/:id/stream |
none | SSE progress: frames id: <n>\ndata: <json>, ending with event: end; ?startIndex=last+1 to resume; cancellable |
| Method | Path | Rate limit | Does |
|---|---|---|---|
GET |
/api/documents/:id/redactions |
read |
every redaction, by page then offset |
POST |
/api/documents/:id/redactions |
none | create a manual redaction (text / region / cell / …; defaults source: user, status: accepted) |
PATCH |
/api/documents/:id/redactions |
none | bulk { ids (≤2000), status?, method? }
|
DELETE |
/api/documents/:id/redactions?redactionId= |
none | delete one redaction |
POST |
/api/documents/:id/search |
search (summary) / read (page) |
server-side find: { spec, page? } → hits per page and cells, or one page's { start, end } offsets; 422 over 50,000 matches |
| Method | Path | Rate limit | Does |
|---|---|---|---|
GET |
/api/documents/:id/rules |
read |
every rule reaching this document (document, batch, global), with here, total, documents, copyId
|
POST |
/api/documents/:id/rules/preview |
search (processing for batch) |
count + first 20 matches in context; for a batch, the count per document. Writes nothing. |
POST |
/api/documents/:id/rules |
processing |
create { pattern, kind, matchCase, wholeWord, category, scope }; planned within budget everywhere, then one transaction; accepted redactions; 422 if over budget |
PATCH |
/api/documents/:id/rules |
processing |
{ id, scope, enabled?, spec?, category? }: off/on or rewrite, everywhere it reached |
DELETE |
/api/documents/:id/rules?ruleId=&scope= |
processing |
remove a rule and every redaction it made |
GET |
/api/rules |
read |
your global rules, patterns unsealed for you |
GET |
/api/rules/export |
export |
{ format: "anonify.rules", version: 1, rules: [...] } |
POST |
/api/rules/import |
processing |
→ { imported, duplicates, invalid: [{ index, problem }] }; 409 past 200 rules |
Patterns: { kind: "literal" | "regex", pattern (≤500), matchCase, wholeWord }, compiled by RE2JS (no backreferences or lookaround, no empty matches), with 5 s and 10,000 matches per document. See Search and Rules.
| Method | Path | Rate limit | Does |
|---|---|---|---|
GET |
/api/assistant |
read |
{ available, model, provider: { id, label, kind, model } } or { available: false, reason }
|
POST |
/api/documents/:id/assistant |
processing |
one streamed agent turn (AI SDK UI message stream): { messages (≤80), readConsent, view }, ≤1.5 MB. Reads wait for consent; changes wait for a signed approval. 429 code: "allowance"; 503 code: not-configured | unsupported | budget. |
See Hush Assistant.
| Method | Path | Rate limit | Does |
|---|---|---|---|
POST |
/api/documents/:id/export |
export ×variants |
{ addLabels, sanitizeMetadata, imageStyle, methods?, variants? (≤4) } → plan, generate, verify, report-check, checksum, seal → { artifactId, variant, checksum, size, appliedRedactions, verifiedValues, downloadUrl, reportUrl, report, vault, artifacts[] }. The vault is inline and stored nowhere. 409 not ready; 500 refused by verification (nothing saved); 400 malformed. |
GET |
/api/documents/:id/download?token=[&part=report] |
none | the artifact or its report, streamed, last piece held until the checksum matches; non-Latin-1 filenames correct |
POST |
/api/restore |
export |
multipart file + vault → the original bytes, with X-Restored-Values, X-Unresolved-Values, X-Vault-Matches. Stores nothing, takes no id. 422 for PDF/images or no matches. |
| Method | Path | Rate limit | Does |
|---|---|---|---|
POST |
/api/batches |
per file, as single uploads | reserve many: { files[], preset?, ttlSeconds? } → { batchId, accepted[{ index, id, pathname }], refused[{ index, filename, reason, retryAfterSeconds }], uploadMode }
|
GET |
/api/batches/:id |
read |
the batch: documents, statuses, carried decisions |
POST |
/api/batches/:id/search |
processing |
count per document (null + note while processing) |
PATCH / DELETE
|
/api/batches/:id/rules |
processing |
switch, rewrite or withdraw a batch decision everywhere |
POST |
/api/batches/:id/export |
processing |
start (or join) a durable export: { addLabels, sanitizeMetadata, imageStyle, method, methodByDocument, output } → 202
|
GET |
/api/batches/:id/export |
none | where it has got to, with a freshly minted downloadUrl when deliverable |
DELETE |
/api/batches/:id/export |
none | stop: the current document finishes and is kept |
GET |
/api/batches/:id/export/stream |
none | SSE snapshots; 409 if not running |
GET |
/api/batches/:id/download?token=&output=original|processed|both[&document=][&part=report] |
none | the archive, streamed in two passes: a rebuilt mailbox, provenance folders, batch-report.json, vaults beside files; 409 nothing exported; 422 a mailbox withheld |
| Method | Path | Does |
|---|---|---|
POST |
/api/upload/token |
Vercel Blob only: a client token scoped to one reserved path. onBeforeGenerateToken checks the owner, status: uploading and the exact path, and bakes in the size ceiling and a 30-minute expiry. onUploadCompleted (the production webhook) records uploadBlobKey. 409 if Blob is not configured. |
POST |
/api/upload/local |
every other backend: multipart documentId + file, streamed into the configured driver → 201 { url, size }; 413 too large; 409 already uploaded |
| Method | Path | Does |
|---|---|---|
GET |
/api/limits |
{ profile, rateLimits[], quotas, batch: { maxFiles, processing, exporting } }; read-only, and not itself rate limited
|
GET |
/api/usage |
analysis usage across your documents: tokens, duration, calls, by model, estimatedCostUsd
|
GET |
/api/cron/cleanup |
the sweep: mark, purge, prune, and admit stalled queued documents → { marked, …, admitted }
|
POST /api/documents → { id, pathname, uploadMode }
uploadMode = vercel-blob:
POST /api/upload/token → token; PUT bytes to Blob (multipart > 5 MiB)
otherwise:
POST /api/upload/local (multipart, with progress)
POST /api/documents/:id/process → 202 { runId } (queued → admitted)
GET /api/documents/:id/stream → SSE until document.ready
GET /api/documents/:id/content?view=outline, then ?page=N
POST /api/documents/:id/search → hits (optional)
POST /api/documents/:id/rules/preview → POST …/rules (optional)
PATCH /api/documents/:id/redactions → accept / reject / method
POST /api/documents/:id/export → { downloadUrl, reportUrl, vault, … }
GET <downloadUrl> → the bytes (within 5 minutes)
Anonify: AI proposes, the application applies, and only what a person accepted is removed.
Repository · Report a bug · Request a feature · Security policy · Apache-2.0
This wiki describes v1.10.0 and summarises and links the in-repo documentation. Where the two disagree, the files under docs/, CHANGELOG.md and SECURITY.md are canonical.
Getting going
Using it
- Supported Formats
- Review Workspace
- Search and Rules
- Hush Assistant
- Redaction Presets
- Export and Anonymisation
- Batches and Mailboxes
How it works
- Architecture
- Processing Workflow
- Redaction Engine
- Storage and Encryption
- Streaming IO
- Data Model
- API Reference
- Security Model
When it breaks
Contributing
Repository · Issues · Releases · Changelog