Skip to navigation
Extract

Extract

Turn documents into structured JSON with per-field citations, in one call.

View as Markdown

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

  1. You send a file (a URL or a file id you already have) and an output shape. The shape is an inline schema or a saved action.
  2. CloudRaker fetches the bytes, parses the document, and runs extraction against your schema.
  3. The call holds until the run finishes, up to ?wait= seconds (60 by default, 120 max). If the run finishes in time, you get 200 with the full result. If not, you get 202 with a run id to poll. This is not an error.
  4. 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.

curl -X POST https://api.cloudraker.com/v1/extract \
-H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf", "name": "w9.pdf" },
"citations": true,
"schema": {
"type": "object",
"properties": {
"business_name": { "type": ["string", "null"] },
"tax_classification": { "type": ["string", "null"] }
}
}
}'

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

{
"object": "extract_run",
"id": "exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
"status": "processed",
"expiresAt": "2026-07-26T15:57:41.907Z",
"statusUrl": "/v1/runs/exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
"files": [
{ "id": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "name": "w9.pdf", "status": "processed" }
],
"file": { "id": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "name": "w9.pdf", "status": "processed" },
"output": {
"value": {
"business_name": null,
"tax_classification": "Individual/sole proprietor or single-member LLC"
},
"citations": {
"tax_classification": [
{
"fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678",
"page": 0,
"bbox": { "x": 0.086, "y": 0.379, "width": 0.261, "height": 0.016 },
"text": "Individual/sole proprietor or single-member LLC"
}
],
"business_name": [
{ "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "notFound": true }
]
},
"documents": [
{
"fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678",
"name": "w9.pdf",
"status": "done",
"value": {
"business_name": null,
"tax_classification": "Individual/sole proprietor or single-member LLC"
},
"citations": {
"tax_classification": [
{
"fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678",
"page": 0,
"bbox": { "x": 0.086, "y": 0.379, "width": 0.261, "height": 0.016 },
"text": "Individual/sole proprietor or single-member LLC"
}
],
"business_name": [
{ "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "notFound": true }
]
}
}
]
}
}

Key fields

FieldWhat it is
objectAlways extract_run.
idThe run id (exr_…). Use it with GET /v1/runs/:id.
statusqueued, processing, processed, failed, cancelled, expired, or needs_input.
expiresAtWhen the run and its files are purged. Controlled by ttl.
files[]One entry per input file, with its own status and error. file is an alias for files[0] on single-file runs.
output.valueThe extracted data for a single-document run. An alias of documents[0].value.
output.citationsPresent only when the run was grounded. Absent otherwise, never an empty object. Maps each field key to the evidence behind it (tax_classification; rows_per_document rows carry the row index prefix, [0].amount). It is an array, because a value drawn from several places is cited at each of them, so read them all rather than the first. Each entry carries fileId and the matched text. Add judge: true for a confidence (0–5, higher is stronger grounding) on each cited entry. Grounding quotes the source and only the judge grades the quote, so an unjudged run carries no confidence at all. Documents also carry page (0-based) and bbox. The bbox is normalized to the page as {x, y, width, height} in 0–1 with a top-left origin. Audio carries timecode instead: seconds from the start of the recording, absent when the quote cannot be located unambiguously. See Cite against a recording. Keys not applicable to a source are omitted. Two kinds of entry carry no quote and no location, only the fileId and their marker. A {"fileId": …, "notFound": true} entry says the value is not in the documents, and the field’s value is null. A {"fileId": …, "uncited": true} entry says the field IS filled but nothing in the source backs it, so treat that value as unverified.
output.citationsOmittedtrue when the citations were dropped because the result exceeded the size budget. Output-level only. It never appears on a document.
output.documents[]Always present. One entry per document, so multi-file runs have a stable shape. Its per-document status is the extraction’s own (done, or failed with an error). It is not the run status. It is also not files[].status, which tracks the document itself.

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 null and the entry carries notFound: 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.

"citations": {
"notes": [
{ "fileId": "fil_…", "page": 0, "bbox": { "x": 0.083, "y": 0.834, "width": 0.515, "height": 0.011 },
"text": "Late payments accrue 1.5 % per month. Returns accepted within 15 days…" },
{ "fileId": "fil_…", "page": 0, "bbox": { "x": 0.083, "y": 0.849, "width": 0.573, "height": 0.011 },
"text": "Discrepancies must be reported within 5 business days of delivery…" }
]
}

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.

"citations": {
"chief_complaint": [
{ "fileId": "fil_…", "timecode": 4.25, "text": "chest pain since last night" }
]
}

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.

FieldTypeWhat it does
file{url, name?, processing?} or {id}One document. Exactly one of file or files is required.
files[]array of the same unionUp to 100 documents in one run.
schemaobjectThe output shape. Exactly one of schema or action is required. Must follow the extraction schema dialect. Can carry optional field types that mark a value as money, a date, or a phone number.
actionstringA saved config, by its act_ id, its installed id, or its slug. Inline fields on the request are merged over the saved configuration.
instructionsstringFree-text guidance applied on top of the schema (“amounts are in EUR”, “ignore the cover letter”).
citationsbooleanGrounding is off by default. Set true to get a citation for every field. Omit it to keep the saved config’s setting.
unitper_document | across_documents | rows_per_documentOne result per document (default), one result over the whole set read as one source, or a row array per document. See One result over many documents.
judgebooleanRequest a second pass that scores the extracted values against the evidence they cite, adding a confidence to each cited entry and leaving the values alone. It is the only pass that scores, so without it citations carry no confidence. If the judge fails, some or all entries stay unscored. The judge runs one pass per text window, or one pass per row with rows_per_document. Each pass scores at most 100 cited entries, so a window or row with more cited entries leaves the entries of its last fields unscored. A missing confidence means not graded, never badly graded. It needs citations: true: the judge audits citations, so with grounding off it has nothing to read and changes nothing.
metadataobjectYour own key/values, echoed back on the run body (not on webhook deliveries). Max 10 KB serialized.
webhook{url} or {id}Where to deliver the terminal event instead of polling. See Webhooks.
ttlinteger seconds, 1–604800How long the run and its files live. Default 24 hours, max 7 days.

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.

?wait=Behavior
omittedHolds up to 60 seconds.
1–120Holds up to that many seconds.
0Returns immediately with 202.

When the run has not finished by the cap, you get 202 with a handle, never a timeout error:

{
"object": "extract_run",
"id": "exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
"status": "processing",
"statusUrl": "/v1/runs/exr_01KYD1J8QW2RN4T6VXZ0ABCDEF"
}

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.

curl -X POST https://api.cloudraker.com/v1/extract \
-H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf", "name": "w9.pdf" },
"citations": true,
"hints": "This is a tax form; capture its identity"
}'

The finished run carries the schema it used at config.schema, alongside the output:

{
"object": "extract_run",
"id": "exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
"status": "processed",
"config": {
"schema": {
"type": "object",
"properties": {
"form_type": { "type": ["string", "null"], "description": "Form identifier (e.g., W-9)" },
"form_revision_date": { "type": ["string", "null"], "description": "Form revision date (e.g., March 2024)" },
"catalog_number": { "type": ["string", "null"], "description": "IRS catalog number for the form (e.g., 10231X)" },
"entity_name": { "type": ["string", "null"], "description": "Name of entity or individual (Line 1)" },
"tax_classification": { "type": ["string", "null"], "description": "Federal tax classification selected (Line 3a)" }
}
}
},
"output": {
"value": {
"form_type": "W-9",
"form_revision_date": "March 2024",
"catalog_number": "10231X",
"entity_name": null,
"tax_classification": null
},
"citations": {
"form_type": [
{
"fileId": "a0375090-2f78-4fc5-a016-cb29dc43f8ea",
"page": 0,
"bbox": { "x": 0.091, "y": 0.037, "width": 0.065, "height": 0.036 },
"text": "Form W-9",
"confidence": 5
}
],
"entity_name": [{ "fileId": "a0375090-2f78-4fc5-a016-cb29dc43f8ea", "notFound": true }],
"tax_classification": [{ "fileId": "a0375090-2f78-4fc5-a016-cb29dc43f8ea", "notFound": true }]
}
}
}

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.

curl -X POST https://api.cloudraker.com/v1/extract/configs \
-H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Invoice lines",
"config": { "schema": { "type": "object", "properties": { } }, "unit": "rows_per_document", "grounding": true }
}'

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.

curl -X POST https://api.cloudraker.com/v1/extract/batch \
-H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "w9-identity",
"files": [
{ "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" },
{ "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" },
{ "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" }
],
"metadata": { "job": "nightly-backfill" }
}'

The response is always 202. Batches never run synchronously:

{
"object": "extract_batch",
"count": 3,
"runs": [
{ "id": "exr_01KYDQEEK5FQT6CWENZZKEA2Z5", "statusUrl": "/v1/runs/exr_01KYDQEEK5FQT6CWENZZKEA2Z5", "file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" } },
{ "id": "exr_01KYDQEEK5PMS75WBRJ962NA53", "statusUrl": "/v1/runs/exr_01KYDQEEK5PMS75WBRJ962NA53", "file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" } },
{ "id": "exr_01KYDQEEK5413ASVYK21GJ375J", "statusUrl": "/v1/runs/exr_01KYDQEEK5413ASVYK21GJ375J", "file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" } }
]
}

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.

FieldTypeRules
actionstringRequired. A saved extract action, by id or slug. A batch has no inline arm.
files[]array of file refs1–100 entries. The same {url, name?, processing?} or {id} union as a single run.
citationsbooleanSet true to ground every run in the batch, overriding the saved config. Off by default, like a single run.
metadataobjectApplied to every run in the batch. This is the handle you filter on later.
webhook{url} or {id}Delivered per run, not once per batch.
ttlinteger secondsApplied to every run.

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:

curl -G "https://api.cloudraker.com/v1/runs" \
-H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
--data-urlencode "object=extract_run" \
--data-urlencode "metadata.job=nightly-backfill"

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.

Next steps