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

# Split

**Classify decides. Split cuts.** `POST /v1/split` calls **no model**. It takes page ranges — yours, or the ones a page-mode [classify](/paperwork/capabilities/classify) run produced — and extracts them as real files. It has no `classes`, no `instructions`, no `rules` and no saved config.

**Split takes PDF only, unencrypted.** Office, image, audio and video inputs fail the run with `unsupported_split_source`. Classify accepts anything parseable; split does not. The asymmetry is deliberate — timecode split has a different output shape and is not part of this verb.

## Two input forms — exactly one

```bash title="You know the ranges"
curl -X POST https://api.cloudraker.com/v1/split \
  -H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": { "id": "file_01K3F…" },
    "segments": [
      { "startPage": 1, "endPage": 3, "classId": "invoice" },
      { "startPage": 4, "endPage": 5, "classId": "invoice" },
      { "startPage": 6, "endPage": 6 }
    ],
    "materialize": true
  }'
```

```bash title="Let classify find them"
curl -X POST https://api.cloudraker.com/v1/split \
  -H "Authorization: Bearer $CLOUDRAKER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": { "id": "file_01K3F…" },
    "classifyRunId": "clr_01K3F…",
    "webhook": { "url": "https://acme.example/hooks/split" }
  }'
```

| Field           | Default | Meaning                                                                             |
| --------------- | ------- | ----------------------------------------------------------------------------------- |
| `segments[]`    | —       | Your own ranges. 1-based inclusive. `classId` is optional and echoed back. Max 100. |
| `classifyRunId` | —       | A `clr_` run. Split reads its `output.segments`.                                    |
| `materialize`   | `true`  | `false` returns the ranges and creates no files — a dry run.                        |
| `webhook`       | —       | `{url}` or `{id}`. See [Webhooks](/paperwork/developers/webhooks).                  |
| `ttl`           | 24 h    | 1 second to 7 days. The children share it.                                          |

**Send exactly one of `segments` / `classifyRunId`.** Neither is `400 segments_required`; both is `400 segments_conflict`.

Segments must ascend, must not overlap, must start at 1 or higher, and must end inside the source's page count. Gaps are fine — drop a separator page on purpose if you want to.

## Example response

```json
{
  "object": "split_run",
  "id": "spr_01K3G…",
  "status": "processed",
  "file": { "id": "file_01K3F…", "name": "intake-packet.pdf" },
  "output": {
    "classifyRunId": "clr_01K3F…",
    "splits": [
      { "index": 0, "classId": "invoice", "startPage": 1, "endPage": 3, "pageCount": 3,
        "confidence": 5, "fileId": "file_01K3H…", "fileName": "intake-packet-p1-3.pdf" },
      { "index": 1, "classId": "invoice", "startPage": 4, "endPage": 5, "pageCount": 2,
        "confidence": 4, "fileId": "file_01K3J…", "fileName": "intake-packet-p4-5.pdf" },
      { "index": 2, "classId": "other", "startPage": 6, "endPage": 6, "pageCount": 1,
        "confidence": 2, "fileId": "file_01K3K…", "fileName": "intake-packet-p6.pdf" }
    ],
    "documentIds": ["file_01K3H…", "file_01K3J…", "file_01K3K…"]
  },
  "usage": { "pages": 6, "split": 6 }
}
```

| Field                             | What it is                                                                                                                 |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `splits[].fileId`                 | The authoritative id of each child file. Feed it straight to any other verb.                                               |
| `output.documentIds[]`            | The same ids as a flat array, ready to paste into `/v1/extract`.                                                           |
| `splits[].classId` / `confidence` | **Echoed, never produced.** From the classify run, or from your own `segments`. Split has no opinion about what a page is. |
| `output.classifyRunId`            | Present only when you passed one.                                                                                          |
| `splits[].fileName`               | `{parentStem}-p{start}-{end}.pdf`, fixed. No templating.                                                                   |

Children carry `parentFileId` back to the source, inherit the parent's `metadata`, and gain `metadata.parentRunId` set to this run's id — so your correlation id survives into every downstream extract. They land in the parent's space; you do not choose it.

**One segment covering the whole file creates nothing.** It comes back with `fileId` set to the parent's id. A byte-identical copy would double the storage, the next parse and the bill for no gain.

## Three calls, and how to skip one

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

**It is three calls, and that is the point.** Between classify and split you can read the segments, correct one, or decide the packet was one document all along.

**A caller who already knows the ranges skips step 1 entirely** and posts `segments` to split. That path runs no model and bills no classification.

The 100-segment cap matches `/v1/extract`'s 100-file cap, so a split packet always fits one downstream call.

## What you pay for

Split bills **per page cut**, and it is the cheaper of the two verbs because it is deterministic byte work: no model runs. `usage` itemizes `{pages, split}`.

**The children are parsed again.** Child PDFs are byte extractions, not parsed at split time — **the first verb that consumes a child parses it.** A 400-page packet therefore pays parse 400, classify 400, split 400, then roughly 400 more parsed pages across the children before extraction starts. Run `materialize: false` first on a large packet to see the ranges before you commit to that.

## Dry runs, retries and failures

* `materialize: false` creates nothing and cannot half-fail. It is the safe first call on a large packet.
* If materialization fails part-way the run is terminal `failed` and the children already created are deleted. `output.splits` is published only on success, so you never see an orphan.
* A queue retry never creates a duplicate child.
* `POST /v1/runs/spr_…/keep` moves the parent **and every child** into a space and clears the TTL. Otherwise the children expire with the run.

## Limits

| Limit              | Value                   |
| ------------------ | ----------------------- |
| segments per split | 100                     |
| source PDF         | 50 MB                   |
| pages considered   | 750                     |
| `wait`             | 60 s default, 120 s max |

A split of a 300-page packet normally returns `202` with a run id. Pass `?wait=0` and poll, or use a webhook.

## Errors

| Code                       | HTTP        | When                                                                                        |
| -------------------------- | ----------- | ------------------------------------------------------------------------------------------- |
| `segments_required`        | 400         | neither `segments` nor `classifyRunId`                                                      |
| `segments_conflict`        | 400         | both of them                                                                                |
| `invalid_segments`         | 400         | overlapping, descending, out-of-range, or more than 100                                     |
| `classify_run_mismatch`    | 400         | the `classifyRunId` is unknown, expired, or belongs to another file                         |
| `classify_run_not_paged`   | 400         | that run used `granularity: "document"`                                                     |
| `page_limit_exceeded`      | 400         | the file has more pages than the range allows                                               |
| `unsupported_split_source` | run failure | the input is not a PDF, or it is encrypted                                                  |
| `split_budget_exceeded`    | run failure | materialization ran past its 13-minute budget — try `materialize: false`, or fewer segments |
| `parse_failed`             | run failure | preprocessing failed                                                                        |

A classify run expires with its TTL, 24 hours by default. Classify on Monday and split on Wednesday and you get `classify_run_mismatch`. Split promptly, or keep the `segments` on your side and post them explicitly.

## Next steps

#### [Classify](/capabilities/classify)

Find the boundaries before you cut.

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

Pull structured data out of each child.

#### [Files](/developers/files)

Registering, downloading and deleting files.

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

Statuses, TTL, and keeping a result.