Extract
Turn documents into structured JSON with per-field citations, in one call.
POST /v1/extract takes one or more documents and a JSON Schema. It returns data shaped like your schema. Add "citations": true to ground every field. Each field then points at the page and region it came from, or is declared absent.
How it works
- You send a file (a URL or a file id you already have) and an output shape. The shape is an inline
schemaor a savedaction. - CloudRaker fetches the bytes, parses the document, and runs extraction against your schema.
- The call holds until the run finishes, up to
?wait=seconds (60 by default, 120 max). If the run finishes in time, you get200with the full result. If not, you get202with a run id to poll. This is not an error. - The run and its files expire on their own (
ttl, 24 hours by default). Nothing accumulates in your organization.
Quickstart
The sample below uses a blank IRS Form W-9 as a public, stable test document. The call works with no local files. Replace the URL with your own when you are ready.
If you prefer a business document over a blank government form, download the sample invoice. It is one fictional page with a vendor, a bill-to, four line items, and totals. Send it with a presigned upload, then extract against its file id.
The TypeScript and Python samples are plain HTTP, so they run with nothing installed. The same call is one line on either SDK as of 0.3.0: client.extract({ file, schema }) in TypeScript, client.extract(file=…, schema=…) in Python. That page shows this exact extraction end to end in both languages.
Example response
Key fields
output appears only after status is processed.
Guaranteed grounding
With citations enabled, every filled field comes back cited, declared absent, or marked unsupported. Nothing is left ambiguous. A field the extractor filled without evidence gets a deterministic second model pass, which either finds the citation or does not.
- Cited. The entry quotes the passage that backs the value.
- Declared absent. The documents do not contain it, so the value is
nulland the entry carriesnotFound: true. - Unsupported. The value was extracted but nothing in the source backs it. It is kept and the entry carries
uncited: true. Treat it as unverified: it is returned so you can see what was read and judge it, not because the document bears it out.
An unsupported value used to be deleted. Keeping it means a grounded run never returns less than an ungrounded one, and you can check the call yourself instead of having it made for you.
A field can carry several citations
A value assembled from more than one place in the source is cited at each of them — one entry per place, in citations[field]. A value read from a single place carries one entry. Each entry quotes a single continuous passage, and with judge: true each is scored on its own, so a confidence grades that quote, not the field.
An invoice’s terms are the everyday case: one field, two lines of small print in different places on the page.
A list field cites each element the same way, so an array of ten line items comes back with the evidence for each of them under one key.
Cite against a recording
A recording is cited the same way, with timecode — seconds from the start — in place of page and bbox. Seek there to hear the value said.
The timecode marks the transcript segment the quoted text falls in, so expect segment granularity rather than word-level precision. It is resolved from the transcript, never taken from the model.
That resolution can fail, and says so rather than guessing. A short quote is often ambiguous — “No.” may be said thirty times in one call — and such a citation is placed only when the run can tell which occurrence is meant. Otherwise it comes back with its text and no timecode, which means “not locatable”, never an error.
For a tighter offset than the segment, fetch the transcript with GET /v1/files/:id → urls.json and read words[] around the timecode. See Audio transcripts for the shape.
POST /v1/extract never inlines the transcript. Only GET /process/:id does, with include=results,content&format=json. On the verbs, plan for the extra file fetch.
Configuration
Every field below is optional unless noted.
processing on a file ref picks how the document is read: auto (default), ocr, simple, transcribe, or transcribe_diarize for audio.
One result over many documents
across_documents reads all the files of a run as one source and returns one record. The files are joined in the order you list them in files[], so list them in reading order. For a recording split into parts, list the parts in recording order. The model then extracts from the whole set at once, so a list field collects items from every file.
Citations still point to the original files. Each one names its fileId, and its page or timecodeStart counts from the start of that file, not from the joined source. In a diarized transcript, speakers are labeled per part (P2 SPEAKER_00), because the same label in two recordings is not always the same person. Known limitation: speakers are not matched across recordings. The model identifies each person from names, roles and context, so a field that depends on who spoke can be wrong when the parts give no such clues.
The joined record is the output.documents[] entry with fileId: "merged". Each file also has its own entry, with the same value and only the citations into that file.
A file that cannot be read is reported failed, and the other files are still read together. The joined source is read in windows of about 400,000 characters, four at a time. One run extracts all the files, so a set too large to finish in one run fails as a whole: split it into several runs. Audio mixed with documents cannot be joined: their citations use seconds and pages. Such a run extracts each file on its own and merges the results. Per field, a real value beats an empty one, and the best-cited value wins. A tie goes to the earliest file in request order.
Sync vs async
The endpoint is synchronous by default and degrades instead of failing.
When the run has not finished by the cap, you get 202 with a handle, never a timeout error:
Poll GET /v1/runs/:id (which also accepts ?wait=), or use a webhook. Send an idempotency-key header to make retries safe. A replay returns the original run and an idempotent-replay: true response header.
Large batches and scanned documents are the common causes of a 202. If you always want the handle, pass ?wait=0. Never block a request thread.
Schema inference
If you do not have a schema yet, omit both schema and action. CloudRaker infers a schema from the document, then extracts against it in the same call. Add hints (up to 2,000 characters) to steer what it looks for.
The finished run carries the schema it used at config.schema, alongside the output:
That response is trimmed. The real call on this document inferred 29 fields. The inferred schema is plain JSON Schema in the extraction dialect. Every field is nullable, and every field carries a description. You can send it back as schema with no editing.
config.schema is present whichever way the shape was decided: inferred, sent inline, or loaded from a saved config. One code path reads the applied shape.
Inference is for exploration, not production. The model picks the fields. Two runs over the same document can return different field names and a different field count. The two runs behind this page produced 32 and 29 fields, with form_number in one and form_type in the other. Nothing downstream of you can rely on that.
Use inference once to discover the shape, then pin it. Copy config.schema into your own request, or save it as an action and call that by name. Production callers must always send schema or action.
hints applies only to inference. Sending it alongside a schema or an action returns 400 invalid_request. Use instructions to guide an extraction whose shape you already fixed.
Save it as a config
Passing the same schema, instructions, and model on every call is repetitive. Save that configuration once in the extract config library. Then call {"file": …, "action": "invoice-lines"} and keep the request to two fields. Inline fields still win when you send them.
GET, PATCH and DELETE /v1/extract/configs/{idOrSlug} read, change and remove a config. GET /v1/extract/configs lists them. The flat /v1/actions routes still work as a deprecated alias over the same objects.
Citations are an add-on you enable per request or on the saved config. A saved config uses the setting’s internal name, grounding. It grounds its runs only when the config says "grounding": true, or when the request itself sends "citations": true.
The id and the slug are interchangeable wherever a config is referenced. Saved configs covers the catalog, the merge rules, and the benefits of the saved ramp. The same configurations are editable in the app under Actions.
Batch
POST /v1/extract/batch runs one saved config over many documents. It mints a separate run per file. Use it for a nightly backfill or a queue drain.
The response is always 202. Batches never run synchronously:
There is no batch object and no batch id. runs[] is the whole handle. Each entry is {id, statusUrl, file} for an accepted file. A file rejected at admission gets {file, error: {code, message}} instead. A bad URL in the list never sinks the rest.
Idempotency-Key is not honoured on a batch. One key cannot address N runs, so a retried batch fans out a second time. If a batch call fails ambiguously (a 429, a timeout), list its runs by shared metadata before you resend.
The body is strict. schema and hints are rejected with 400 invalid_request (“Unrecognized key”), not silently ignored. Save the schema as an action first. That is the point of the endpoint. A saved sign action passed as action is also a 400.
Track a batch
The batch costs one rate-limit token. Shared metadata is how you find its runs again:
That returns the three runs and their statuses in one call instead of three polls. See listing runs. For a per-run result, GET /v1/runs/:id stays authoritative. A webhook removes the polling entirely.