# Quickstart Four calls get a document parsed: find a knowledge base, upload, parse, poll. Everything after that — extract, split, search — reuses the same conventions, so read this page once and the rest of the API follows. ```bash export DOCAI_API_KEY="docai_..." export DOCAI_API="https://api.providus.ai" ``` ## Base URL Cloud is `https://api.providus.ai`. Every path below is relative to it. On-prem deployments serve the same `/v1` surface from their own host, so set `DOCAI_API` to that instead. Nothing else in this page changes. ## Authentication Every request carries an API key in the `x-api-key` header: ```bash curl -s "$DOCAI_API/v1/knowledge-bases" -H "x-api-key: $DOCAI_API_KEY" ``` Keys are created by an organization admin in Settings → API Keys. Staff members cannot create, list, rotate or revoke them. Keys are prefixed `docai`, and the secret is shown once at creation. ### A key is organization-root This is the most important thing to understand before you put a key anywhere. DocAI has a role-based access control system: staff members hold permissions such as `files.read` or `parse.run`, plus per-resource grants on individual knowledge bases and agents. **An API key bypasses all of it.** The check is one line, in `access.ts`: ```ts export function isRootContext(ctx: OrgContext) { return ctx.role === "org_admin" || ctx.user.type === "api_key"; } ``` Every permission check and every resource-access check returns true for a root context. In practice: - The key can read and write **every** knowledge base in the organization, not the ones someone granted it. - There is no read-only key and no knowledge-base-scoped key today. - Anything holding the key — a CI job, an editor config, a teammate's laptop — has organization-wide access. Treat a key as production access. If you need per-user restriction, use the session-based path with `x-docai-org-id` rather than a key. An API key is already bound to one organization. Sending `x-docai-org-id` or `?org_id=` alongside it is allowed only if the value matches; a mismatch is rejected with `api_key_org_mismatch`. ## The happy path ### 1. Find a knowledge base Documents live in knowledge bases. List the ones the key can reach: ```bash curl -s "$DOCAI_API/v1/knowledge-bases" -H "x-api-key: $DOCAI_API_KEY" ``` ```json { "knowledge_bases": [ { "id": "0f2c9a54-1d3e-4b77-9a2e-6c1f0b4d8e11", "org_id": "3b8f1c22-7d90-4a11-8c5e-1f2a3b4c5d6e", "name": "Contracts", "description": null, "metadata": {}, "created_at": "2026-08-01T09:12:44.000Z", "updated_at": "2026-08-01T09:12:44.000Z" } ] } ``` `POST /v1/knowledge-bases` creates one if you have none. ### 2. Upload a document Multipart, with the file in a field called `file` and the target knowledge base in `knowledge_base_id`: ```bash curl -s -X POST "$DOCAI_API/v1/files" \ -H "x-api-key: $DOCAI_API_KEY" \ -F "file=@contract.pdf" \ -F "knowledge_base_id=0f2c9a54-1d3e-4b77-9a2e-6c1f0b4d8e11" ``` Responds `201`: ```json { "file": { "id": "9d1e77b0-4c2a-4e8f-b3d5-72a0c9e14f83", "org_id": "3b8f1c22-7d90-4a11-8c5e-1f2a3b4c5d6e", "knowledge_base_id": "0f2c9a54-1d3e-4b77-9a2e-6c1f0b4d8e11", "folder_id": null, "filename": "contract.pdf", "content_type": "application/pdf", "metadata": {}, "deleted_at": null, "current_version": { "id": "b5c4a3d2-9e18-4f6a-8c07-2d1b3e4f5a69", "version_number": 1, "byte_size": 481233, "page_count": null } } } ``` Optional form fields: `folder_id`, `filename` to override the uploaded name, `metadata` as a JSON string, and `auto_parse=true` to queue the parse in the same call. With `auto_parse`, the response also carries `parse_job`, `parse_usage` and `credit_reservation`, and you can skip straight to step 4. Filenames must be unique within a knowledge base folder; a clash returns `duplicate_filename`. ### 3. Parse it ```bash curl -s -X POST "$DOCAI_API/v1/files/$FILE_ID/parse" \ -H "x-api-key: $DOCAI_API_KEY" \ -H "content-type: application/json" \ -d '{}' ``` Responds `202` — the job is queued, not done: ```json { "job": { "job_id": "c7e21f48-3a6b-4d90-91f2-58c0ab7e2d34", "status": "queued", "raw_status": "queued", "kind": "parse", "stage": null, "percent": 0, "total_pages": 0, "completed_pages": 0, "error": null, "result": {}, "created_at": "2026-08-19T11:04:02.117Z", "started_at": null, "cancel_requested_at": null, "completed_at": null }, "usage": { "estimated_pages": 12, "required_credits": 12, "balance_credits": 686, "reserved_credits": 12, "monthly_used_credits": 324, "monthly_quota_credits": 1010 }, "credit_reservation": { "id": "e3a91b26-5c47-4f80-a1d9-0b6e2f7c8a45", "status": "reserved", "estimated_credits": 12, "reserved_credits": 12, "used_credits": 0 }, "idempotency_key": "parse:b5c4a3d2-9e18-4f6a-8c07-2d1b3e4f5a69:1f0c2b3a4d5e6f70", "idempotent": false } ``` The body accepts `options` (passed through to the parser, for example `{"backend": "glm-ocr"}`) and `idempotency_key`. ### 4. Poll the job ```bash curl -s "$DOCAI_API/v1/files/$FILE_ID/jobs/$JOB_ID" -H "x-api-key: $DOCAI_API_KEY" ``` ```json { "job": { "job_id": "c7e21f48-3a6b-4d90-91f2-58c0ab7e2d34", "status": "running", "raw_status": "in_progress", "kind": "parse", "stage": "layout", "percent": 42, "total_pages": 12, "completed_pages": 5, "error": null, "result": {}, "created_at": "2026-08-19T11:04:02.117Z", "started_at": "2026-08-19T11:04:09.882Z", "cancel_requested_at": null, "completed_at": null } } ``` `GET /v1/files/{fileId}/jobs` lists every job on a file, and `GET /v1/files/{fileId}/jobs/{jobId}/events` returns the event stream if you want stage-level detail rather than a percentage. ### 5. Read the artifacts When the job completes, list the file's artifact manifests: ```bash curl -s "$DOCAI_API/v1/files/$FILE_ID/artefacts" -H "x-api-key: $DOCAI_API_KEY" ``` ```json { "artifacts": [ { "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "file_id": "9d1e77b0-4c2a-4e8f-b3d5-72a0c9e14f83", "file_version_id": "b5c4a3d2-9e18-4f6a-8c07-2d1b3e4f5a69", "job_id": "c7e21f48-3a6b-4d90-91f2-58c0ab7e2d34", "artifact_type": "parse", "manifest": { "pages": 12 }, "links": { "result_md": "/v1/artifacts/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/result_md", "grounding_json": "/v1/artifacts/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/grounding_json", "result_json": null, "workbook_json": null, "tables_json": "/v1/artifacts/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/tables_json", "pages": "/v1/artifacts/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/pages", "imgs": "/v1/artifacts/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/imgs" } } ] } ``` A `null` link means the parse did not produce that artifact. Fetch a non-null one directly: ```bash curl -s "$DOCAI_API/v1/artifacts/$MANIFEST_ID/result_md" -H "x-api-key: $DOCAI_API_KEY" ``` `pages` and `imgs` are directories. Fetching one without a query returns an entry listing; add `?file=` to pull a single member. For structured text with page coordinates rather than raw markdown, `GET /v1/files/{fileId}/segments` is usually what you want. ### Skip the loop `GET /v1/knowledge-bases/{kbId}/index` returns every file in a knowledge base with parse status and artifact links in one call, which is faster than calling `GET /v1/files` and then fetching each file individually. Prefer the index endpoint whenever you need more than one file's status. ## Jobs and operations Two different asynchronous shapes exist, and confusing them is the most common integration mistake. | | Job | Operation | |---|---|---| | What runs | parse or extract on one file | a queued document workflow | | Started by | `POST /v1/files/{id}/parse`, `POST /v1/files/{id}/extract` | `POST /v1/extract/runs`, `POST /v1/split/runs`, `POST /v1/split/rules/suggest`, `POST /v1/classify/runs`, `POST /v1/edit/runs`, `POST /v1/extract/schemas/generate` | | Identifier | `job_id` | `operation_id` | | Poll at | `GET /v1/files/{fileId}/jobs/{jobId}` | `GET /v1/operations/{operationId}` | | Owned by | the processing plane | the control plane | | Result | artifact manifests, fetched separately | `output`, inline in the poll response | They even spell "in flight" differently, which is a useful tell when you are staring at a response: a job reports `running`, an operation reports `in_progress`. ### Job statuses `status` is normalised to five values. `raw_status` carries the unnormalised value as stored, if you need it. | `status` | `raw_status` | Meaning | |---|---|---| | `queued` | `queued` | Accepted, waiting for a worker to lease it | | `running` | `started`, `in_progress` | A worker has it; `percent` and `completed_pages` advance | | `completed` | `completed` | Artifacts are written and `result` is populated | | `failed` | `failed` | `error` explains why; the reservation is finalised as `failed` | | `cancelled` | `cancelled` | Cancellation was honoured | `cancel_requested_at` is set the moment cancellation is requested, which can be before the status becomes terminal. Deleting a file is blocked while a parse or extract job is active. ### Operation statuses Operations report `queued`, `in_progress`, `completed`, `failed` or `cancelled` — no normalisation, so `in_progress` appears literally. ```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": "split_run", "status": "in_progress", "progress": { "done": 3, "total": 8, "phase": "windows" }, "output": null, "error": null, "created_at": "2026-08-19T11:22:31.004Z", "completed_at": null } ``` **`output` is null until `status` is `completed`.** It is withheld on every other status, including `failed` and `cancelled`, so a client that reads `output` on its first poll sees null rather than an empty result. Keep polling. `POST /v1/operations/{operationId}/cancel` requests cancellation. It is cooperative — the run stops at its next checkpoint — and an already-terminal operation is left as it is rather than erroring. The response shape matches the GET. Document workflows are slow by design. A split window takes tens of seconds at the default reasoning level, and a run covers several windows, so budget in minutes and poll rather than holding a request open. ## Idempotency Parse, extract and the queued workflows all accept an `idempotency_key`. If you omit it, one is derived for you, so replay protection is on by default. **Parse.** The default key is `parse:{file_version_id}:{options_hash}`. Sending the same request again returns the existing job with `"idempotent": true` and does not queue a second one or reserve a second lot of credits. Two exceptions worth knowing: - A job that ended `failed` or `cancelled` is retryable. Without an explicit key, a repeat request starts a fresh attempt under a new derived key and returns `retry_of_job_id` pointing at the previous one. - With an explicit `idempotency_key`, that key is matched exactly, and a terminal job under it is returned as-is rather than retried. Pass a new key to force a retry. Because the derived key includes the file version, uploading new content via `PUT /v1/files/{fileId}/content` gives a new version and therefore a new key — re-parsing after a content change is never suppressed. **Extract.** The default key is derived from the file version plus hashes of the schema and options, so the same schema against the same version replays instead of re-running. **Queued workflows** (`/v1/extract/runs`, `/v1/split/runs`, `/v1/split/rules/suggest`, and the rest) behave slightly differently, because the result lives on the operation: - Replaying a key whose operation already **completed** returns `200` with the output inline, not `202`. - Replaying while it is still queued or running re-attaches: `202` with the same `operation_id`. - Replaying a **failed or cancelled** operation starts a fresh attempt and charges again. The credit ledger records each attempt under its own `:attempt-N` key, so the charges are traceable. ## Errors Every failure returns the same envelope: ```json { "error": "Knowledge base access denied", "code": "knowledge_base_access_denied" } ``` **Branch on `code`.** It is stable across releases. `error` is a human-readable sentence and may be reworded at any time, so matching on it will break. Some responses add a `message` field with extra detail; authentication failures generally do. Codes you will meet early: | Status | Code | Cause | |---|---|---| | 401 | `invalid_api_key` | Key is wrong, revoked, or expired | | 403 | `api_key_org_mismatch` | `x-docai-org-id` does not match the key's organization | | 403 | `permission_required` | Session-based caller lacks a permission | | 402 | `insufficient_credits` | Balance minus active reservations is below the estimate | | 402 | `monthly_quota_exceeded` | The organization's monthly quota would be exceeded | | 404 | `file_not_found`, `job_not_found`, `operation_not_found` | Wrong id, another organization's id, or soft-deleted | | 409 | `file_must_be_parsed_before_extraction` | Extract was called on a file with no parse artifacts | | 409 | `duplicate_filename` | A live file of that name exists in the same folder | The full list is on the [error codes](/errors) page, generated from the API's own registry. ## Credits Parse is billed per page: one credit per estimated page. The flow is reserve-then-commit, not charge-on-request. 1. **Reserve.** When the parse is queued, DocAI estimates the page count and reserves that many credits against the organization's account. If the available balance — balance minus everything already reserved — is short, the request fails `402 insufficient_credits` before any work starts. A monthly quota, where one is set, is checked at the same point and fails `402 monthly_quota_exceeded`. 2. **Commit.** The processing plane reports pages as it finishes them, and each one debits the ledger against the reservation. A page cannot debit beyond what was reserved; that would fail `reserved_credits_exhausted`. 3. **Finalise.** When the job reaches a terminal state the reservation is closed: `committed` if pages were actually billed, `released` if the job completed without billing any, `failed` or `cancelled` otherwise. A released or cancelled reservation stops holding balance. That is why the parse response carries both `usage` (the estimate and your account position) and `credit_reservation` (what is being held). The queued workflows are billed differently — a fixed one credit each, debited after validation and before any model work. Search costs nothing. `POST /v1/knowledge-bases/{kbId}/search` is a Postgres full-text read over parsed evidence and calls no model, so search first and read only what you need. Check the account at any time: ```bash curl -s "$DOCAI_API/v1/billing/credits" -H "x-api-key: $DOCAI_API_KEY" ``` ```json { "credits": { "balance_credits": 686, "reserved_credits": 0, "total_used_credits": 324, "monthly_used_credits": 324, "monthly_quota_credits": 1010 }, "subscription": null } ``` `total_used_credits` is the lifetime debit total; `monthly_used_credits` counts from the current billing period start. `monthly_quota_credits` is null when no quota applies. `GET /v1/billing/ledger` returns the individual entries. ## Where to next - **[Parsing](/parsing).** What a parse produces, how to fetch each artifact, and how to trust the coordinates. - **[Extraction](/extraction).** `POST /v1/files/{fileId}/extract` takes a schema and returns JSON shaped to it, with citations back to the source segments. - **[Knowledge bases and search](/knowledge-bases).** Organise documents, read a whole knowledge base in one call, and search parsed evidence for nothing. - **[MCP server](/mcp).** The same API as tools inside Claude Code, Cursor and Codex. - **[API reference](/api).** Every endpoint, request body and response.