---
name: cloudraker-api
description: >-
  Use when writing or debugging code against the CloudRaker paperwork API
  (api.cloudraker.com, the /v1 endpoints) — extracting structured data from
  documents, parsing, redacting PII, filling form PDFs, collecting e-signatures,
  running pipelines, or driving multi-step agents with human sign-off. Triggers
  on CLOUDRAKER_API_KEY, api.cloudraker.com, @cloudraker/api, the `cloudraker`
  Python package, or run ids like exr_/par_/rdr_/flr_/sgr_/plr_/agr_.
---

# CloudRaker API

One REST API over `https://api.cloudraker.com` that turns documents into data
and documents into other documents. Six verbs, one run lifecycle — plus agents,
a separate surface for multi-step work with human sign-off.

**Fetch the current contract before writing code** — this file is a snapshot:

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

Full OpenAPI: `https://docs.cloudraker.com/openapi.json` — the full gateway
spec, batch and run listing included, re-exported whenever the surface changes;
where it and this file ever disagree, this file is right. Any docs page as
Markdown: append `.md` to its URL. Docs index: `https://docs.cloudraker.com/llms.txt`.

## Auth

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

The key belongs to exactly one organization and *is* the tenant — never pass a
tenant, account, or workspace id. Read the key from the environment; never
commit it.

## The six verbs

All are `POST`, all create a run, all are synchronous by default with
`?wait=` (0–120 s, default 60). Minimal bodies:

```jsonc
// extract: document -> JSON matching your schema
// citations are OFF by default; add "citations": true to ground every field
POST /v1/extract
{ "file": { "url": "https://example.com/invoice.pdf" },
  "schema": { "type": "object", "properties": { "total": { "type": ["number", "null"] } } },
  "citations": true }

// extract, exploration mode: omit schema, add hints -> a schema is inferred and applied
POST /v1/extract
{ "file": { "url": "…" }, "hints": "It's an invoice; capture the header fields" }

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

// parse: document -> markdown + structured JSON, no schema
POST /v1/parse
{ "file": { "url": "…" } }

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

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

// sign: e-signature envelope; always 202, status needs_input
// "cc" parties never sign — they only receive the sealed PDF at completion
POST /v1/sign
{ "file": { "id": "<fileId>" }, "signers": [{ "name": "Jane Doe", "email": "jane@example.com" }],
  "cc": [{ "name": "Marc Renaud", "email": "marc.renaud@example.com" }] }

// pipeline: several capabilities over one file set, in PARALLEL (never chained)
POST /v1/pipeline
{ "files": [{ "url": "…" }],
  "steps": [{ "extract": { "schema": {} } }, { "redact": { "categories": ["ssn"] } }] }
```

Optional on every verb: `metadata` (your ids, ≤10 KB, echoed and filterable),
`webhook` ({url} or {id}), `ttl` (seconds, default 86400, max 604800), and the
`idempotency-key` header. `extract`/`redact`/`fill`/`sign` also accept
`action: "<id or slug>"` in place of inline config; inline fields override the
saved ones. `citations` (default `false`) is accepted on `/v1/extract`,
`/v1/extract/batch`, and `extract` pipeline steps.

## Files

A file reference is always one of two shapes — there is no multipart upload:

```jsonc
{ "url": "https://…", "name": "invoice.pdf", "processing": "auto" }
{ "id": "a04d6597-4e34-4a99-94ea-964c289a4c68" }
```

`processing`: `auto` | `ocr` | `simple` | `transcribe` | `transcribe_diarize`.
Single-file verbs take `file`, multi-file verbs take `files[]` (≤100); sending
both is a 400.

Persist a file with `POST /v1/files`: `{url, name?}` (the source must serve a
`Content-Length`) or `{name, mimeType}` → `uploadUrl` (15 min) that you `PUT`
the bytes to with the *identical* `Content-Type`, then poll `GET /v1/files/:id`
until `status: "ready"`. Reuse file ids instead of re-sending URLs: no re-fetch,
no re-parse, lower cost.

## 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
```

Prefixes: `exr_` extract, `par_` parse, `rdr_` redact, `flr_` fill, `sgr_` sign,
`plr_` pipeline. Ids are opaque — never parse them.

Statuses: `queued`, `processing`, `processed`, `failed`, `cancelled`, `expired`,
`needs_input`. `output` exists only at `processed`. `needs_input` = an open
signature envelope; read it via `envelopeUrl`.

`GET /v1/runs` accepts `object`, `status` (all but `queued`), `limit` (1–50,
default 20), `cursor`, and up to three `metadata.<key>=<value>` pairs. It returns
`{object:"list", data, has_more, cursor}` and is **eventually consistent** —
`GET /v1/runs/:id` is authoritative.

Runs and their inline files are purged at `expiresAt`. Sign runs are exempt while
the envelope is open. Nothing a run touches is visible in the CloudRaker app
until you `POST /v1/runs/:id/keep`.

## Agents (multi-step, human sign-off)

A separate surface from the verbs and from `/v1/runs`. An **agent** is a saved,
versioned automation: steps, the saved actions it may take, and where a person
signs off. An **agent run** (`agr_…`) is one execution, and it can wait days.

```
GET  /v1/agents                                 agents you can run
GET  /v1/agents/:id                             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
// start: 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`. `paused` (`paused.reason`) is resumable, not a failure; a
`completed` run with unfinished work carries `incomplete: true`. The run body
carries `tasks[]` (`executor: agent|human`, `status: pending|ready|in_progress|
completed|skipped`), `approvals[]` (`kind: before|output`, with the proposed
`params`/`files`), `waiting: {approvals, tasks, summary}`, `progress.tasks`,
`result`, `output.files[]` (signed ~1 h), `error`, `metadata`, `expiresAt` (~7 d).

Traps specific to this surface:

- A `queued` agent run **must be polled** — reading it is what picks it up once
  its files are prepared. A webhook alone leaves it `queued` forever.
- **No list, no cancel, no delete**, and agent runs never appear in
  `GET /v1/runs`. Keep the `agr_` id, or find runs by your own `metadata`.
- No `ttl`: files passed by URL become persistent files. `expiresAt` is a
  *deadline* — an unfinished run parks for good past it.
- Only `executor: "human"` steps are completable (`422` otherwise). Unmet
  `dependsOn`, or an already-closed step, is `409`.
- `{"decision":"reject"}` without a `note` is a `400`. Deciding the same
  approval twice is a `409`.
- Events: `agent_run.waiting`, `agent_run.approval_requested` (minimal by
  design — re-read the run for `params`), `agent_run.task_ready`,
  `agent_run.completed`, `agent_run.failed` (also carries `cancelled`/`expired`;
  trust `data.status`). Same JWT signature and JWKS as every other delivery,
  `processingId` carrying the `agr_` id.

## Extract output shape

```jsonc
{ "config": { "schema": { /* the schema actually applied */ } },
  "output": {
    "value": { "total": 1889.45, "po_number": null },
    "citations": { "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 } ] },
    "documents": [ { "fileId": "…", "status": "done", "value": { /* … */ }, "citations": { /* per document */ } } ] } }
```

`page` is 0-based, `bbox` is normalized 0–1 top-left origin, `confidence` is 0–5,
missing fields are `null`. Audio grounds with a `timecode` instead of
`page`/`bbox`.

Citations are an explicit add-on: send `"citations": true` on the request (or
turn on `grounding` on the saved action — it defaults to off). Without it the
`citations` key is **absent entirely** from the output, never an empty object.

With citations on, grounding is guaranteed-or-flagged: every filled field comes
back with at least one citation, or with a `{ "fileId": "…", "notFound": true }`
entry declaring the value is not in the documents — in which case its `value` is
`null`, never a guess. `notFound` entries carry only `fileId`. If the result
exceeds the size budget the citations are dropped and the output carries
`"citationsOmitted": true` (output level only, never per document).

## Schema dialect (extract)

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

1. Root must be `{"type": "object"}`.
2. Max nesting depth 5.
3. Rejected keywords — exactly these: `$defs`, `$ref`, `oneOf`, `anyOf`,
   `allOf`, `const`, `pattern`. Keyword positions only; a property *named*
   `pattern` is fine. Use `enum` for value constraints, `description` for
   formats.
4. Serialized schema under 64 KB.

Nullable primitives (`{"type": ["string", "null"]}`) are **recommended, never
enforced** — a non-nullable field pressures the extractor into inventing a
value instead of returning `null`. Highest-impact quality change there is. With
`citations: true`, an absent value is explicitly marked `notFound` rather than
left ambiguous.

A `description` per property is the strongest accuracy lever. `unit` controls
cardinality: `per_document` (default) | `across_documents` | `rows_per_document`.

## Errors

```json
{ "code": "invalid_request", "message": "…", "retryable": false,
  "requestId": "req_…", "docUrl": "https://docs.cloudraker.com/developers/errors#invalid_request" }
```

Branch on `code`, never on `message`. `requestId` is also the `x-request-id`
header. Retry only when `retryable` is true. 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_*` (422); `rate_limited` (429);
`internal_error`, `file_unreadable`, `file_upload_failed` (5xx).

## Rate limits

At least **67 requests/minute per organization**, shared across all of `/v1` — a
guaranteed floor enforced per edge location, so a distributed caller may sustain
more. Over it: `429`, `code: "rate_limited"`, `Retry-After: 60`; nothing started,
nothing billed. Sleep for `Retry-After`, then exponential backoff with jitter.
One batch call and one long-poll each cost a single request — both beat looping.

## Traps that cost real debugging time

- A slow run returns `202` with the run handle, never a timeout error. Poll
  `statusUrl` or use a webhook.
- Inference picks its own field names and can differ run to run. Use it once,
  then pin `config.schema` or save it as an action.
- `hints` alongside `schema` or `action` is a 400 — use `instructions` instead.
- `POST /v1/extract/batch` has a strict body (`schema`/`hints` are rejected,
  not ignored) and never runs synchronously.
- Pipeline steps run in parallel over the *original* files; a step never reads
  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 retry.
- Presigned PUT `Content-Type` must exactly equal the registered `mimeType`.
- URL registration needs the source to serve a `Content-Length`; HTML pages and
  gzip endpoints often don't — upload those instead.

## MCP

- API: `https://mcp.cloudraker.com` (bearer = the same API key, or OAuth).
- Docs: `https://docs.cloudraker.com/_mcp/server`.
