> ## Documentation Index
> Fetch the complete documentation index at: https://docs.case.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Assemble PDF Volumes

> Build indexed, Bates-stamped PDF volumes from page ranges of vault PDFs

`POST /vault/:vaultId/assemblies` builds new PDF volumes from pages of PDFs that
are already in a vault. You send a manifest that says which pages go where;
Case.dev assembles them exactly as written. There is no AI in this step: the
same sources and manifest always produce byte-identical volumes.

Use it to turn a large record set into a production:

* reorder and combine pages from many source PDFs
* split the result into volumes
* add a bookmark outline to each volume
* stamp continuous Bates numbers across every volume

> [!NOTE]
>
> PDF assembly is in early access. Requests from organizations that are not enabled return `403`.
> Contact support to enable it.

## Create an assembly

```bash title="cURL" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl -X POST "https://api.case.dev/vault/$VAULT_ID/assemblies" \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: matter-123-production-1" \
  -d '{
    "sources": [
      { "objectId": "obj_records_a" },
      { "objectId": "obj_records_b", "rendition": "original" }
    ],
    "volumes": [
      {
        "filename": "Medical-Records-Vol-01.pdf",
        "title": "Medical Records, Volume 1",
        "pages": [
          { "source": 0, "start": 1, "end": 40 },
          { "source": 1, "start": 12, "end": 12 }
        ],
        "outline": [
          {
            "title": "2024-03-02 | St. Mary | Dr. Lee | Labs",
            "page": 1,
            "children": [{ "title": "Lipid panel", "page": 4 }]
          },
          { "title": "2024-03-09 | St. Mary | Dr. Lee | Discharge", "page": 41 }
        ]
      },
      {
        "filename": "Medical-Records-Vol-02.pdf",
        "pages": [{ "source": 1, "start": 1, "end": 11 }]
      }
    ],
    "bates": { "prefix": "TSCAN", "padTo": 6, "legend": "CONFIDENTIAL" },
    "clientReference": "matter-123-production-1"
  }'
```

```json title="202 Response" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "id": "asm_6f1c2a9b0d3e4f5a6b7c8d9e",
  "status": "queued",
  "volumes": [
    { "objectId": "obj_vol_01", "filename": "Medical-Records-Vol-01.pdf" },
    { "objectId": "obj_vol_02", "filename": "Medical-Records-Vol-02.pdf" }
  ],
  "clientReference": "matter-123-production-1"
}
```

Each volume is reserved as a vault object right away, with `ingestionStatus: "processing"`. Use
the returned `objectId`s to track the volumes.

## The manifest

### Sources

`sources` lists the vault PDFs to draw pages from. Page ranges refer to a source by its
**index in this list**, starting at `0`. Each object may appear once.

| Field | Description |
| - | - |
| `objectId` | A PDF object in this vault |
| `rendition` | `searchable_pdf` (default) uses the OCR'd copy made at ingestion. `original` uses the uploaded file. |

A `searchable_pdf` source must have finished ingestion. If its searchable copy does not exist
yet, the request fails with `409` and code `SEARCHABLE_RENDITION_MISSING`. Ingest the source
first, or request `original`.

### Volumes

Each volume becomes one output PDF.

| Field | Description |
| - | - |
| `filename` | Output filename, ending in `.pdf`. Unique within the request. |
| `title` | Optional PDF document title |
| `pages` | Page ranges in output order |
| `outline` | Optional bookmark tree |

Page ranges are `{ "source", "start", "end" }`. Pages are **1-based and inclusive**, so
`{ "source": 0, "start": 1, "end": 3 }` is the first three pages of the first source. Ranges can
come from any source in any order, and a page can appear more than once; each copy gets its own
Bates number.

### Outline

`outline` is a tree of bookmarks. Each entry has a `title`, a `page`, and optional `children`.
`page` is **1-based within that volume**, not within a source. Outlines can nest up to 8 levels.
Top-level entries open expanded and deeper entries start collapsed.

> [!WARNING]
>
> Source bookmarks, named destinations, page labels and form fields are **not** carried over. The
> outline you send is the only outline in the output. Form fields still render, but they are no
> longer fillable.

Internal links between pages that are both in the same volume keep working and point at the new
page. Links to pages that were left out, or that landed in another volume, are removed. Web links
are kept.

### Bates numbering

`bates` numbers every output page once, continuing from one volume to the next.

| Field | Default | Description |
| - | - | - |
| `prefix` | `""` | Up to 40 characters |
| `suffix` | `""` | Up to 20 characters |
| `start` | `1` | First number |
| `padTo` | `0` | Zero-pad numbers to this many digits (up to 12) |
| `position` | `bottom_right` | `bottom_right`, `bottom_center` or `bottom_left` |
| `fontSize` | `9` | 6 to 14 points. Long labels shrink to fit, down to 6 points. |
| `legend` | | Optional line printed above each label, such as `CONFIDENTIAL` |

Bates text must be printable ASCII. Labels are drawn on the page's visible area along the edge a
reader sees as the bottom, including rotated and cropped pages.

### Other options

| Field | Description |
| - | - |
| `linearize` | `true` optimizes each volume for fast web viewing. Default `false`. |
| `clientReference` | Your own identifier, returned in responses and events |

## Limits

| Limit | Value |
| - | - |
| Sources | 1,000 |
| Total source size | 2 GB |
| Output pages, across all volumes | 20,000 |
| Volumes | 100 |
| Outline depth | 8 levels |
| Outline entries, across all volumes | 50,000 |

Each assembly has 30 minutes to finish.

## Track completion

Subscribe to `vault.assembly.completed` and `vault.assembly.failed` (see
[Vault Webhooks](https://docs.case.dev/vault/webhooks.md)). Event payloads carry IDs and counts only, never titles or
filenames.

```json title="vault.assembly.completed data" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "vaultId": "vault_123",
  "assemblyId": "asm_6f1c2a9b0d3e4f5a6b7c8d9e",
  "volumeObjectIds": ["obj_vol_01", "obj_vol_02"],
  "outputPageCount": 52,
  "clientReference": "matter-123-production-1"
}
```

You can also read an assembly at any time:

```bash title="cURL" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
curl "https://api.case.dev/vault/$VAULT_ID/assemblies/asm_6f1c2a9b0d3e4f5a6b7c8d9e" \
  -H "Authorization: Bearer $CASEDEV_API_KEY"
```

```json title="200 Response" theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{
  "id": "asm_6f1c2a9b0d3e4f5a6b7c8d9e",
  "vaultId": "vault_123",
  "status": "completed",
  "volumes": [
    {
      "objectId": "obj_vol_01",
      "filename": "Medical-Records-Vol-01.pdf",
      "pageCount": 41,
      "sizeBytes": 18234511,
      "sha256": "9f2c…",
      "batesStart": "TSCAN000001",
      "batesEnd": "TSCAN000041"
    }
  ],
  "timings": {
    "acceptedAt": "2026-11-06T18:00:00.000Z",
    "workerStartedAt": "2026-11-06T18:00:04.512Z",
    "completedAt": "2026-11-06T18:02:31.904Z",
    "workerElapsedMs": 147392
  }
}
```

`status` is `queued`, `processing`, `completed` or `failed`. Volumes are published together: either
every volume is available or none is.

## After completion

Completed volumes have `ingestionStatus: "uploaded"`. They can be downloaded, but they are **not
indexed for search automatically**. To make a volume searchable, call
[`POST /vault/:vaultId/ingest/:objectId`](https://docs.case.dev/vault/upload).

If an assembly fails, its reserved volumes are marked `failed`.

## Errors

A failed assembly reports `error`:

```json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{ "code": "SOURCE_ENCRYPTED", "message": "A source PDF requires a password", "retryable": false, "stage": "open", "sourceIndex": 1 }
```

| Code | Retry? | Meaning |
| - | - | - |
| `SOURCE_ENCRYPTED` | No | A source needs a password to open |
| `SOURCE_UNREADABLE` | No | A source is not a readable PDF |
| `SOURCE_PAGE_OUT_OF_RANGE` | No | A page range is past the end of its source |
| `OUTPUT_VALIDATION_FAILED` | Yes | An assembled volume failed its own check |
| `ASSEMBLY_OUTPUT_INVALID` | Yes | An uploaded volume did not match what was assembled |
| `ASSEMBLY_WORKER_TIMEOUT` | Yes | The assembly did not finish in 30 minutes |
| `ASSEMBLY_DISPATCH_FAILED` | Yes | The assembly could not be started |
| `ASSEMBLY_WORKER_FAILED` | Yes | Any other failure |

`sourceIndex` and `volumeIndex` point into your manifest's `sources` and `volumes`.

Retry a failed assembly with a **new** `Idempotency-Key`. Sending the same key and body again returns
the original assembly; the same key with a different body returns `409`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.