Skip to content

API Reference

Nabeel Wasif edited this page Sep 29, 2026 · 2 revisions

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.


Auth model

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 shape

{ "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 codes

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

Documents

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

Processing

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

Review

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

Rules

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.

Hush

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.

Export, download and restore

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.

See Export and Anonymisation.

Batches

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

See Batches and Mailboxes.

Upload

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

System

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 }

A typical client flow

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)

See also

Clone this wiki locally