LibrariEval System
LeaderboardDocsPricingSign UpLogin
Librari Evals — Docs

Call the Extract API

Send a document to /api/extract from your application code and get back structured JSON keyed to a Request Schema Version you've already benchmarked.

You've built a Request Schema Version, benchmarked it with manual and scheduled runs, and picked a model deployment you trust. Now you want your application to actually use it. That's POST /api/extract — bearer-authenticated, multipart, returns structured JSON.

The same Schema Version your benchmarks ran against is the one the API uses. What you measured is what production gets — no separate "production schema" to drift.

What you'll need

  • A published Extraction API Key (the full key, copied at creation — only the last 4 characters are recoverable afterwards).
  • The UUID of a published Request Schema Version. Open the version in the admin; the ID is in the URL after /request-schema-versions/.
  • The UUID of a Model Deployment. Make sure a tenant LLM API key is assigned to it, or the call returns 400.
  • A document to extract from — PDF is the supported path; other formats work to the extent the model deployment accepts them.

The endpoint at a glance

MethodPOST
URLhttps://evals.thelibrari.com/api/extract (or your own host)
AuthAuthorization: Bearer <extraction-api-key>
Bodymultipart/form-data

Request fields

FieldRequiredTypeNotes
filefile partThe document to extract from. PDF is the primary supported type.
requestSchemaVersionIdstring (UUID)A Request Schema Version belonging to your tenant. Published is the typical pick; Draft works too if you're testing.
modelDeploymentIdstring (UUID)A deployment you can see in Supported LLM Models. Must have a tenant LLM API key assigned.

Response shape (200 OK)

{
  "success": true,
  "data": {
    "effective_date": "2024-03-01",
    "renewal_term_months": 12,
    "is_contract": true
  },
  "usage": {
    "inputTokens": 4821,
    "outputTokens": 312,
    "totalTokens": 5133,
    "cachedInputTokens": 0
  },
  "cost": {
    "inputCost": 0.01446,
    "outputCost": 0.00468,
    "totalCost": 0.01914,
    "inputPricePerMillion": 3.0,
    "outputPricePerMillion": 15.0
  },
  "durationMs": 4218
}

data is the structured extraction keyed by the field names you defined on the schema version. usage, cost, and durationMs are diagnostic — useful for client-side logging, surface metrics, or showing your users what an extraction cost and how long it took.

The cost object

cost breaks the spend for the call into its parts, all in US dollars, computed from the token counts in usage and the deployment's configured per-million-token prices.

FieldTypeNotes
inputCostnumberDollar cost of the input (prompt) tokens.
outputCostnumberDollar cost of the output (completion) tokens.
totalCostnumberinputCost + outputCost — the per-call spend.
inputPricePerMillionnumber | nullThe deployment's input price per million tokens used for the calculation. null if the deployment has no input price set.
outputPricePerMillionnumber | nullThe deployment's output price per million tokens. null if unset.

Walkthrough

  1. 1. Mint and stash a bearer token

    Create an Extraction API Key — see Manage Extraction API Keys. Copy the full key the moment it's shown; after the create screen, only the last 4 characters are stored.

    Stash it in your application's secret manager (env var, AWS Secrets Manager, Vault — whatever you already use). Never check it into source.

  2. 2. Grab the Schema Version and Model Deployment IDs

    Open the Request Schema Version you want production to use. The UUID is the last segment of the URL, after /admin/collections/request-schema-versions/.

    For the model deployment, open Browse Supported LLM Models. The deployment ID isn't shown directly in the table — open the underlying record from Model Deployments (Models → Model Deployments in the sidebar) to copy it, or pull it from a /api/model-deployments query if you're scripting setup.

  3. 3. POST multipart/form-data

    curl:

    curl -X POST https://evals.thelibrari.com/api/extract \
      -H "Authorization: Bearer $LIBRARI_EXTRACT_KEY" \
      -F "file=@/path/to/contract.pdf" \
      -F "requestSchemaVersionId=20448b1b-87d1-4e99-b521-80360b676035" \
      -F "modelDeploymentId=9f7e3c4d-b8a2-4e1f-8c6d-2a5b9c4d3e7f"

    Python (requests):

    import os, requests
    
    with open("/path/to/contract.pdf", "rb") as f:
        resp = requests.post(
            "https://evals.thelibrari.com/api/extract",
            headers={"Authorization": f"Bearer {os.environ['LIBRARI_EXTRACT_KEY']}"},
            files={"file": ("contract.pdf", f, "application/pdf")},
            data={
                "requestSchemaVersionId": "20448b1b-87d1-4e99-b521-80360b676035",
                "modelDeploymentId": "9f7e3c4d-b8a2-4e1f-8c6d-2a5b9c4d3e7f",
            },
            timeout=120,
        )
    resp.raise_for_status()
    extraction = resp.json()["data"]

    TypeScript / Node 20+ (fetch + FormData):

    import { readFile } from "node:fs/promises"
    
    const fileBuf = await readFile("/path/to/contract.pdf")
    const form = new FormData()
    form.append("file", new Blob([fileBuf], { type: "application/pdf" }), "contract.pdf")
    form.append("requestSchemaVersionId", "20448b1b-87d1-4e99-b521-80360b676035")
    form.append("modelDeploymentId", "9f7e3c4d-b8a2-4e1f-8c6d-2a5b9c4d3e7f")
    
    const res = await fetch("https://evals.thelibrari.com/api/extract", {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.LIBRARI_EXTRACT_KEY!}` },
      body: form,
    })
    if (!res.ok) throw new Error(`Extract failed: ${res.status} ${await res.text()}`)
    const { data } = await res.json() as { data: Record<string, unknown> }
  4. 4. Read the response

    Happy path:

    {
      "success": true,
      "data": { "...": "..." },
      "usage": { "inputTokens": 4821, "outputTokens": 312, "totalTokens": 5133, "cachedInputTokens": 0 },
      "cost": { "inputCost": 0.01446, "outputCost": 0.00468, "totalCost": 0.01914, "inputPricePerMillion": 3.0, "outputPricePerMillion": 15.0 },
      "durationMs": 4218
    }

    data is the only field most production code touches. cost.totalCost is per-call spend in US dollars — handy if you want to surface cost-per-extraction to your end users, or shed load when a customer is over quota. (If the deployment has no pricing configured, totalCost is 0 and the …PricePerMillion fields are null — check the price, not the cost, to detect that.)

Text-layer requirement

OCR and extraction are two separate calls. /api/extract never runs OCR — it only extracts. OCR (flattening a scan into a PDF with a selectable text layer) is its own endpoint: Apply OCR to a Document (POST /api/ocr).

If the Request Schema Version has Requires Text set and you upload a PDF, /api/extract first probes the PDF for a text layer:

  • Has a text layer (born-digital PDF, or already OCRd) → extraction proceeds normally.
  • No text layer (a raw scan), or the probe can't confirm one → the call is rejected with text_layer_required (HTTP 422) before the model is ever called. The model is never sent a scan it can't read.

When you get a text_layer_required back, the fix is a two-step flow:

  1. POST /api/ocr with the scan → you get back the same PDF with a text layer applied.
  2. POST /api/extract with the OCRd PDF → extraction proceeds.

One caveat: OCR can succeed and still find no text — a photo, a blank page, an illegible scan. The /api/ocr response tells you which case you're in via its X-Ocr-Has-Text-Layer header (it runs this same probe). If that header says false, resubmitting to /api/extract will just produce this 422 again — treat the document as unreadable instead.

This split lets you OCR once and reuse the flattened PDF across many extractions, and keeps OCR-service slowness or outages from being charged against — or confused with — your extraction calls. Schema versions that use Requires Vision (the model reads the page images directly) don't need a text layer and skip this check entirely.

How limit rejections are reported

"This document is too big" can be caught in two different places, and which one fires decides the shape you get back. Understanding the split is the key to handling size/page/format rejections cleanly.

Layer 1 — pre-flight, before the model is ever called. The API probes your file (page count, byte size, image dimensions and format) and checks it against the limits declared on the chosen Model Deployment. Any violation comes back as a single precheck_failed 400 that lists every failed limit at once. No provider round-trip, no token spend, fully deterministic. This only covers limits you've actually set on the deployment — a null limit is skipped.

Layer 2 — the provider's own rejection, echoed back. If a limit wasn't declared on the deployment (or can't be known until the model tokenises the input, like the context window), the document goes to the provider and the provider rejects it. The API catches that raw vendor error, parses it, and re-emits it in the same structured shape — translating each provider's idiosyncratic wording into a stable type (page_cap_exceeded, payload_too_large, context_overflow, images_per_request_exceeded) with typed numeric fields like pagesUsed / tokensLimit.

The practical consequence: the same physical limit can surface under two different type values. A PDF that's over the page cap returns precheck_failed if you set maxPages on the deployment, or page_cap_exceeded if you didn't and the provider caught it. Tell them apart by shape — precheck_failed carries a failures[] array and may list several problems together; the provider-echoed types are single-issue with their own numeric fields. Both are documented below.

Errors

The endpoint returns two different error shapes depending on where the failure happened.

Setup / auth errors — flat string

Returned for problems the API can detect before it ever calls the LLM provider: missing bearer token, missing required fields, wrong UUID, no provider key wired up. The body is { "error": "<message>" }.

HTTPerror bodyCauseFix
401UnauthorizedMissing/invalid bearer token, or the token doesn't match any active API key.Check the Authorization header. If you can't find the full key value, mint a new one in Manage Extraction API Keys and roll your app.
400Missing required fields: requestSchemaVersionId, modelDeploymentId, fileOne of the three required multipart parts is absent. Most often: sent as JSON instead of multipart, or file was attached as pdf / document / some other name.Use multipart/form-data. Field names are case-sensitive.
400No tenant LLM API key is configured for model deployment <id>...Your tenant doesn't have an LLM provider key wired up to this deployment.In the admin, open the right Tenant LLM API Key and add the deployment to its modelDeployments relation.
404Request schema version not found for this tenantWrong UUID, or the version belongs to a different tenant than your API key.Re-check the UUID from the admin URL. Don't try to call against another tenant's schemas.
404Model deployment not foundWrong UUID.Re-check from Supported LLM Models or the Model Deployments admin list.

Provider / extraction errors — structured object

Once the request reaches the LLM provider, any failure (rate limit, context overflow, bad credentials, timeout, …) comes back as a structured object so your client can branch on it without parsing prose:

{
  "error": {
    "type": "context_overflow",
    "status": 400,
    "retryable": false,
    "message": "Anthropic request failed (model=claude-haiku-4-5-20251001): 400 ... prompt is too long: 204004 tokens > 200000 maximum",
    "provider": "Anthropic",
    "providerStatus": 400,
    "tokensUsed": 204004,
    "tokensLimit": 200000
  }
}

The HTTP status matches error.status. Fields:

FieldAlways presentNotes
typeStable identifier (see table below). Branch on this, not on the human message.
statusHTTP status code; equals the response's status.
retryabletrue if retrying the same request with backoff might succeed.
messageThe raw underlying provider message, truncated to 1000 chars. Useful for logs; don't show verbatim to end users.
provideroptionalInferred provider name when the wrapper made it identifiable: Anthropic, OpenAI, AWS, Google, xAI. Azure OpenAI errors don't carry a prefix and won't have this set.
providerStatusoptionalThe upstream provider's own HTTP status, when we could parse it out.
tokensUsed / tokensLimitoptionalSet on context_overflow when the provider reported both.
pagesUsed / pagesLimitoptionalSet on page_cap_exceeded (PDF page-count cap).
imagesUsed / imagesLimitoptionalSet on images_per_request_exceeded (Azure OpenAI image-block cap).

Pre-flight limit validation (type: "precheck_failed")

Before the document is sent to the LLM, the API checks it against every limit declared on the chosen Model Deployment. If anything is out of bounds, you get a precheck_failed response with every failed limit listed in one shot — no need to fix-and-retry one at a time:

{
  "error": {
    "type": "precheck_failed",
    "status": 400,
    "retryable": false,
    "message": "Document violates 2 model-deployment limit(s); see failures for details.",
    "failures": [
      {
        "limit": "maxPages",
        "observed": 250,
        "allowed": 100,
        "unit": "pages",
        "message": "PDF has 250 pages; this deployment caps PDF input at 100 pages per request."
      },
      {
        "limit": "maxRequestPayloadMb",
        "observed": 38.4,
        "allowed": 32,
        "unit": "MB",
        "message": "Uploaded file is 38.4 MB; this deployment caps a single request payload at 32 MB."
      }
    ]
  }
}

Each entry in failures has the same shape:

FieldNotes
limitThe deployment field path. E.g., maxPages, maxRequestPayloadMb, imageLimits.supportedImageFormats. Stable — use this to branch in code.
observedWhat your document actually presented. Number for sizes/dimensions, string for formats.
allowedWhat the deployment allows. Number for caps, array of strings for enum-style limits (supported formats).
unitHuman display unit: pages, MB, px, format.
messagePlain-English summary suitable to show end users or log.

Limits enforced before the LLM call:

Deployment limitApplies toNotes
maxPagesPDFsPage count from a fast probe of the uploaded PDF.
maxRequestPayloadMbAllRaw file size — the cap on the whole request body.
maxCombinedDocumentsMbAllSame as file size for this single-doc endpoint.
imageLimits.supportedImageFormatsImagesRejects any MIME the deployment didn't list.
imageLimits.maxPerImageMbImagesFile size for an image upload.
imageLimits.maxImageDimensionPxImagesThe longer side of width/height — read from the image header.

Not pre-flight-checked (relies on the provider's own error to surface): contextWindowTokens, maxTokens, and the multi-doc variants of maxImagesPerRequest / maxDocumentsPerRequest (this endpoint only takes one file). If the document slips past pre-flight but blows the model's context window, you'll get the normal context_overflow error instead.

If the deployment has a limit set to null, that limit is skipped — pre-flight only enforces what the catalog explicitly declares.

Error types

typeHTTPretryableWhat it meansTypical fix
extract_at_capacity503yesThis replica is already handling its maximum number of concurrent extractions and fast-failed rather than queue yours. Fires only during a burst, before scale-out catches up.Back off and retry with jitter — see Rate limits and concurrency. It clears within seconds once a replica frees up or a new one starts.
rate_limit429yesProvider throttled this tenant/key.Back off and retry; consider a smaller batch size or a different deployment.
context_overflow400noDocument + prompt exceeded the model's context window. tokensUsed / tokensLimit tell you by how much.Switch to a larger-context deployment, trim the document, or split the extraction.
payload_too_large413noRequest body or attached file exceeded the transport cap.Compress or split the PDF before resending.
page_cap_exceeded400noThe model has a per-request PDF page cap (e.g., Anthropic 100 pages, Vertex 1000).Split the PDF into chunks, or pick a deployment with a higher cap.
images_per_request_exceeded400noAzure OpenAI image-block cap (the model rasterises PDF pages to images).Reduce page count or use a deployment that accepts the document differently.
invalid_request400noA provider 4xx we couldn't classify further.Read message — usually a malformed schema, unsupported MIME type, or content-policy block.
auth502noProvider rejected our credentials — the key was revoked, rotated, lacks permission, or points at the wrong endpoint.Update the Tenant LLM API Key. Reported as 502 because the caller's request was fine; the cloud config is broken.
no_structured_output422noThe model answered but returned nothing matching your schema — typically a refusal, a content-policy block, or a truncated/empty response. (A field the model simply couldn't find is returned as null, not an error.)Usually a content-policy refusal or a model poorly suited to the document. Read message; try a different Model Deployment.
document_render_failed422noThe PDF reached the service but a page could not be rasterized for a vision model (corrupt or unreadable page). The model is never called.The document is malformed for image rendering. Re-export / repair the PDF, or run it through POST /api/ocr to reflatten it, then resubmit.
text_layer_required422noThe schema has Requires Text but the uploaded PDF has no extractable text layer (a raw scan), or the text-layer probe couldn't confirm one. The model is never called. See the note below: this shape omits provider.OCR the document first via POST /api/ocr, then resubmit the OCRd PDF. A born-digital PDF that already has a text layer passes straight through.
model_unavailable502noModel isn't enabled in the upstream account, or has been deprecated.Pick a different Model Deployment.
insufficient_credits502noProvider's billing/quota check failed.Top up the upstream account.
timeout504yesUpstream took too long to respond.Retry with backoff; consider a faster deployment for large documents.
upstream_unavailable502yesProvider returned 5xx or the transport failed.Retry with backoff.
precheck_failed400noThe document violated one or more limits declared on the chosen Model Deployment — caught before the LLM was called.Read the failures array; each entry names the violated limit, the observed value, and what the deployment allows. Fix the document (split a long PDF, downscale an image, etc.) or pick a deployment with looser limits.
unknown500noThe error didn't match any known pattern.message has the raw text; please report it so we can add a matcher.

A minimal client-side retry rule — branch on error.retryable, never on the prose message:

const res = await fetch(extractUrl, { method: "POST", headers, body: form })
if (res.ok) return res.json()

const { error } = await res.json() as { error: { type: string; retryable: boolean; message: string } }
if (error.retryable) {
  // back off (exponential WITH FULL JITTER), then retry the same request
} else {
  // surface error.type to your monitoring; do NOT auto-retry
}

The retryable types are extract_at_capacity (503), rate_limit (429), timeout (504), and upstream_unavailable (502). Everything else is terminal — retrying sends the same document into the same wall.

Versioning, rollouts, and rollbacks

The Schema Version UUID is what pins your extraction to a specific (fields × base prompt × per-field prompts) tuple. When you want to ship a change:

  1. Build the new Schema Version under the same Schema Type. The version number auto-increments — v2, then v3.
  2. Benchmark v2 side-by-side with v1 using Run a Manual Test against the same Test Set.
  3. When v2 clears your bar, change the requestSchemaVersionId your app sends. Old v1 calls (in-flight when you flip the config) still extract against v1 — there's no hard cutover.
  4. Rollback is the same flip in reverse. v1 stays Published indefinitely.

Same applies to model swaps via modelDeploymentId. You don't need to redeploy your application to switch models — that's the whole point of keeping the IDs as configuration.

Rate limits and concurrency

The service runs on an autoscaling pool of replicas, each of which accepts a bounded number of concurrent extractions. Two distinct limits can throttle you:

  • Service concurrency (extract_at_capacity, 503). When a replica is already at its in-flight ceiling, it fast-fails immediately with a retryable 503 rather than queueing your request and risking a memory blowout. The pool scales out under load, but a new replica takes roughly 30–70 seconds to come online — so a simultaneous burst can see a few 503s during that window. They clear on their own; the fix is client-side retry.
  • Provider rate limits (rate_limit, 429). Beyond the service, your own BYOK provider key has its own throttle, surfaced as a retryable 429.

Both are retryable and both want the same handling: exponential backoff with full jitter.

Recommended policy (identical to the OCR client — see Apply OCR to a Document):

ParameterValue
Retry onextract_at_capacity (503), rate_limit (429), timeout (504), upstream_unavailable (502), and transport errors
Max attempts5 (1 initial + 4 retries)
Backoffbase 4s, ×2 (4 → 8 → 16 → 32s), capped at 30s, full jitter
Retry deadline~120s for transient errors
Per-request timeout~150s (an extraction can run a couple of minutes)