# Extraction Extraction reads a document that DocAI has already parsed and returns JSON shaped to a schema you define, plus a citation for each value pointing at the segment it was read from. **Before you start.** Extraction needs a file that has already been parsed. Run a parse first — [parsing](/parsing) covers the dispatch, the poll and the artifacts it writes. There are two entry points, and they are not interchangeable. Read [which one to use](#which-one-to-use) before you write any code. ```bash export DOCAI_API_KEY="docai_..." export DOCAI_API="https://api.providus.ai" ``` ## Parse first Extraction never touches the original PDF. It runs over the [parse artifacts](/parsing#what-a-parse-produces), so a file with no parse artifact cannot be extracted, and the failure is a `409` before any model work happens. The two entry points check different things and return different codes: | Endpoint | Checks | Code | |---|---|---| | [`POST /v1/files/{fileId}/extract`](/api/file-processing#queue-extract-job-for-file-through-the-control-plane) | a parse manifest exists **for the current file version** | `file_must_be_parsed_before_extraction` | | [`POST /v1/extract/runs`](/api/document-extraction#enqueue-a-product-extraction-run) | any parse manifest exists for the file, its artifact is readable, and the segment selection is non-empty | `file_must_be_parsed` | ```json { "error": "file_must_be_parsed", "code": "file_must_be_parsed" } ``` The version scoping on the first row matters in practice: replacing a file's content with [`PUT /v1/files/{fileId}/content`](/api/files#upload-new-file-version) creates a new version, and that version has no parse artifact until you parse it again. The old version's artifact does not satisfy the check. Both codes are on the [error codes](/errors) page. `POST /v1/extract/runs` also returns `file_must_be_parsed` when you pass `segment_ids` that match nothing in the parse output — an empty evidence selection is treated the same as no parse at all, rather than extracting from nothing. ## Which one to use | | `POST /v1/extract/runs` | `POST /v1/files/{fileId}/extract` | |---|---|---| | Runs as | a queued **operation** | a queued **job** | | Poll at | [`GET /v1/operations/{operationId}`](/api/operations#get-queued-operation-status-progress-and-output) | [`GET /v1/files/{fileId}/jobs/{jobId}`](/api/file-processing#fetch-parse-extract-job-status-for-a-file) | | Schema format | DocAI field list (`fields`) or a saved `schema_id` | JSON Schema | | Result | `output` inline in the poll response | an artifact you fetch separately | | Result stored | on the operation row | `processing.extraction_results` plus an `extract` artifact manifest | | Validation | none | JSON Schema validation, reported in `warnings` | | Credits | 1 credit per run | none in v1 | | Scoping | `segment_ids` selects part of the document | `options.scope` | Use **`/v1/extract/runs`** when you are building an application flow: it takes the same schema objects the schema endpoints produce, the result arrives inline when you poll, and there is nothing else to fetch. Use **`/v1/files/{fileId}/extract`** when you want the result kept as a durable artifact against the file version, or when you want your JSON Schema enforced — this is the only path that validates the model's output and reports the errors. It is also the path bulk extraction uses. The job and operation split is not specific to extraction — [workflows](/workflows) explains it once for split, classify and the rest. Both require the `extract.run` permission and viewer access to the file. An API key bypasses both checks, as it does everywhere else. ## Schemas A DocAI extraction schema is a flat-or-nested list of fields: ```json { "version": 1, "title": "Invoice", "description": "Header and line items", "fields": [ { "name": "invoice_number", "type": "string", "description": "Document number", "required": true }, { "name": "issue_date", "type": "date", "description": "Date issued", "required": true }, { "name": "total_amount", "type": "number", "description": "Grand total", "required": true }, { "name": "line_items", "type": "table", "description": "Billed lines", "required": false, "children": [ { "name": "description", "type": "string", "description": "Line description", "required": false }, { "name": "amount", "type": "number", "description": "Line amount", "required": false } ] } ] } ``` `type` is one of `string`, `number`, `boolean`, `date`, `object`, `array`, `table`. Anything else is coerced to `string`. Field names are normalised: lowercased, with every run of characters outside `[a-z0-9_]` replaced by `_`. `Invoice Number` is stored and returned as `invoice_number`, and that normalised name is the key you will find in `jsonResult`. Each field also gets a stable `id` derived from its path, so `field-invoice-number`. ### Generate one from a document [Generate and save a schema](/api/document-extraction#generate-and-save-reusable-extraction-schema) from a parsed file: ```bash curl -s -X POST "$DOCAI_API/v1/extract/schemas/generate" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{"file_id":"'"$FILE_ID"'","title":"Invoice"}' ``` This is a queued operation, not a synchronous call — see [queued operations](#queued-operations-and-the-200-vs-202-rule). It debits 1 [credit](/quickstart#credits), and when it completes it has already saved the schema, so the output carries a `schema_id` you can use straight away: ```json { "operation_id": "b81f4c07-59ad-4f2a-8c31-6e0d92a7f145", "schema_id": "2f7d6a19-84c3-4b0e-9d51-7ac83e15b920", "schema": { "version": 1, "title": "Invoice", "description": "...", "fields": [] }, "jsonSchema": { "type": "object", "title": "Invoice", "properties": {}, "required": [] }, "modelId": "deepseek/deepseek-v4-flash", "documentType": "invoice", "schemaRationale": "...", "metrics": { "segmentCount": 84, "pageCount": 3, "contextChars": 21044, "modelCalls": 1 } } ``` Optional body fields: `segment_ids` to generate from part of the document only, `model_id` and `reasoning_level` to override the deployment defaults (`reasoning_level` defaults to `medium`, and an unknown value falls back to `medium` rather than erroring), and `idempotency_key`. ### Save and reuse [`POST /v1/extract/schemas`](/api/document-extraction#save-reusable-extraction-schema) stores a schema you wrote yourself. It is CRUD — no model call, no credit: ```bash curl -s -X POST "$DOCAI_API/v1/extract/schemas" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{"schema":{"title":"Invoice","fields":[{"name":"invoice_number","type":"string"}]},"knowledge_base_id":"'"$KB_ID"'"}' ``` Responds `201 { "schema": { ... } }`. Every schema endpoint returns the same object, and it carries **two** representations of the same thing: ```json { "schema": { "id": "2f7d6a19-84c3-4b0e-9d51-7ac83e15b920", "org_id": "3b8f1c22-7d90-4a11-8c5e-1f2a3b4c5d6e", "knowledge_base_id": "0f2c9a54-1d3e-4b77-9a2e-6c1f0b4d8e11", "file_id": null, "title": "Invoice", "description": "", "schema": { "version": 1, "title": "Invoice", "description": "", "fields": [] }, "jsonSchema": { "type": "object", "title": "Invoice", "properties": {}, "required": [], "additionalProperties": false }, "metadata": {}, "created_by_user_id": "…", "created_at": "2026-08-19T11:41:07.220Z", "updated_at": "2026-08-19T11:41:07.220Z" } } ``` - `schema` is the DocAI field list. Send it — or its `id` as `schema_id` — to `POST /v1/extract/runs`. - `jsonSchema` is a JSON Schema derived from the same fields. Send it as `schema` to `POST /v1/files/{fileId}/extract`. You may supply your own `jsonSchema` when saving; if you omit it, DocAI derives one. The derivation is deliberate but lossy in one place: a `table` or `array` field becomes `{"type": "array", "items": {"type": "object"}}` with no item properties. The job path builds per-column result nodes from `items.properties`, so if you want rows broken down on that path, write the `items.properties` yourself rather than relying on the derived version. The rest of the set: [`GET /v1/extract/schemas`](/api/document-extraction#list-reusable-extraction-schemas) lists them, [`GET /v1/extract/schemas/{schemaId}`](/api/document-extraction#fetch-extraction-schema) reads one, [`PATCH /v1/extract/schemas/{schemaId}`](/api/document-extraction#update-extraction-schema) updates title, description, `schema`, `jsonSchema` or `metadata` (metadata is merged, not replaced), and [`DELETE /v1/extract/schemas/{schemaId}`](/api/document-extraction#soft-delete-extraction-schema) soft-deletes, returning `{ "deleted": true, "schema_id": "…" }`. Access follows whatever the schema is attached to: viewer on its `file_id` if it has one, else on its `knowledge_base_id`, else the plain `files.read` / `files.write` permission. A missing or deleted schema is [`404 schema_not_found`](/errors). ## Queued operations, and the 200 vs 202 rule `POST /v1/extract/runs` and `POST /v1/extract/schemas/generate` do not run inline. The normal response is `202`: ```json { "operation_id": "7f4c8b19-2d63-4e05-9a8b-3c1d5e7f9a02", "status": "queued" } ``` `status` is the row's real status, never a fixed string, so a request that re-attaches to a run already in flight comes back `202` with `"status": "in_progress"`. The one case that is **not** `202` is an idempotent replay of a run that has already finished. Then the response is `200`, and the body is the completed output itself — not the `{ operation_id, status }` envelope: | Prior attempt for this idempotency key | Status | Body | Charged | |---|---|---|---| | none | `202` | `{ operation_id, status: "queued" }` | yes | | `queued` or `in_progress` | `202` | `{ operation_id, status }` | no | | `completed` | `200` | the full result | no | | `failed` or `cancelled` | `202` | `{ operation_id, status: "queued" }` | yes, a fresh attempt | **Branch on the status code, not on the body.** A `202` body has no result fields at all. If you send no `idempotency_key`, one is derived from `file_id`, the resolved schema and `model_id`. It hashes the schema *contents*, not the `schema_id`, so editing a saved schema produces a new key and a genuine re-run, while two identical schemas under different ids collide onto one. ### Polling ```bash curl -s "$DOCAI_API/v1/operations/$OPERATION_ID" -H "x-api-key: $DOCAI_API_KEY" ``` ```json { "operation_id": "7f4c8b19-2d63-4e05-9a8b-3c1d5e7f9a02", "operation_type": "extract_run", "status": "in_progress", "progress": { "done": 0, "total": 1, "phase": "extract" }, "output": null, "error": null, "created_at": "2026-08-19T11:22:31.004Z", "completed_at": null } ``` `output` is null on every status except `completed`, including `failed` and `cancelled` — read `error` on those. An extraction is one model call, so `progress` only ever reads `0/1` then `1/1`; it tells you the run has started, not how far through the document it is. Start polling at about 2s and back off to a 10s ceiling. Stop on `completed`, `failed` or `cancelled`. The operation endpoints are shared by every queued workflow and are documented once under [workflows](/workflows); in the reference they sit on the [Operations](/api/operations) tag. `POST /v1/operations/{operationId}/cancel` stops a run at its next checkpoint. An extraction has exactly one checkpoint, immediately before the model call: a cancel that lands before it stops the run before any model work, and a cancel that lands after it has no effect. Cancelling needs `extract.run` as well as viewer access, because it moves credits. Reading only needs viewer access. ## The output When an `extract_run` operation completes, `output` looks like this: ```json { "operation_id": "7f4c8b19-2d63-4e05-9a8b-3c1d5e7f9a02", "schema": { "version": 1, "title": "Invoice", "fields": [] }, "normalizedSchema": { "version": 1, "title": "Invoice", "fields": [] }, "jsonResult": { "invoice_number": "INV-2026-0417", "issue_date": "2026-03-11", "total_amount": 18400.5, "line_items": [ { "description": "Advisory retainer, March", "amount": 15000 }, { "description": "Expenses", "amount": 3400.5 } ] }, "resultTree": [ { "id": "field-invoice-number", "fieldId": "field-invoice-number", "path": "/invoice_number", "label": "invoice_number", "type": "string", "value": "INV-2026-0417", "confidence": 0.94, "citations": [ { "path": "/invoice_number", "segmentId": "p1_e4", "pageNumber": 1, "quote": "Invoice No. INV-2026-0417", "confidence": 0.94 } ], "children": [] } ], "citations": [ { "path": "/invoice_number", "segmentId": "p1_e4", "pageNumber": 1, "quote": "Invoice No. INV-2026-0417", "confidence": 0.94 } ], "modelId": "deepseek/deepseek-v4-flash", "warnings": [], "metrics": { "segmentCount": 84, "selectedSegmentCount": 84, "pageCount": 3, "contextChars": 21044, "modelCalls": 1, "durationMs": 0, "timedOut": false } } ``` **`jsonResult`** is the plain data object, keyed by normalised field name. If you only want the values, stop here. **`resultTree`** is the same data walked against the schema, one node per field, in schema order. Each node carries `path`, `label`, `type`, `value`, its own `citations`, and `children` for nested fields. `type` is the field's schema type, except that a null value reports `"null"`. `confidence` is taken from the node's first citation, or `0.8` when it has none — treat a bare `0.8` as "unattributed", not as a measurement. Use `resultTree` when you are rendering fields for review; use `jsonResult` when you are feeding a downstream system. **`citations`** is the flat list, and the join key is `path`. Each entry has: - `path` — a slash path into `jsonResult`: `/total_amount`, or `/line_items/0/amount` for an array member. - `segmentId` — the id of the parse segment the value was read from. This is the same id [`GET /v1/files/{fileId}/segments`](/api/knowledge-base-corpus#read-segment-map-data-from-latest-parse-artifacts) returns, so you can look up the segment's markdown and its `coordinates` (page number and bounding box) and highlight the exact region that produced the value. [Parsing](/parsing) explains the 0–1000 coordinate scale those boxes use. - `pageNumber` — the page the segment sits on. - `quote` — the snippet of segment text supporting the value. - `confidence`. That is what makes the output auditable: every field can be traced to a page, a region, and a quote, without a second call to the model. On this endpoint the citations come straight from the model and are passed through as they arrive, so treat a missing or unmatched citation as "no provenance for this field" rather than an error — check that the `path` you expect is actually present before showing a value as sourced. **`warnings`** on this path is whatever the model returned, and is usually empty. The file-job path below produces real, computed warnings instead. **`metrics`** describes the evidence that went in: how many segments and pages were used, and how many characters of context were sent. `modelCalls` is 1. `durationMs` is always `0` and `timedOut` always `false` on this path — they are part of the shape, not a measurement, so do not build timing dashboards on them. There is no `credit` field. Credit spend is visible in the ledger ([`GET /v1/billing/ledger`](/api/billing#list-credit-ledger-entries-or-grouped-activity)), not echoed on the operation. ## Extracting as a file job [`POST /v1/files/{fileId}/extract`](/api/file-processing#queue-extract-job-for-file-through-the-control-plane) takes a **JSON Schema** and returns `202` with a job: ```bash curl -s -X POST "$DOCAI_API/v1/files/$FILE_ID/extract" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{"schema_name":"invoice_v1","schema":{"type":"object","properties":{"invoice_number":{"type":"string"},"total_amount":{"type":"number"}},"required":["invoice_number"]}}' ``` ```json { "job": { "job_id": "d4a6f2b8-1c39-4e7a-9051-8b2c3d4e5f60", "status": "queued", "kind": "extract", "stage": null, "percent": 0, "result": {} }, "idempotency_key": "extract:b5c4a3d2-9e18-4f6a-8c07-2d1b3e4f5a69:1f0c2b3a4d5e6f70:9a8b7c6d5e4f3021" } ``` The default idempotency key is `extract:{file_version_id}:{schema_hash}:{options_hash}`, so the same schema against the same version replays: the response carries the existing job and `"idempotent": true` instead of queuing a second one. Pass your own `idempotency_key` to force a new run. `options` is passed through to the processing plane; `options.scope` narrows which parts of the parse are used. Poll with [`GET /v1/files/{fileId}/jobs/{jobId}`](/api/file-processing#fetch-parse-extract-job-status-for-a-file). The job walks stages `load-parse`, `build-evidence`, `extract`, `validate`, `write-artifacts`. On completion `result` carries `extraction_result_id`, `artifact_manifest_id` and `validation_errors`, and the extraction itself is written as an artifact. List the manifests with [`GET /v1/files/{fileId}/artefacts`](/api/knowledge-base-corpus#fetch-parse-extract-artefact-links): ```bash curl -s "$DOCAI_API/v1/files/$FILE_ID/artefacts" -H "x-api-key: $DOCAI_API_KEY" ``` The `extract` manifest exposes a `result_json` link. [Fetch it](/api/knowledge-base-corpus#download-or-list-an-artefact-by-generated-link): ```bash curl -s "$DOCAI_API/v1/artifacts/$MANIFEST_ID/result_json" -H "x-api-key: $DOCAI_API_KEY" ``` The document behind that link carries `schema`, `data`, `jsonResult` (the same object as `data`), `resultTree`, `citations`, `evidence`, `warnings`, `metrics`, `validationErrors`, `schema_name` and `modelId`. Three things differ from the operation path, all of them in your favour if you care about correctness: - **Validation is real.** The output is validated against your JSON Schema with a Draft 2020-12 validator, and every error is reported both in `validationErrors` (`path` plus `message`) and as a warning with code `schema_validation_error`. A required field that came back empty adds a `required_value_missing` warning. - **Citations are computed, not just trusted.** Model-supplied citations are merged with citations resolved against the parse grounding, and each entry additionally carries `elementId` and `bbox`, so you can draw a box without looking the segment up. - **Result nodes flag absence.** A node with no value gains `"missingReason": "not_in_evidence"`, and `confidence` is the minimum across its citations rather than the first one's. `metrics` on this path is measured rather than nominal: `durationMs` is the real elapsed time, and it adds `extractedFieldCount` and `citationCount`. ## Bulk extraction [`POST /v1/files/extract/bulk`](/api/file-processing#queue-extract-jobs-for-multiple-files) queues one job per file with a single schema: ```bash curl -s -X POST "$DOCAI_API/v1/files/extract/bulk" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{"file_ids":["'"$FILE_A"'","'"$FILE_B"'"],"schema":{"type":"object","properties":{"invoice_number":{"type":"string"}}}}' ``` Responds `202 { "jobs": [ … ] }`, one entry per requested file in request order, each the same shape as the single-file dispatch. When you pass an `idempotency_key`, each file's job gets `"{key}:{fileId}"` so the files do not collide. **It is not atomic.** Files are dispatched in order, and the first failure aborts the request — a file that is unparsed, missing, or not accessible throws mid-loop. Jobs already queued for earlier files in the list stay queued and keep running; you get an error response and a partially started batch. Check that every file is parsed before calling, or call the single-file endpoint per file so a failure is scoped to one document. ### What it costs Extraction jobs on the processing plane do not debit credits in v1. The cost of a bulk extraction is the [parsing](/parsing) that had to happen first, at 1 credit per parsed page — so a hundred unparsed 20-page documents is 2,000 credits of parse before a single field is extracted, not a free operation. If you instead loop `POST /v1/extract/runs` over the same files, that is a fixed 1 credit per file. Check [`GET /v1/billing/credits`](/api/billing#get-credits-quota-subscription-status-and-ledger-derived-usage-totals) before starting a large batch; there is no dry-run mode and no per-request cap. ## Credits `POST /v1/extract/runs` and `POST /v1/extract/schemas/generate` each debit **exactly 1 credit**, regardless of document size or how many internal model calls the run makes. The order is fixed and matters: 1. The request is validated — file access, permission, parse artifacts, schema, model. 2. The credit is debited. 3. Only then is the operation row created or reset, and the work queued. A declined debit (`402 insufficient_credits`, or `402 monthly_quota_exceeded` where a quota is set) throws before anything is enqueued, so a refused payment can never leave a live job behind. Each attempt is charged under its own ledger key, `:attempt-N`. A `failed` or `cancelled` attempt is refunded under `:attempt-N:refund` as a separate `reversal` row, so the charge and the refund stay individually auditable. That is why retrying a failed run charges again — the first attempt's credit was already returned. Schema CRUD (`POST`/`GET`/`PATCH`/`DELETE /v1/extract/schemas`) never debits. ## Worked example Parse a document, generate a schema from it, extract against that schema, and read the result. Uploading and parsing is covered in the [quickstart](/quickstart); this picks up from a completed parse. **1. Confirm the file is parsed.** A parse manifest has to exist, or everything below returns `409`. ```bash curl -s "$DOCAI_API/v1/files/$FILE_ID/artefacts" -H "x-api-key: $DOCAI_API_KEY" \ | jq '.artifacts[] | select(.artifact_type == "parse") | .id' ``` **2. Generate a schema.** ```bash curl -s -X POST "$DOCAI_API/v1/extract/schemas/generate" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{"file_id":"'"$FILE_ID"'","title":"Invoice"}' ``` ```json { "operation_id": "b81f4c07-59ad-4f2a-8c31-6e0d92a7f145", "status": "queued" } ``` **3. Poll until it completes, then take the `schema_id`.** ```bash curl -s "$DOCAI_API/v1/operations/b81f4c07-59ad-4f2a-8c31-6e0d92a7f145" \ -H "x-api-key: $DOCAI_API_KEY" | jq '{status, schema_id: .output.schema_id}' ``` ```json { "status": "completed", "schema_id": "2f7d6a19-84c3-4b0e-9d51-7ac83e15b920" } ``` Review the generated fields before you rely on them — `GET /v1/extract/schemas/2f7d6a19-84c3-4b0e-9d51-7ac83e15b920` returns the full definition, and `PATCH` edits it. **4. Run the extraction.** ```bash curl -s -X POST "$DOCAI_API/v1/extract/runs" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{"file_id":"'"$FILE_ID"'","schema_id":"2f7d6a19-84c3-4b0e-9d51-7ac83e15b920"}' ``` ```json { "operation_id": "7f4c8b19-2d63-4e05-9a8b-3c1d5e7f9a02", "status": "queued" } ``` One credit has been debited at this point. **5. Poll until completed.** ```bash curl -s "$DOCAI_API/v1/operations/7f4c8b19-2d63-4e05-9a8b-3c1d5e7f9a02" \ -H "x-api-key: $DOCAI_API_KEY" ``` ```json { "operation_id": "7f4c8b19-2d63-4e05-9a8b-3c1d5e7f9a02", "operation_type": "extract_run", "status": "completed", "progress": { "done": 1, "total": 1, "phase": "extract" }, "output": { "jsonResult": { "invoice_number": "INV-2026-0417", "issue_date": "2026-03-11", "total_amount": 18400.5 }, "citations": [ { "path": "/invoice_number", "segmentId": "p1_e4", "pageNumber": 1, "quote": "Invoice No. INV-2026-0417", "confidence": 0.94 }, { "path": "/total_amount", "segmentId": "p3_e11", "pageNumber": 3, "quote": "Total due: 18,400.50", "confidence": 0.9 } ], "warnings": [], "modelId": "deepseek/deepseek-v4-flash" }, "error": null, "created_at": "2026-08-19T11:22:31.004Z", "completed_at": "2026-08-19T11:23:04.881Z" } ``` **6. Show where a value came from.** Take the citation's `segmentId` and look it up in the file's segments: ```bash curl -s "$DOCAI_API/v1/files/$FILE_ID/segments" -H "x-api-key: $DOCAI_API_KEY" \ | jq '.[] | select(.id == "p3_e11") | {id, markdown, coordinates}' ``` ```json { "id": "p3_e11", "markdown": "Total due: 18,400.50", "coordinates": { "pageNumber": 3, "xmin": 341, "ymin": 688, "xmax": 522, "ymax": 706 } } ``` Those coordinates are the box to highlight on page 3. That is the whole provenance chain: field → citation → segment → page region. ## Errors | Status | Code | Cause | |---|---|---| | 400 | `file_id_is_required` | No `file_id` on a workflow request | | 400 | `schema_is_required` | No `schema` on a file extract or bulk request | | 400 | `unsupported_model` | `model_id` is not an available model | | 402 | `insufficient_credits` | Balance minus active reservations is below the charge | | 402 | `monthly_quota_exceeded` | The organization's quota would be exceeded | | 403 | `permission_required` | Caller lacks `extract.run` | | 404 | `schema_not_found` | Unknown, deleted, or another organization's `schema_id` | | 404 | `operation_not_found` | Unknown operation id, or its file was permanently deleted | | 409 | `file_must_be_parsed` | `/v1/extract/runs` on a file with no readable parse artifact, or an empty `segment_ids` selection | | 409 | `file_must_be_parsed_before_extraction` | `/v1/files/{fileId}/extract` on a file version with no parse artifact | | 422 | `invalid_schema` | Neither `schema_id` nor an inline `schema` was supplied | | 503 | `model_provider_unavailable` | No default model is configured or reachable | Branch on `code`, never on `error` — the sentence may be reworded, the code is stable. The full list is on the [error codes](/errors) page. ## Where to next - [Knowledge bases](/knowledge-bases) — read a whole corpus in one call and search it without spending credits, to find the documents worth extracting from. - [Split](/split) and [Classify](/classify) — separate a combined PDF or label it before you extract, so each schema runs on one document. - [MCP server](/mcp) — `docai_generate_schema` and `docai_extract_bulk` drive this same flow from Claude Code, Cursor or Codex.