Classify

Label a document, or label every page and find where each document starts.
View as Markdown

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

granularityWhat you getPages billed
document (default)one label for the whole filethe first and last window only
pageone label per page, plus derived segments[]every page

Page mode is what feeds 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

$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" }
> }'

Example response — document mode

1{
2 "object": "classify_run",
3 "id": "clr_01K3F…",
4 "status": "processed",
5 "statusUrl": "/v1/runs/clr_01K3F…",
6 "expiresAt": "2026-08-25T14:02:11Z",
7 "file": { "id": "file_01K3F…", "name": "mail-2026-08-24.pdf" },
8 "config": { "classes": [], "granularity": "document" },
9 "output": {
10 "granularity": "document",
11 "classId": "invoice",
12 "confidence": 5,
13 "reasoning": "Page 1 carries 'Invoice No. 4471', a line-item table and 'Total due'.",
14 "documents": [
15 { "fileId": "file_01K3F…", "status": "processed", "classId": "invoice", "confidence": 5,
16 "reasoning": "Page 1 carries 'Invoice No. 4471'…" }
17 ]
18 },
19 "usage": { "pages": 3, "parse": 40 },
20 "metadata": { "batch": "aug-24" }
21}

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.

Page mode

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

1{
2 "object": "classify_run",
3 "id": "clr_01K3F…",
4 "status": "processed",
5 "file": { "id": "file_01K3F…", "name": "intake-packet.pdf" },
6 "config": { "granularity": "page", "rules": { "minPages": 1 }, "classes": [] },
7 "output": {
8 "granularity": "page",
9 "pages": [
10 { "page": 1, "classId": "invoice", "documentStart": true, "confidence": 5 },
11 { "page": 2, "classId": "invoice", "documentStart": false, "confidence": 5 },
12 { "page": 3, "classId": "invoice", "documentStart": false, "confidence": 5 },
13 { "page": 4, "classId": "invoice", "documentStart": true, "confidence": 4 },
14 { "page": 5, "classId": "invoice", "documentStart": false, "confidence": 4 },
15 { "page": 6, "classId": "other", "documentStart": true, "confidence": 2 }
16 ],
17 "segments": [
18 { "startPage": 1, "endPage": 3, "classId": "invoice", "confidence": 5 },
19 { "startPage": 4, "endPage": 5, "classId": "invoice", "confidence": 4 },
20 { "startPage": 6, "endPage": 6, "classId": "other", "confidence": 2 }
21 ],
22 "unassignedPages": []
23 },
24 "usage": { "pages": 6, "parse": 6 }
25}
  • 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

FieldTypeWhat it does
file / files{url, name?, processing?} or {id}Required. Up to 100 files per call.
classesarray of {id, description}, 2–50Required. The class list.
granularitydocument | pageDefault document.
rules.minPagesintegerPage mode only. Default 1. A shorter segment merges into its neighbour.
instructionsstringExtra 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 }.
actionstringThe id or slug of a saved config. Inline fields merge over it.
metadataobjectYour own key/values, echoed on the run.
webhook{url} or {id}Terminal-event delivery. See Webhooks.
ttlinteger seconds, 1–604800Default 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.

ResultWhat to do
confidence >= 4auto-process
confidence <= 3route 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"}:

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

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

Sync vs async

Behaviour matches extract. ?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

CodeHTTPWhen
classes_required400fewer than 2 classes
duplicate_class_id400a non-unique id
too_many_classes400more than 50
config_too_large400the serialized config exceeds 16 KB
page_limit_exceeded400the file has more pages than pageRange allows
capability_mismatch400the action ref points at another capability
insufficient_textrun failuretext yield below threshold — re-send with "processing": "ocr"
parse_failedrun failurepreprocessing failed; the reason travels with it

Next steps