> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.cloudraker.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.cloudraker.com/_mcp/server.

# API context for agents

Point a coding agent at **`https://docs.cloudraker.com/developers/agents.md`**. The page gives it all it needs to write a working integration. It covers auth, the request and error envelopes, all six verbs, and the run lifecycle. It also covers multi-step agent runs, the schema dialect, and the limits.

```bash
curl -s https://docs.cloudraker.com/developers/agents.md
```

Every page on this site has a `.md` twin. `https://docs.cloudraker.com/llms.txt` indexes them all. Start from this page.

## Base URL and auth

Base URL: `https://api.cloudraker.com`. Every request carries an organization API key as a bearer token:

```
Authorization: Bearer $CLOUDRAKER_API_KEY
Content-Type: application/json
```

One key belongs to one organization. The key identifies the tenant, so you never pass a tenant, account, or workspace id. Create keys in the app under Admin → API keys. The value is shown once.

## The six verbs

Each verb is a single `POST` that creates a **run**. Each is synchronous by default and accepts `?wait=` (0–120 seconds, 60 default). Minimal bodies:

```jsonc
// POST /v1/extract — document -> JSON matching your schema; add "citations": true for evidence
{ "file": { "url": "https://example.com/invoice.pdf" }, "citations": true,
  "schema": { "type": "object", "properties": { "total": { "type": ["number", "null"] } } } }

// POST /v1/extract — no schema: infer one, then extract with it (exploration only)
{ "file": { "url": "https://example.com/invoice.pdf" }, "hints": "It's an invoice; capture the header fields" }

// POST /v1/extract/batch — one saved config over many files; always 202
{ "action": "invoice-header", "files": [{ "url": "…" }, { "url": "…" }] }

// POST /v1/parse — document -> markdown + structured JSON, no schema
{ "file": { "url": "https://example.com/contract.pdf" } }

// POST /v1/redact — destructive PII removal; returns a NEW file
{ "file": { "id": "<fileId>" }, "categories": ["ssn", "ein"], "mode": "targeted" }

// POST /v1/fill — fill a form PDF; exactly one of "values" (deterministic, no model)
// or "files" (drafted from sources); "template" optional when "action" carries one
{ "template": { "id": "<templateId>" }, "files": [{ "id": "<fileId>" }], "review": "none", "output": "flattened" }
{ "action": "<fill config id or slug>", "values": { "<field name>": "Acme Co." } }

// POST /v1/sign — e-signature envelope; always 202, status needs_input
{ "file": { "id": "<fileId>" }, "signers": [{ "name": "Jane Doe", "email": "jane@example.com" }] }

// POST /v1/pipeline — several capabilities over one file set, in parallel (not chained)
{ "files": [{ "url": "…" }],
  "steps": [{ "extract": { "schema": { "type": "object", "properties": {} } } }, { "redact": { "categories": ["ssn"] } }] }
```

Shared optional fields on every verb: `metadata` (your own key/values, ≤10 KB, echoed back and filterable), `webhook` (`{url}` or `{id}`), `ttl` (seconds, default 86400, max 604800), and the `idempotency-key` request header.

Citations are **off by default everywhere**. To get them, send `"citations": true` on `POST /v1/extract`, on `POST /v1/extract/batch`, or on an extract step in a pipeline. On a saved config, the `grounding` install setting does the same and also defaults to `false`.

`extract`, `redact`, `fill`, and `sign` also accept `action: "<id or slug>"` instead of inline config. Inline fields win over the saved ones. Save a config with `POST /v1/{extract|redact|fill}/configs {name, config}`. The flat `POST /v1/actions {capability, name, config}` is a deprecated alias. Discover configurable shapes with `GET /v1/actions/catalog`.

## File references

Every file input takes one of two shapes, never a multipart upload:

```jsonc
{ "url": "https://…", "name": "invoice.pdf", "processing": "auto" }  // fetched server-side
{ "id": "a04d6597-4e34-4a99-94ea-964c289a4c68" }                     // already registered or produced
```

`processing`: `auto` (default), `ocr`, `simple`, `transcribe`, `transcribe_diarize`. Single-file verbs take `file`. Multi-file verbs take `files[]` (up to 100). A request with both is a `400`.

Register a persistent file with `POST /v1/files`. Send `{url, name?}` — the source **must** serve a `Content-Length`. Or send `{name, mimeType}` to get an `uploadUrl` valid for 15 minutes. `PUT` the bytes to it with the identical `Content-Type`. Poll `GET /v1/files/:id` until `status: "ready"`. Files a run creates inline expire with the run. Files you register do not.

## Runs

```
GET    /v1/runs                   list (filters below)
GET    /v1/runs/:id               status + result; accepts ?wait=
POST   /v1/runs/:id/cancel        stop in-flight work
DELETE /v1/runs/:id               purge now (204, idempotent)
POST   /v1/runs/:id/keep          persist into a space; clears the TTL
GET    /v1/runs/:id/output/:name  302 to a signed download URL
```

Id prefixes: `exr_` extract, `par_` parse, `rdr_` redact, `flr_` fill, `sgr_` sign, `plr_` pipeline. Treat ids as opaque strings.

Statuses: `queued`, `processing`, `processed`, `failed`, `cancelled`, `expired`, `needs_input`. `output` exists only at `processed`. `needs_input` means a signature envelope is open; read it through `envelopeUrl`.

A synchronous call that does not finish within `?wait=` returns **`202` with the run handle, never a timeout error**. Poll `statusUrl` or use a webhook. Sign runs are exempt from the TTL purge while the envelope is open. Every other run and its inline files are purged at `expiresAt`.

`GET /v1/runs` takes `object`, `status` (the six values above minus `queued`), `limit` (1–50, default 20), `cursor`, and up to three `metadata.<key>=<value>` pairs. It returns `{object:"list", data:[…handles…], has_more, cursor}`. The list is **eventually consistent**. `GET /v1/runs/:id` is authoritative.

## Agents and agent runs

This surface is separate from the verbs and from `/v1/runs`. An **agent** is a saved, versioned, multi-step automation (steps + saved configs + sign-off gates). An **agent run** (`agr_…`) is one execution of an agent, and it can wait days on a person.

```
GET  /v1/agents                                  agents you can run
GET  /v1/agents/:id                              one agent: tasks[], actions[], version
POST /v1/agent-runs                              start one; ?wait= 0–120, default 60
GET  /v1/agent-runs/:id                          read it; ?wait= 0–120, default 0
POST /v1/agent-runs/:id/approvals/:approvalId     {decision: "approve"|"reject", note?, params?, files?}
POST /v1/agent-runs/:id/tasks/:taskId/complete    {note?, files?} — send {} if neither
```

```jsonc
// POST /v1/agent-runs — files 1–200, metadata ≤10 KB, no ttl
{ "agent": "<agentId>", "files": [{ "url": "https://example.com/intake.pdf" }],
  "metadata": { "caseId": "42" }, "webhook": { "url": "https://example.com/hooks" } }
```

Statuses: `queued`, `processing`, `waiting`, `paused`, `completed`, `failed`, `cancelled`, `expired`. A `completed` run with unfinished work also carries `incomplete: true`. `paused` (`paused.reason`) is resumable, **not** a failure. The run carries `tasks[]` (`executor: "agent"|"human"`, `status: pending|ready|in_progress|completed|skipped`), `approvals[]` (`kind: "before"|"output"`, with the proposed `params` and `files`), `waiting: {approvals, tasks, summary}`, `progress.tasks`, `result`, `output.files[]` (signed \~1 h), `error`, `metadata`, `expiresAt` (\~7 days).

Rules that differ from capability runs:

* **Poll a `queued` agent run.** A read starts it once its files are prepared. A webhook alone leaves it `queued`.
* **No list, no cancel, no delete.** Agent runs never appear in `GET /v1/runs`. Keep the `agr_` id, or tag runs with `metadata`.
* **No `ttl`.** Files passed by URL become persistent files. `expiresAt` (\~7 days) is the *deadline*. Past it, an unfinished run parks permanently.
* Only `executor: "human"` steps are completable (`422` otherwise). An unmet `dependsOn` or an already-closed step is `409`. A rejection **requires** `note`. A second decision on the same approval is `409`.
* Webhook events: `agent_run.waiting`, `agent_run.approval_requested` (minimal; re-read the run for `params`), `agent_run.task_ready`, `agent_run.completed`, `agent_run.failed` (also carries `cancelled`/`expired`; trust `data.status`). Deliveries use the same signature and JWKS as every other delivery. `processingId` carries the `agr_` id.

## Extract output

```jsonc
{
  "object": "extract_run", "id": "exr_…", "status": "processed",
  "config": { "schema": { /* the schema actually applied — inline, saved, or inferred */ } },
  "output": {
    "value": { "total": 1889.45, "po_number": null },  // alias of documents[0].value
    "citations": {                                     // only when the run was grounded; field path -> evidence
      "total": [{ "fileId": "…", "page": 0, "bbox": { "x": 0.1, "y": 0.2, "width": 0.1, "height": 0.02 },
                  "text": "CAD 1,889.45", "confidence": 5 }],
      "po_number": [{ "fileId": "…", "notFound": true }]  // declared absent; value is null, never a guess
    },
    "documents": [{ "fileId": "…", "name": "invoice.pdf", "status": "done", "value": { /* … */ }, "citations": { /* same shape, per document */ } }]
  }
}
```

`page` is 0-based. `bbox` is normalized `0`–`1` with a top-left origin. `confidence` is `0`–`5`. Audio grounds with a `timecode` instead of `page`/`bbox`. A `notFound` entry carries only `fileId`: no page, bbox, timecode, text, or confidence. A field the document does not contain comes back `null`. Declare every property nullable for this reason.

Grounding is guaranteed-or-flagged. When citations are on, every filled field returns at least one citation or an explicit `notFound` declaration. A deterministic second model pass cites or declares absent anything the first pass left uncited. When the run was **not** grounded, the `citations` key is absent, never an empty object. `citationsOmitted: true` on `output` means the result exceeded the size budget, so citations were dropped. It appears at output level only, never per document.

## Extraction schema dialect

Plain JSON Schema with four constraints, checked before the run starts (`400 invalid_schema`, with the offending path):

1. The root must be `{"type": "object"}`.
2. Nesting may not exceed 5 levels.
3. No `$defs`, `$ref`, `oneOf`, `anyOf`, `allOf`, `const`, or `pattern`. These are the only rejected keywords, and only in keyword positions (a property *named* `pattern` is valid). Constrain values with `enum`. Describe formats in `description`.
4. The serialized schema must stay under 64 KB.

Make every primitive nullable: `{"type": ["string", "null"]}`. This is **recommended, never enforced**. A non-nullable field pressures the extractor to invent a value instead of a `null`. This change has the highest impact on extraction quality. With citations on, an absent value is also marked `notFound`, not left ambiguous.

Add a `description` per property. It is the strongest accuracy lever. `unit` controls cardinality: `per_document` (default), `across_documents`, `rows_per_document`.

## Errors

Every `/v1` failure is the same envelope, with `x-request-id` also on the response headers:

```json
{ "code": "invalid_request", "message": "file: provide exactly one of `file` or `files`",
  "retryable": false, "requestId": "req_…", "docUrl": "https://docs.cloudraker.com/developers/errors#invalid_request" }
```

Branch on `code`, never on `message`. Common codes: `unauthorized`, `invalid_token` (401); `invalid_request`, `invalid_schema`, `action_unknown`, `file_ineligible`, `run_too_large`, `schema_too_large` (400); `not_found` (404); `file_not_ready`, `output_not_ready`, `already_kept`, `pipeline_running` (409); `run_expired` (410); `file_fetch_failed`, `webhook_endpoint_not_found`, `webhook_endpoint_disabled` (422); `rate_limited` (429); `internal_error`, `file_unreadable`, `file_upload_failed` (5xx). Retry only where `retryable` is `true`.

## Rate limits

The limit is at least **67 requests per minute per organization**, shared across every `/v1` endpoint. This is a guaranteed floor enforced per edge location, so a distributed caller can sustain more. Over the limit, you get `429`, `code: "rate_limited"`, `Retry-After: 60`. Nothing starts, and nothing is billed. Respect `Retry-After`, then back off exponentially with jitter. One batch call and one long-poll each cost one request. Both beat looping for this reason.

## Webhooks

Send `webhook: {"url": "https://…"}` on any verb, or register an endpoint with `POST /v1/webhooks` and reference it as `webhook: {"id": …}`. Deliveries are signed JWTs. Verify them against `GET /v1/webhooks/jwks.json` (public, unauthenticated, never rate limited). Deliveries do **not** include run `metadata`. Re-fetch the run by id.

## Rules that save you a debugging session

* No `/v1` run data appears in the CloudRaker app until you `POST /v1/runs/:id/keep`. This is deliberate: ephemeral by default.
* Schema inference (`hints`, no `schema`) picks its own field names and can differ between runs. Use it once to discover the shape, then pin `config.schema` or save it as an action.
* `hints` with a `schema` or an `action` is a `400`. Use `instructions` instead.
* `POST /v1/extract/batch` has a strict body: it rejects `schema` and `hints`, not ignores them. It always returns `202`. It accepts `citations`.
* Pipeline steps run in parallel over the same files. A step never consumes another step's output.
* A produced file is registered a moment after `processed`. Until then, `output.file.url` and `/output/:name` return `409 output_not_ready` (retryable, never `404`). Wait a moment and ask again.
* Reuse `fileId`s instead of URLs. The API does not fetch or parse the document again.

## Full reference

* OpenAPI: `https://docs.cloudraker.com/openapi.json` — the full gateway spec, with `POST /v1/extract/batch` and `GET /v1/runs`. It is re-exported when the surface changes. Where the two disagree, this page is right.
* Markdown index for agents: `https://docs.cloudraker.com/llms.txt`
* MCP server: `https://mcp.cloudraker.com` — see [MCP server](/paperwork/developers/mcp)
* Human-readable start: [Agent quickstart](/paperwork/developers/agent-quickstart), [Quickstart](/paperwork/developers/quickstart)