> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.cloudraker.com/paperwork/capabilities/extract/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.cloudraker.com/_mcp/server. # Extract ## Docs - [Extraction schema dialect](https://docs.cloudraker.com/paperwork/capabilities/extract/schema.md): The subset of JSON Schema that `POST /v1/extract` accepts, and the meaning of each rejection. - [Field types](https://docs.cloudraker.com/paperwork/capabilities/extract/field-types.md): Optional annotations that tell extraction what a field means, such as a currency, a phone number, or a date, without a change to the response shape. > **Note:** This page contains both a page directory (above) and the landing page content (below). The page directory is generated for agent use and does not appear on the landing page. > For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.cloudraker.com/paperwork/capabilities/extract/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.cloudraker.com/_mcp/server. # Extract `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`](#save-as-an-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](/paperwork/developers/files#presigned-upload), then extract against its file id. **`curl`** ```bash title="curl" 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"] } } } }' ``` **`TypeScript`** ```ts title="TypeScript" const res = await fetch("https://api.cloudraker.com/v1/extract", { method: "POST", headers: { Authorization: `Bearer ${process.env.CLOUDRAKER_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ 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"] }, }, }, }), }); const run = await res.json(); console.log(run.status, run.output?.value); ``` **`Python`** ```python title="Python" import os, requests res = requests.post( "https://api.cloudraker.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['CLOUDRAKER_API_KEY']}"}, json={ "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"]}, }, }, }, ) run = res.json() print(run["status"], run.get("output", {}).get("value")) ``` > **Note** > > The TypeScript and Python samples are plain HTTP, so they run with nothing installed. The same call is one line on either [SDK](/paperwork/developers/sdks) 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 ```json { "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", "confidence": 5 } ], "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", "confidence": 5 } ], "business_name": [ { "fileId": "b88ea8f9-20d4-4704-b379-ddee5a23c678", "notFound": true } ] } } ] } } ``` ## Key fields | Field | What it is | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `object` | Always `extract_run`. | | `id` | The run id (`exr_…`). Use it with [`GET /v1/runs/:id`](/paperwork/developers/quickstart#track-a-run). | | `status` | `queued`, `processing`, `processed`, `failed`, `cancelled`, `expired`, or `needs_input`. | | `expiresAt` | When 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.value` | The extracted data for a single-document run. An alias of `documents[0].value`. | | `output.citations` | **Present 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`) — 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`, the matched `text`, and `confidence` (`0`–`5`, 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 `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](#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.citationsOmitted` | `true` 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. ### 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 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. ```jsonc "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…", "confidence": 5 }, { "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…", "confidence": 5 } ] } ``` 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. ```jsonc "citations": { "chief_complaint": [ { "fileId": "fil_…", "timecode": 4.25, "text": "chest pain since last night", "confidence": 5 } ] } ``` 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 `confidence` 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](/paperwork/developers/files#audio-transcripts) for the shape. > **Note** > > `POST /v1/extract` never inlines the transcript. Only [`GET /process/:id`](/paperwork/developers/process-api#inlined-content) does, with `include=results,content&format=json`. On the verbs, plan for the extra file fetch. ## Configuration Every field below is optional unless noted. | Field | Type | What it does | | -------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `file` | `{url, name?, processing?}` or `{id}` | One document. Exactly one of `file` or `files` is required. | | `files[]` | array of the same union | Up to 100 documents in one run. | | `schema` | object | The output shape. Exactly one of `schema` or `action` is required. Must follow the [extraction schema dialect](/paperwork/capabilities/extract/schema). Can carry optional [field types](/paperwork/capabilities/extract/field-types) that mark a value as money, a date, or a phone number. | | `action` | string | A saved config, by its `act_` id, its installed id, or its slug. Inline fields on the request are merged over the saved configuration. | | `instructions` | string | Free-text guidance applied on top of the schema ("amounts are in EUR", "ignore the cover letter"). | | `citations` | boolean | Grounding is **off by default**. Set `true` to get a citation for every field. Omit it to keep the saved config's setting. | | `unit` | `per_document` \| `across_documents` \| `rows_per_document` | One result per document (default), one result over the whole set, or a row array per document. | | `judge` | boolean | Request 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. | | `metadata` | object | Your 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](/paperwork/developers/webhooks). | | `ttl` | integer seconds, 1–604800 | How 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 | | --------- | ------------------------------- | | omitted | Holds up to 60 seconds. | | `1`–`120` | Holds up to that many seconds. | | `0` | Returns immediately with `202`. | When the run has not finished by the cap, you get `202` with a handle, never a timeout error: ```json { "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. > **Tip** > > 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. ```bash 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`: ```json { "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](/paperwork/capabilities/extract/schema). 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. > **Warning** > > **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`](#configuration) 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. ```bash 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](/paperwork/capabilities/actions) covers the catalog, the merge rules, and the benefits of the saved ramp. The same configurations are editable in the app under [Actions](/workspace/actions/overview). ## 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. ```bash 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: ```json { "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. | Field | Type | Rules | | ----------- | ------------------ | ------------------------------------------------------------------------------------------------------------ | | `action` | string | **Required.** A saved extract action, by id or slug. A batch has no inline arm. | | `files[]` | array of file refs | 1–100 entries. The same `{url, name?, processing?}` or `{id}` union as a single run. | | `citations` | boolean | Set `true` to ground every run in the batch, overriding the saved config. Off by default, like a single run. | | `metadata` | object | Applied 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. | | `ttl` | integer seconds | Applied 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](/paperwork/capabilities/sign) action passed as `action` is also a `400`. ### Track a batch The batch costs one [rate-limit](/paperwork/developers/rate-limits) token. Shared `metadata` is how you find its runs again: ```bash 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](/paperwork/developers/runs#list-runs). For a per-run result, `GET /v1/runs/:id` stays authoritative. A `webhook` removes the polling entirely. ## Next steps #### [Extraction schema dialect](/capabilities/extract/schema) The five rules your `schema` must satisfy, and the meaning of each rejection. #### [Field types](/capabilities/extract/field-types) Mark a field as currency, a date, or a phone number for better reads. #### [Parse](/capabilities/parse) Get clean markdown and structured JSON without a schema. #### [Saved configs](/capabilities/actions) Save a schema once and reference it by id or slug. #### [Runs](/developers/runs) Statuses, TTL, downloading outputs, and keeping a result.