REST API
bibr serve runs a LitServe
application exposing the bibr extraction pipeline over HTTP. See
Production deployment for running the server
(Docker, concurrency, hardware sizing); this page documents the REST
surface — endpoints, request/response shapes, caching, and error codes.
The server binds to 127.0.0.1:8000 by default. Interactive docs are at
/docs (Swagger UI) and /redoc; these and /openapi.json require the same
bearer token as extraction when authentication is enabled.
Endpoints
Health
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness probe -- LitServe's built-in endpoint; returns plain-text ok (status 200) |
GET |
/ready |
Readiness probe -- checks OCR service, configured classifier artifacts, and Redis when response caching is enabled; 200 when ready, 503 otherwise |
The probes are public. With authentication enabled, an anonymous /ready
response contains only {"status": "ready"} or {"status": "not_ready"}.
A valid bearer token also exposes checks and the deployment build_sha.
/ready reuses its OCR, Redis and job-store results for 2 seconds, and
concurrent probes share one round of checks, so a change in those services
shows within 2 seconds and probe traffic does not multiply requests to them.
Papers
POST /papers/extract
Extract metadata from a scientific paper synchronously. Returns JSON.
Request: multipart/form-data
This is the only public synchronous extraction ingress. The complete multipart
body is limited to 51 MiB by default, including boundaries, headers, and form
fields; the file bytes within it are limited to 50 MiB (with MCP_ENABLED
the body cap grows to fit a 50 MiB file in base64 form — see the MCP guide).
At most 1 MiB of the upload remains in API memory before the multipart spool
rolls to disk.
Exactly one file part is accepted. The nine optional fields below must each
appear at most once and are capped at 64 bytes; duplicate parts, a second file,
a file part under another name or more than 25 text parts in all return 400.
A text field the route does not know is ignored, and up to 16 of them fit
alongside every field below.
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | Paper file (PDF, DOCX, XML/JATS, HTML, or ePub) |
start_page |
int | No | Start page for PDFs (0-indexed, inclusive) |
end_page |
int | No | End page for PDFs (0-indexed, inclusive) |
include_figures |
bool | No | Emit figure images as data: URIs (default: false) |
include_regions |
bool | No | Emit the extraction.regions layout debug payload (default: false). Contains per-region geometry and recognition content; response size depends on the document. |
include_region_meta |
bool | No | Emit extraction.text_regions: the page, box (PDF points from the top-left), font and region type of each text row, keyed by text_id (default: false). This is what a viewer needs to highlight a sentence on the source PDF; it adds about a fifth to the response. Text without page layout (DOCX input, a scanned PDF without a native text layer) has no row. |
crossref |
bool | No | Run Crossref/resolver reference enrichment for this request (true) or skip it (false). Omit to follow the server's CROSSREF_ENRICH setting, which is off by default. The response cache keys on the effective value. |
consolidate |
fill | replace |
No | Merge accepted Crossref matches into bib before export (fill fills only missing fields, replace also overwrites disagreeing ones, but only from a match carrying the reference's printed DOI; a match's catch-all bib_type other fills a missing type but never replaces a printed one). Omit to defer to the server's CROSSREF_CONSOLIDATE setting. |
refs |
ner | llm | llm-chunked | ref-llm | off |
No | Per-request override of the reference-parsing strategy (REF_PARSE_STRATEGY). |
ref_seg |
geom | region | llm | crf |
No | Per-request override of the reference-segmentation strategy (REF_SEG_STRATEGY). |
Response: JSON conforming to the bibr v12.3 schema. Top-level keys, grouped by role: the paper and its input file — paper_id, schema_version (its presence at the root is how readers dispatch v11 and later from earlier versions), source (input-file identity: file name, SHA-256, format); what the paper says — metadata (scalar paper-level metadata), author, affiliation, funding, text, section, url, bib, xref, figure, table, footnote, eq; what external registries returned — metadata_match (matches for the paper's own identity), affiliation_match and funding_match (ROR organizations), bib_match; how the output was produced — extraction (engines, per-run settings, timings, LLM usage, enrichment completeness, identity receipts, qualification provenance, the output-validation result (extraction.validation: error/warning counts, promotion disposition and issue list), diagnostics receipts, the state of each tracked field (extraction.fields, since 12.1: whether title, author, abstract, keywords, doi, published, journal, funding_statement, funding, paper_type and bib were extracted, absent, abstained, failed or not_attempted, with the producing step, the rule of the field's decision and the codes that explain the state), figure/table piece locations and warnings ({code, message} objects); regions is added there when include_regions=true, and text_regions when include_region_meta=true). Every key is always present, and content rows carry no processing fields. Figure and table rows describe the whole object.
metadata is scalar-only by design — pipeline telemetry lives under extraction and the input file's identity under source — so R consumers can call as.data.frame(metadata) cleanly.
Within major version 12 the schema is additive-only: new optional fields and
new enum values may appear in any 12.x release, and clients should ignore
keys they don't recognize and accept enum values they don't know. Dispatch on the presence of the root schema_version key, never
on parsing its value — pre-v11 responses have no such key at all. See
CHANGELOG.md for the v12 break and forward-versioning policy.
Example:
curl -X POST http://localhost:8000/papers/extract \
-F "file=@paper.pdf" \
-F "include_regions=false" \
-F "crossref=true"
LitServe's internal POST /_bibr/inference route accepts only the API
process's opaque disk descriptor and returns 404 to direct HTTP callers.
Only the upload UUID, filename, size, SHA-256, and extraction options cross the
worker queue; neither bytes nor a filesystem path do. Worker decode securely
reads and verifies the owned file once, then deletes it.
Async jobs
Holding an HTTP connection open for a full extraction (tens of seconds) is fragile behind proxies and load balancers. These routes offer a fire-and-poll alternative instead:
POST /papers/jobs
Accepts the same multipart/form-data fields as /papers/extract.
Returns 202 immediately with {"job_id", "status": "queued",
"status_url"} and runs the extraction in the background. It persists the
upload once and dispatches the same opaque descriptor through LitServe; it
does not rebuild or self-proxy a multipart request.
GET /papers/jobs/{id}
Returns the job's status (queued, running, succeeded, or failed)
plus timestamps — no result body. A succeeded status includes a
result_url pointing at the next endpoint.
GET /papers/jobs/{id}/result
Returns the extracted paper JSON once the job has succeeded (same shape
as /papers/extract's response). Responds 409 while the job is still
queued/running, or the job's original error and status code if it failed.
GET /papers/jobs/{id}/references
Returns the job's reference list as a file a reference manager imports, and
answers like /result while there is no result (404, 409, or the job's
error). The query parameters are:
| Parameter | Default | Values |
|---|---|---|
format |
csl-json |
csl-json, bibtex, ris |
prefer |
printed |
printed, record |
check_note |
true |
false leaves out the reference-check note |
format |
Content-Type |
File name |
|---|---|---|
csl-json |
application/vnd.citationstyles.csl+json; charset=utf-8 |
{id}.json |
bibtex |
application/x-bibtex; charset=utf-8 |
{id}.bib |
ris |
application/x-research-info-systems; charset=utf-8 |
{id}.ris |
The file name comes in a Content-Disposition: attachment header. Any other
format or prefer answers 422. With the default prefer=printed every
value is printed in the reference; prefer=record lets a matched Crossref
record fill or replace them, with a note naming the values it supplied. The values written are those of
Result.to_bibtex():
curl -H "Authorization: Bearer $KEY" -o refs.bib \
"http://localhost:8000/papers/jobs/$JOB/references?format=bibtex"
DELETE /papers/jobs/{id}
Cancels a job that is still queued. The job is failed at once with the
error {"detail": "job cancelled before it started", "error_code":
"job_cancelled"}: it stops counting against JOBS_MAX_ACTIVE, its upload is
deleted, and /result answers 410. Clients that poll for succeeded or
failed therefore stop as they do for any failed job. Returns 200 with the
job's status (also when it was already cancelled), 409 with
{"detail", "status"} for a job that is running or finished (a running
extraction cannot be stopped yet), and 404 for an unknown job. With the
Redis job store, a job cancelled through a replica other than the one that
accepted it keeps its upload on that replica's disk until a job worker there
reaches it or that replica's queue fills up to JOBS_MAX_ACTIVE entries.
With JOBS_DEDUPE_INFLIGHT=true (default false), a POST /papers/jobs
whose file (SHA-256), filename and options match a job the same server still
has queued or running answers 202 with that job's job_id, its current
status and "duplicate": true, instead of running the paper a second time.
Options are compared by value (1/true, 02/2); an option left out does
not match the same option sent with its default, and a renamed copy of a file
runs again (the export names the file it was sent as). The JOBS_MAX_ACTIVE
cap is checked before the duplicate lookup, so a full server answers a
duplicate with 429 like any other upload. bibr serve has one principal (the
shared API key), so every caller can be handed every other caller's job: enable
it only when that is fine, and deduplicate per user in a multi-user front end
instead.
Jobs are held in an in-process store and purged after JOBS_TTL_SECONDS
(default 3600); JOBS_MAX_ACTIVE (default 32) caps concurrently
active jobs, returning 429 past the cap, and JOBS_MAX_RUNNING (default
2) caps how many run at once. Completed results are also evicted
oldest-first beyond JOBS_MAX_RETAINED results (default 128) or
JOBS_MAX_RETAINED_BYTES of encoded JSON (default 256 MiB; 0 disables the
byte budget); the newest result is always kept, so a fetch of /result can
answer 404 once a result has been evicted. The whole async API can be
disabled with JOBS_ENABLED=false. By default (JOBS_STORE=memory) the job
queue, status, and results are process-local and a server restart loses them.
JOBS_STORE=redis keeps status and results in Redis instead, so any replica of
a load-balanced deployment answers the polls for a job another replica accepted
and the active-job cap spans all replicas — see
Multiple bibr-serve replicas.
Each status carries replica, the instance executing the job; with the Redis
store unreachable the job routes answer 503. A job whose replica stopped before
it finished is failed with 503, so the client resubmits it: with error_code
job_lost when the replica died (by the first upload after its one-minute lease in
Redis runs out), or {"detail": "replica shut down before the job finished"} after
a clean shutdown. The service always pins one HTTP
API process per instance—even with jobs disabled—because upload ownership and
dispatch tracking are process-local. PIPELINE_RESTART_WORKERS=false
fail-stops on worker death; true is an unsupported opt-in until the locked
LitServe compatibility gate proves reliable completion notification and does
not make jobs durable.
Authentication
Set AUTH_API_KEY to require a bearer token on every route except
/health and /ready:
curl -X POST http://localhost:8000/papers/extract \
-H "Authorization: Bearer your-secret-token" \
-F "file=@paper.pdf"
A missing or wrong token gets a 401 with a WWW-Authenticate: Bearer
header. A key with non-ASCII characters matches whether the client sends it
UTF-8-encoded (curl) or latin-1-encoded (browsers, Python's http.client).
When AUTH_API_KEY is unset, the CLI permits loopback-only serving,
and the server then refuses non-loopback Host headers (421) and
state-changing requests from other sites (403); network-visible binds
require a key at least 32 characters long.
See Authentication in the
deployment guide for the production-hardening checks (ENVIRONMENT=production)
that force it on.
Caching
When CACHE_ENABLED=true (the default) and Redis is configured, the API caches
successful extraction responses that are final: a response shaped by a failure
a retry could avoid is not cached, so the next request runs the extraction
again. That covers a blocking validation issue other than a front-matter
abstention (VAL_METADATA_MULTI_ITEM), incomplete enrichment, and warnings
such as OCR_REGION_FAILED, CROSSREF_ENRICHMENT_TIMEOUT or an LLM task's
*_LLM_FAILED. A deterministic failure, such as REF_SEG_FAILED, fails the
same way on every run, so its response is cached. Keys distinguish file
content (its full SHA-256), the file extension (which picks the parser), page
range, figure/region output, consolidation, and reference-strategy overrides.
The cache namespace also includes a settings fingerprint and code version.
Identical concurrent cache misses are coalesced; failed Redis operations are
bounded and extraction continues without the cache.
Configure caching:
| Variable | Description | Default |
|---|---|---|
CACHE_ENABLED |
Enable response caching when Redis is configured | true |
REDIS_URL |
Redis connection URL | auto-generated |
REDIS_PASSWORD |
Redis password | (none) |
CACHE_VERSION |
Cache key prefix version | auto-computed from source hash |
CACHE_TTL_SECONDS |
Cache TTL (0 = no expiry) |
86400 (24h) |
CACHE_OPERATION_TIMEOUT_SECONDS |
Maximum wait for one cache operation | 5 |
Request metering
With METER_ENABLED=true (the default), non-probe HTTP responses carry
x-request-id and x-bibr-duration-ms. A valid client-supplied x-request-id
is echoed; otherwise the server generates one. Request and extraction records
go to the bibr.serve.metering logger; METER_LOG_PATH optionally adds
rotating JSONL files: the API process writes request records to METER_LOG_PATH
itself, while the worker writes extraction records (the only ones carrying
LLM token usage) to the sibling <stem>.worker<suffix> file. Each process
rotates only its own file, so usage tallies must read both files. Cache hits
do not count the original extraction's LLM tokens as new usage.
Error responses
| Status | Meaning |
|---|---|
400 |
Invalid input (missing filename, malformed/bounded option, duplicate multipart part, an unexpected file part, too many text parts) |
401 |
Missing or invalid bearer token (AUTH_API_KEY set) |
404 |
Unknown job id (expired past JOBS_TTL_SECONDS, evicted by the retention limits, or never existed) |
409 |
Job result or reference list requested before the job finished, or DELETE on a job that is running or finished |
410 |
Result or reference list of a cancelled job (error_code: job_cancelled) |
413 |
Upload limit exceeded (50 MiB file / 51 MiB multipart envelope) |
422 |
Extraction processing error, including an LLM response that was truncated at its token limit (error_code: llm_truncated) or failed validation (llm_invalid_output); retrying the same request fails the same way. A failure caused by an internal or library exception only names its stage and code (Processing failed in layout (layout_failed)); the exception text goes to the server log, not the response. When a service or model the pipeline needs was down (an OCR server that refused or dropped the connection, a layout model or LLM server that could not start), the detail also carries "outage": true and the message ends in : service temporarily unavailable: the same request may succeed later |
429 |
Upload admission or async-job active cap reached |
500 |
Unexpected internal error |
502 |
Upstream service failed (OCR server, LLM API); an LLM failure carries error_code llm_timeout or llm_failed |
503 |
/ready reports an unavailable dependency or required classifier artifact |
504 |
The request ran out of PIPELINE_TIMEOUT, counted from when the worker took it up: waiting for an in-flight slot or for an identical extraction counts too |
503 |
Job store unreachable (JOBS_STORE=redis): the upload was dropped and nothing queued — retry later |
507 |
Insufficient temporary storage for the disk-backed upload spool |