> 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`** and it has everything it needs to write a working integration: auth, the request and error envelopes, all six verbs, the run lifecycle, the schema dialect, and the limits.

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

Every page on this site has the same `.md` twin, and `https://docs.cloudraker.com/llms.txt` indexes them all. This page is the one to start from.

## 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 *is* the tenant, so no tenant, account, or workspace id is ever passed. Keys are created in the app under Admin → API keys and shown once. Staging (`https://api.staging.raker.one`) and development (`https://api.dev.raker.one`) are separate environments with separate keys.

## The six verbs

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

```jsonc
// POST /v1/extract — document -> JSON matching your schema, with citations
{ "file": { "url": "https://example.com/invoice.pdf" },
  "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 action 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 from source documents
{ "template": { "id": "<templateId>" }, "files": [{ "id": "<fileId>" }], "review": "none", "output": "flattened" }

// 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.

`extract`, `redact`, `fill`, and `sign` also accept `action: "<id or slug>"` instead of inline config; inline fields win over the saved ones. Save one with `POST /v1/actions {capability, name, config}`; discover configurable shapes with `GET /v1/actions/catalog`.

## File references

Anywhere a file is taken, the value is 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). Sending both is a `400`.

Register a persistent file with `POST /v1/files` — either `{url, name?}` (the source **must** serve a `Content-Length`) or `{name, mimeType}`, which returns an `uploadUrl` valid 15 minutes that you `PUT` the bytes to 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 human is required — a fill review (`tasks[]`) or an open signature envelope (`envelopeUrl`).

A synchronous call that doesn't 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}` and is **eventually consistent** — `GET /v1/runs/:id` is authoritative.

## 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 },                 // alias of documents[0].value
    "citations": {                                  // 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 }]
    },
    "documents": [{ "fileId": "…", "name": "invoice.pdf", "status": "done", "value": {}, "citations": {} }]
  }
}
```

`page` is 0-based; `bbox` is normalized `0`–`1` with a top-left origin; `confidence` is `0`–`5`. A field the document doesn't contain comes back `null`, which is why every property should be declared nullable.

## 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 fine). Constrain values with `enum`; describe formats in `description`.
4. The serialized schema must stay under 64 KB.

Making every primitive nullable — `{"type": ["string", "null"]}` — is **recommended, never enforced**: a non-nullable field pressures the extractor into inventing a value instead of returning `null`. It is the single highest-impact change to extraction quality.

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

At least **67 requests per minute per organization**, shared across every `/v1` endpoint — a guaranteed floor enforced per edge location, so a distributed caller may sustain more. Over it: `429`, `code: "rate_limited"`, `Retry-After: 60`. Nothing is started and nothing is billed. Respect `Retry-After`, then back off exponentially with jitter. One batch call and one long-poll each cost a single request, which is why both beat looping.

## 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 verified against `GET /v1/webhooks/jwks.json` (public, unauthenticated, never rate limited). Run `metadata` is **not** included in deliveries — re-fetch the run by id.

## Rules that save you a debugging session

* Nothing a `/v1` run touches appears in the CloudRaker app until you `POST /v1/runs/:id/keep`. That 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 — `schema` and `hints` are rejected, not ignored — and always returns `202`.
* 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 re-sending URLs: the document is neither re-fetched nor re-parsed.

## Full reference

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