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, and returns data shaped like your schema. Add "citations": true and every field also comes back grounded — pointing at the page and region it came from, or explicitly declared absent.

How it works

  1. You send a file (a URL or a file id you already have) and an output shape — either 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 it finishes in time you get 200 with the full result. If it doesn’t, you get 202 with a run id to poll instead of an error.
  4. The run and its files expire on their own (ttl, 24 hours by default), so nothing accumulates in your organization.

Quickstart

The sample below uses a blank IRS Form W-9 as a public, stable test document, so the call works with no local files. Replace it with your own URL when you’re ready.

Want a business document rather than a blank government form? Download the sample invoice — one fictional page with a vendor, a bill-to, four line items, and totals — then send it with a presigned upload and 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 — and 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.
statusUrlPath to poll for this run.
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.citationsOnly present when the run was grounded — absent entirely otherwise, never an empty object. Map of field key (tax_classification; rows_per_document rows are prefixed with the row index, [0].amount) to the evidence behind it. Each entry carries fileId, the matched text, and confidence (05, higher is stronger grounding); documents also carry page (0-based) and bbox, a box normalized to the page as {x, y, width, height} in 01 with a top-left origin; audio carries a timecode instead of page and bbox. 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) — not the run status, and not files[].status, which tracks the document itself.

output appears only once status is processed.

Guaranteed grounding

With citations enabled, every filled field comes back either cited or declared absent — nothing is left ambiguous. Any field the extractor filled without evidence gets a deterministic second model pass that either 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.

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, and can carry optional field types that tell extraction a value is money, a date, or a phone number.
actionstringA saved action — its act_ id, its installed id, or its slug. Inline fields on the request are merged over the action’s 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 whatever the saved action specifies.
unitper_document | across_documents | rows_per_documentOne result per document (default), one result over the whole set, or a row array per document.
modelstringPin a specific model instead of the default for the capability.
judgebooleanAsk for a second verification pass that re-scores the extracted values. It runs only when a judge model is configured for the action; without one the extraction model’s own scores are kept, unaudited.
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, 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, and the best-grounded value wins: with citations on, that is the value with the strongest citation; on ties — including every tie when grounding is off — the earliest document in request order wins. Values are taken whole, together with their citations: arrays are not unioned across documents, so a list field comes from exactly one document.

Use it for related-but-independent documents that each contribute fields to one record — 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 uploading, so extraction 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 hasn’t 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 let a webhook tell you. 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 and never block a request thread.

Schema inference

Don’t have a schema yet? Omit both schema and action and CloudRaker infers one 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, every field carries a description. You can send it straight back as schema with no editing.

config.schema is present whichever way the shape was decided — inferred, sent inline, or loaded from a saved action — so one code path reads the applied shape.

Inference is for exploration, not production. The model picks the fields, so two runs over the same document can return different field names and a different number of them (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 it 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 should always send schema or action.

hints only applies to inference. Sending it alongside a schema or an action is a 400 invalid_request — use instructions to guide an extraction whose shape you already fixed.

Save as an action

Passing the same schema, instructions, and model on every call gets old. Saving that configuration once as an action lets you 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/actions \
> -H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "capability": "extract",
> "name": "Invoice lines",
> "config": { "schema": { "type": "object", "properties": { } }, "unit": "rows_per_document", "grounding": true }
> }'

Citations are an add-on you enable per request or on the saved action. A saved action’s config uses the setting’s internal name, grounding — it grounds its runs only when the config says "grounding": true (or the request itself sends "citations": true).

The id and the slug are interchangeable wherever an action is referenced. Saved actions covers the catalog, the merge rules, and what the saved ramp buys you; the same actions are editable in the app under Actions.

Batch

POST /v1/extract/batch runs one saved action over many documents, minting a separate run per file — the shape you want 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 either {id, statusUrl, file} for an accepted file or {file, error: {code, message}} for one rejected at admission, so 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 action. Off by default, like a single run.
metadataobjectApplied to every run in the batch — 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 resending.

The body is strict: schema and hints are rejected with 400 invalid_request (“Unrecognized key”) rather than 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, and 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, and a webhook removes the polling entirely.

Next steps