Skip to main content
An extraction takes a stored document and a schema, and returns fields that validated against that schema, each traceable back to the words in the document it came from.
Needs extractions:create. Long work, so it answers 202 with a job_id — see Jobs.

The schema

The same closed subset as structured outputs, enforced by the same validator. There is no second one: two validators would agree today and disagree in a year, and the one that disagrees about a payment amount is the one that matters. Refused with a 400 naming the keyword: $ref, pattern, oneOf/anyOf/ allOf/not, if/then/else, const, default. Bounds: 16 KB, 6 levels deep, 200 nodes, 100 properties per object, 200 enum values.

The result

data is null until the extraction completes. The schema is echoed back verbatim so a caller comparing two results can see which shape each was asked for.

Evidence is checkable, not decorative

char_start and char_end are offsets into the same string GET /documents/{id}/representation returns. Slice it and see the words for yourself. That is the point of publishing them: evidence a caller cannot check is a claim. match says how the quote was found. normalized means the words are the document’s but the whitespace or case is not — which you need to know if you are rendering a highlight. page is null rather than 0 when the parser reported no layout covering that offset, because 0 reads as “page zero”. evidence_coverage is how much of the answer is actually traceable, as a number you can threshold. A caller who has to count an evidence list to find that out will not.

Pruning and warnings

additionalProperties defaults to false and prunes: keys the model volunteered that you did not ask for are removed before validation, and the removal is reported in warnings. A model volunteering an extra field is the commonest form of plausible-looking noise, and neither silently returning it nor failing the whole extraction over it is the right answer.

Failure names paths, never values

You get the paths and the reasons. You do not get what the fields contained.
A required field that came back null counts as missing, not present. {"invoice_number": null} is not an answer, it is the absence of one wearing a key. Write "type": ["string", "null"] if you genuinely want “present, possibly null”.

Extracting does not publish

That field is in the resource on purpose. Extracting a salary from a CV does not make that salary something an agent will quote to the next person who asks, and you should not have to infer that from the absence of a field. Publication is a separate, deliberate act — knowledge ingestion.

Regular and premium

Extraction has two tiers, and the tier is decided from the document itself, never from anything you send. Not a field, not a header, not a filename — a caller who picks their own tier picks their own price. The filename is used only to look up a format, and even that is overridden by the file’s own magic bytes: a scanned PDF uploaded as notes.txt is probed as a PDF, because the alternative is a rename that changes the bill. metadata is never read for this at all.
“Unsure” is not a tier — it is an instruction to go and find out. The document goes through the regular path first and the outcome is the tiebreaker: readable text means regular, a failure to find readable text means premium.The platform never bills premium on suspicion, and never bills regular on hope.
Premium is recorded when a premium extraction actually ran. Where premium was warranted but could not run — no engine configured, a format the premium path cannot render — the document is recorded as regular, with a reason saying so. Charging the premium rate for work nobody did is the worse error of the two.
Each tier decision carries the measurements it was made from, the thresholds in force at the time, and a version. A disputed line is answerable with “here is what we measured on your file”, and a later threshold change does not silently rewrite why an old document was charged what it was charged.Reason codes are coarse where the evidence is coarse. no_text_layer does not claim to know whether a scan is typed or handwritten, because at that point nothing has looked at the pixels.
Rates are shared during evaluation and are not published here.

Listing

Needs extractions:read. Returns next_cursor, read back as cursor. A listing carries neither the echoed schema nor the evidence. Both are unbounded per row, and a fifty-row page carrying every quote from fifty documents — or fifty copies of a 16 KB schema — is a response nobody wanted. The detail endpoint is where the whole resource lives.

Idempotency

POST /extractions sets Idempotent-Replay: true on a replay — not Idempotency-Replayed, which is what the runtime endpoints use. See Idempotency.