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

1{
2 "object": "extract_run",
3 "id": "exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
4 "status": "processed",
5 "expiresAt": "2026-07-26T15:57:41.907Z",
6 "statusUrl": "/v1/runs/exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
7 "files": [
8 { "id": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "name": "w9.pdf", "status": "processed" }
9 ],
10 "file": { "id": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "name": "w9.pdf", "status": "processed" },
11 "output": {
12 "value": {
13 "business_name": null,
14 "tax_classification": "Individual/sole proprietor or single-member LLC"
15 },
16 "citations": {
17 "tax_classification": [
18 {
19 "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678",
20 "page": 0,
21 "bbox": { "x": 0.086, "y": 0.379, "width": 0.261, "height": 0.016 },
22 "text": "Individual/sole proprietor or single-member LLC",
23 "confidence": 5
24 }
25 ],
26 "business_name": [
27 { "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "notFound": true }
28 ]
29 },
30 "documents": [
31 {
32 "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678",
33 "name": "w9.pdf",
34 "status": "done",
35 "value": {
36 "business_name": null,
37 "tax_classification": "Individual/sole proprietor or single-member LLC"
38 },
39 "citations": {
40 "tax_classification": [
41 {
42 "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678",
43 "page": 0,
44 "bbox": { "x": 0.086, "y": 0.379, "width": 0.261, "height": 0.016 },
45 "text": "Individual/sole proprietor or single-member LLC",
46 "confidence": 5
47 }
48 ],
49 "business_name": [
50 { "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "notFound": true }
51 ]
52 }
53 }
54 ]
55 }
56}

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). Each entry carries fileId, the matched text, and confidence (05, higher is stronger grounding). Documents also carry page (0-based) and bbox. The bbox is normalized to the page as {x, y, width, height} in 01 with a top-left origin. Audio citations carry no location at all — see Cite against a recording. Keys not applicable to a source are omitted. A {"fileId": …, "notFound": true} entry says the value is not in the documents. It carries nothing else, and the field’s value is null.
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 or declared absent. Nothing is left ambiguous. A field the extractor filled without evidence gets a deterministic second model pass. That pass finds the citation or declares the value not present. A field declared not present is returned as null with a notFound citation, never a guess.

Cite against a recording

Extraction over audio grounds each field in the transcript, not in the audio timeline. An audio citation names the fileId, the quoted text, and a confidence. It carries no page, no bbox, and no timecode.

The transcript supplies the timing. Fetch it, then match the citation against it:

1

Fetch the transcript

GET /v1/files/:id, then fetch urls.json. See Audio transcripts for the shape.

2

Match the citation text to a segment

Normalize both strings first. The extractor re-punctuates what it quotes, so compare on letters and digits only, in lower case.

3

Seek to that segment

The matching segment’s start is the position in seconds. Use words[] when you need a tighter offset.

Two details make the match reliable. Join every segment into one normalized string, so a quote that crosses a segment boundary still resolves to where it begins. Then, if the whole quote does not match, retry with its first five to eight words — the platform trims long quotes at the tail, rarely at the head.

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, or a row array per document.
judgebooleanRequest a second pass that re-scores the extracted values against the evidence they cite. 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 extracts each document independently. It then deterministically folds the per-document results into one record. It is not a joint pass over all files at once.

Per field, a real value always beats an empty one. The best-grounded value wins. With citations on, that is the value with the strongest citation. On ties, the earliest document in request order wins. With grounding off, every comparison is a tie.

Values are taken whole, together with their citations. Arrays are not unioned across documents. A list field comes from exactly one document.

Use it for related but independent documents that each contribute fields to one record. Examples: an application form plus a bank statement, a contract plus its amendment. It is not built for fragments of a single source. For a recording split into parts, concatenate the audio into one file before upload. Extraction then sees one continuous document.

Sync vs async

The endpoint is synchronous by default and degrades instead of failing.

?wait=Behavior
omittedHolds up to 60 seconds.
1120Holds 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:

1{
2 "object": "extract_run",
3 "id": "exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
4 "status": "processing",
5 "statusUrl": "/v1/runs/exr_01KYD1J8QW2RN4T6VXZ0ABCDEF"
6}

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:

1{
2 "object": "extract_run",
3 "id": "exr_01KYD1J8QW2RN4T6VXZ0ABCDEF",
4 "status": "processed",
5 "config": {
6 "schema": {
7 "type": "object",
8 "properties": {
9 "form_type": { "type": ["string", "null"], "description": "Form identifier (e.g., W-9)" },
10 "form_revision_date": { "type": ["string", "null"], "description": "Form revision date (e.g., March 2024)" },
11 "catalog_number": { "type": ["string", "null"], "description": "IRS catalog number for the form (e.g., 10231X)" },
12 "entity_name": { "type": ["string", "null"], "description": "Name of entity or individual (Line 1)" },
13 "tax_classification": { "type": ["string", "null"], "description": "Federal tax classification selected (Line 3a)" }
14 }
15 }
16 },
17 "output": {
18 "value": {
19 "form_type": "W-9",
20 "form_revision_date": "March 2024",
21 "catalog_number": "10231X",
22 "entity_name": null,
23 "tax_classification": null
24 },
25 "citations": {
26 "form_type": [
27 {
28 "fileId": "a0375090-2f78-4fc5-a016-cb29dc43f8ea",
29 "page": 0,
30 "bbox": { "x": 0.091, "y": 0.037, "width": 0.065, "height": 0.036 },
31 "text": "Form W-9",
32 "confidence": 5
33 }
34 ],
35 "entity_name": [{ "fileId": "a0375090-2f78-4fc5-a016-cb29dc43f8ea", "notFound": true }],
36 "tax_classification": [{ "fileId": "a0375090-2f78-4fc5-a016-cb29dc43f8ea", "notFound": true }]
37 }
38 }
39}

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:

1{
2 "object": "extract_batch",
3 "count": 3,
4 "runs": [
5 { "id": "exr_01KYDQEEK5FQT6CWENZZKEA2Z5", "statusUrl": "/v1/runs/exr_01KYDQEEK5FQT6CWENZZKEA2Z5", "file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" } },
6 { "id": "exr_01KYDQEEK5PMS75WBRJ962NA53", "statusUrl": "/v1/runs/exr_01KYDQEEK5PMS75WBRJ962NA53", "file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" } },
7 { "id": "exr_01KYDQEEK5413ASVYK21GJ375J", "statusUrl": "/v1/runs/exr_01KYDQEEK5413ASVYK21GJ375J", "file": { "url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf" } }
8 ]
9}

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