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
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
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.
A document the classifier cannot place is not guessed at
A document the classifier cannot place is not guessed at
“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.
The tier follows the work that was actually performed
The tier follows the work that was actually performed
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.
Every decision is recorded with its evidence
Every decision is recorded with its evidence
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.Listing
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.
