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

# Classify

**Classify decides. Split cuts.** `POST /v1/classify` is the only one of the two that calls a model: it reads the parsed text and answers with a class id you chose. `POST /v1/split` calls no model at all — it takes page ranges and cuts the PDF.

Classify accepts **any parseable file** — PDF, office, image, audio, video.

## Two granularities

| `granularity`        | What you get                                  | Pages billed                   |
| -------------------- | --------------------------------------------- | ------------------------------ |
| `document` (default) | one label for the whole file                  | the first and last window only |
| `page`               | one label per page, plus derived `segments[]` | every page                     |

Page mode is what feeds [split](/paperwork/capabilities/split). Document mode is for routing: branch a workflow, pick an extraction schema.

## Classes are descriptions, not training data

A class is `{id, description}`. There is no labelled sample set, no training step and no display name. The **description is the accuracy lever** — write it the way you would brief a new hire.

* `classes` is required: 2 to 50 entries.
* `id` matches `^[a-z0-9][a-z0-9_-]{0,63}$` and is unique. It is your only branch key.
* `description` is at most 500 characters.
* **One class must be the catch-all.** The convention is `id: "other"`. If you send none, CloudRaker injects `{"id": "other", "description": "None of the above."}` and echoes the effective config back on the run. A closed set with no exit makes the model guess.

## Quickstart — document mode

```bash title="curl"
curl -X POST https://api.cloudraker.com/v1/classify \
  -H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": { "url": "https://acme.example/scans/mail-2026-08-24.pdf" },
    "classes": [
      { "id": "invoice",  "description": "A bill from a supplier with line items and a total due." },
      { "id": "contract", "description": "A signed agreement with clauses and signature blocks." },
      { "id": "other",    "description": "None of the above." }
    ],
    "granularity": "document",
    "metadata": { "batch": "aug-24" }
  }'
```

```ts title="TypeScript"
const res = await fetch("https://api.cloudraker.com/v1/classify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CLOUDRAKER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    file: { url: "https://acme.example/scans/mail-2026-08-24.pdf" },
    classes: [
      { id: "invoice", description: "A bill from a supplier with line items and a total due." },
      { id: "contract", description: "A signed agreement with clauses and signature blocks." },
      { id: "other", description: "None of the above." },
    ],
    granularity: "document",
    metadata: { batch: "aug-24" },
  }),
});

const run = await res.json();
console.log(run.output.classId, run.output.confidence);
```

```python title="Python"
import os, requests

res = requests.post(
    "https://api.cloudraker.com/v1/classify",
    headers={"Authorization": f"Bearer {os.environ['CLOUDRAKER_API_KEY']}"},
    json={
        "file": {"url": "https://acme.example/scans/mail-2026-08-24.pdf"},
        "classes": [
            {"id": "invoice", "description": "A bill from a supplier with line items and a total due."},
            {"id": "contract", "description": "A signed agreement with clauses and signature blocks."},
            {"id": "other", "description": "None of the above."},
        ],
        "granularity": "document",
        "metadata": {"batch": "aug-24"},
    },
)

run = res.json()
print(run["output"]["classId"], run["output"]["confidence"])
```

### Example response — document mode

```json
{
  "object": "classify_run",
  "id": "clr_01K3F…",
  "status": "processed",
  "statusUrl": "/v1/runs/clr_01K3F…",
  "expiresAt": "2026-08-25T14:02:11Z",
  "file": { "id": "file_01K3F…", "name": "mail-2026-08-24.pdf" },
  "config": { "classes": [], "granularity": "document" },
  "output": {
    "granularity": "document",
    "classId": "invoice",
    "confidence": 5,
    "reasoning": "Page 1 carries 'Invoice No. 4471', a line-item table and 'Total due'.",
    "documents": [
      { "fileId": "file_01K3F…", "status": "processed", "classId": "invoice", "confidence": 5,
        "reasoning": "Page 1 carries 'Invoice No. 4471'…" }
    ]
  },
  "usage": { "pages": 3, "parse": 40 },
  "metadata": { "batch": "aug-24" }
}
```

`config.classes` echoes the **effective** class list, with the catch-all injected if you left it out.

`output.classId`, `output.confidence` and `output.reasoning` describe the **first document**, for the one-file case. `output.documents[]` is authoritative when you send `files: [...]`. This mirrors [extract](/paperwork/capabilities/extract).

## Page mode

Send `"granularity": "page"` to get a label per page and the segments derived from it.

```json
{
  "object": "classify_run",
  "id": "clr_01K3F…",
  "status": "processed",
  "file": { "id": "file_01K3F…", "name": "intake-packet.pdf" },
  "config": { "granularity": "page", "rules": { "minPages": 1 }, "classes": [] },
  "output": {
    "granularity": "page",
    "pages": [
      { "page": 1, "classId": "invoice", "documentStart": true,  "confidence": 5 },
      { "page": 2, "classId": "invoice", "documentStart": false, "confidence": 5 },
      { "page": 3, "classId": "invoice", "documentStart": false, "confidence": 5 },
      { "page": 4, "classId": "invoice", "documentStart": true,  "confidence": 4 },
      { "page": 5, "classId": "invoice", "documentStart": false, "confidence": 4 },
      { "page": 6, "classId": "other",   "documentStart": true,  "confidence": 2 }
    ],
    "segments": [
      { "startPage": 1, "endPage": 3, "classId": "invoice", "confidence": 5 },
      { "startPage": 4, "endPage": 5, "classId": "invoice", "confidence": 4 },
      { "startPage": 6, "endPage": 6, "classId": "other",   "confidence": 2 }
    ],
    "unassignedPages": []
  },
  "usage": { "pages": 6, "parse": 6 }
}
```

* `pages[]` is the model's raw answer. `page` is **1-based**.
* Boundaries come from **`documentStart`**, not from a change of class. That is what separates an invoice followed by another invoice — the most common packet shape there is. Pages 1–3 and 4–5 above are both `invoice` and are still two documents.
* `segments[]` is derived in code from `documentStart`, then a segment shorter than `rules.minPages` merges into the one before it (index 0 merges forward). `startPage`/`endPage` are 1-based inclusive, contiguous and non-overlapping.
* A segment's `confidence` is the **minimum** of its pages' — one uncertain page inside a segment is exactly where a boundary is wrong.
* `unassignedPages` is normally `[]`. It reports any page the repair pass could not place.

Page mode needs per-page structure. A source that only produced markdown answers document mode but fails page mode with `insufficient_text`.

## The pipeline is three calls

```
POST /v1/classify  { "file": {…}, "classes": […], "granularity": "page" }   → clr_…
POST /v1/split     { "file": {…}, "classifyRunId": "clr_…" }                → splits[].fileId
POST /v1/extract   { "files": [{ "id": "file_…" }, …], "schema": … }
```

**Three, not two** — on purpose. Between step 1 and step 2 you can inspect the segments, override them, or skip split entirely when the answer is "this whole file is one invoice". Route on `classId`: that is what a stable class id is for.

**If you already know the ranges, skip step 1.** Post `segments` to `/v1/split` directly and pay for no model call at all.

## Configuration

| Field            | Type                                  | What it does                                                                                      |
| ---------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `file` / `files` | `{url, name?, processing?}` or `{id}` | **Required.** Up to 100 files per call.                                                           |
| `classes`        | array of `{id, description}`, 2–50    | **Required.** The class list.                                                                     |
| `granularity`    | `document` \| `page`                  | Default `document`.                                                                               |
| `rules.minPages` | integer                               | Page mode only. Default `1`. A shorter segment merges into its neighbour.                         |
| `instructions`   | string                                | Extra guidance, e.g. "Treat a continuation sheet as part of the document before it."              |
| `pageRange`      | `{start, end}`                        | The page window to consider. Default `{ "start": 1, "end": 750 }`.                                |
| `action`         | string                                | The id or slug of a [saved config](/paperwork/capabilities/actions). Inline fields merge over it. |
| `metadata`       | object                                | Your own key/values, echoed on the run.                                                           |
| `webhook`        | `{url}` or `{id}`                     | Terminal-event delivery. See [Webhooks](/paperwork/developers/webhooks).                          |
| `ttl`            | integer seconds, 1–604800             | Default 24 hours, max 7 days.                                                                     |

The whole configuration must serialize under **16 KB**. A file with more pages than `pageRange` allows **fails** with `page_limit_exceeded` — it is never silently truncated.

There is no `minConfidence`, no `tier`, no `model` and no per-class `examples`. Put your examples in the description.

## Confidence

`confidence` is an integer **0–5**, the same scale as citation confidence everywhere else on the platform.

| Result            | What to do                                                      |
| ----------------- | --------------------------------------------------------------- |
| `confidence >= 4` | auto-process                                                    |
| `confidence <= 3` | route to a human, or sharpen the class `description` and re-run |

**A low confidence never rewrites the label.** The model's chosen class stands. "Unsure" and "sure it is `other`" have to stay distinguishable, or your routing code loses the only signal it has. You set the threshold.

`reasoning` is one short sentence of prose. It is not evidence, it is not grounded, and it carries no citations. Do not parse it.

## What you pay for

Classify bills **per page sent to the model**, and `usage` itemizes it:

* **Document mode** bills only the first and last window — that is what decides whole-file identity. `usage.pages` counts the pages actually read, never the pages in the file.
* **Page mode** bills every page in `pageRange`.
* `usage.parse` counts every parsed page. **Parsing is always billed on top of classification.**

## Save it as a config

A taxonomy is usually organization-wide. Save it once and every call shrinks to `{"file": …, "action": "mailroom"}`:

```bash
curl -X POST https://api.cloudraker.com/v1/classify/configs \
  -H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mailroom",
    "config": {
      "classes": [
        { "id": "invoice", "description": "A bill from a supplier with line items and a total due." },
        { "id": "other",   "description": "None of the above." }
      ],
      "granularity": "page"
    }
  }'
```

`GET`, `PUT` and `DELETE /v1/classify/configs/{idOrSlug}` do the rest. See [Saved configs](/paperwork/capabilities/actions).

There is **no** `/v1/split/configs`: split has nothing to configure.

## Sync vs async

Behaviour matches [extract](/paperwork/capabilities/extract#sync-vs-async). `?wait=` accepts `0` to `120` seconds, default 60. A document-mode classify of a short file returns `200` inline. A page-mode classify of a long packet returns `202` with a run id — that is the contract, not an error. Pass `?wait=0` and poll, or use a webhook.

## Errors

| Code                  | HTTP        | When                                                            |
| --------------------- | ----------- | --------------------------------------------------------------- |
| `classes_required`    | 400         | fewer than 2 classes                                            |
| `duplicate_class_id`  | 400         | a non-unique `id`                                               |
| `too_many_classes`    | 400         | more than 50                                                    |
| `config_too_large`    | 400         | the serialized config exceeds 16 KB                             |
| `page_limit_exceeded` | 400         | the file has more pages than `pageRange` allows                 |
| `capability_mismatch` | 400         | the `action` ref points at another capability                   |
| `insufficient_text`   | run failure | text yield below threshold — re-send with `"processing": "ocr"` |
| `parse_failed`        | run failure | preprocessing failed; the reason travels with it                |

## Next steps

#### [Split](/capabilities/split)

Cut a packet into one file per document.

#### [Extract](/capabilities/extract)

Pull structured data out of each child file.

#### [Saved configs](/capabilities/actions)

Save a taxonomy once and reference it by slug.

#### [Runs](/developers/runs)

Statuses, TTL, and keeping a result.