API Reference
Complete reference for all documents API endpoints. All routes are mounted under:
Extraction jobs are watched through the generic jobs surface at /api/v1/jobs, documented in Background Jobs.
Upload
curl -X POST http://localhost:8000/api/v1/documents \
-F "file=@renewal-letter.pdf" \
-F "title=Medicaid renewal RFI" \
-F "kind=letter"
| Status | Meaning |
|---|---|
201 |
stored; a new document |
200 |
these exact bytes are already stored, and this is that document |
The 200 is the dedupe rule answering, not an error. Fields: file (required), title, kind, document_date, source, channel, note.
List and read
GET /api/v1/documents?page=1&page_size=50&kind=letter&tag=medicaid&search=renewal
GET /api/v1/documents/{id}
GET /api/v1/documents/{id}/content # the bytes, with their media type
GET /api/v1/documents/tags # every tag with its count
include_superseded=true adds documents a newer version replaced; they are hidden by default.
Update, tag, delete
PATCH /api/v1/documents/{id} # title, kind, document_date, note,
# channel, supersedes_id, protected
POST /api/v1/documents/{id}/tags # {"label": "medicaid"}
DELETE /api/v1/documents/{id}/tags/{label}
DELETE /api/v1/documents/{id} # soft delete
| Status | When |
|---|---|
204 |
deleted |
409 |
the document is protected |
404 |
no such document for this owner |
Setting supersedes_id is how a new version retires an old one; the service refuses a document that supersedes itself, or one that is already superseded by something else.
Extraction
POST /api/v1/documents/{id}/extract # in-process, returns counts
POST /api/v1/documents/{id}/extract?background=true # queued, returns a job id
POST /api/v1/documents/{id}/extract?force=true # re-read pages already read
| Status | Body |
|---|---|
200 |
{"read": 7, "unread": 0, "skipped": 0} |
202 |
{"job_id": "cc35bde2..."} — with background=true |
502 |
the row outlived its object: storage no longer has those bytes |
Without force, pages already read are skipped, so a second run on a fully-read document returns all zeros with skipped set. See Extraction for what happens to each page.
Pages
GET /api/v1/documents/{id}/pages # one row per page
GET /api/v1/documents/{id}/pages/{number} # that page with its text
GET /api/v1/documents/{id}/pages/{number}/image # the rendered PNG
A page row carries status (read or unread), method (text_layer, vision, or none), model when a model read it, and detail — the reason, when a page could not be read. A page that failed is a row saying so, never an absence.
Watching a background extraction
JOB=$(curl -s -X POST "http://localhost:8000/api/v1/documents/2/extract?background=true" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")
curl -N http://localhost:8000/api/v1/jobs/$JOB/events
event: status
data: {"job_id": "...", "status": "running", "label": "Reading page 2 of 7..."}
event: status
data: {"job_id": "...", "status": "done", "result": {"read": 7, "unread": 0}}
The stream sends label updates and then exactly one terminal frame. GET /api/v1/jobs lists every job, running first; GET /api/v1/jobs/events is the same feed for all of them at once, which is what the modal's Activity tab reads.
See also:
- Getting Started - Service overview and data model
- Dashboard - The Overseer modal
- Extraction - What each page status means
- Background Jobs - The surface behind
background=true