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

# Migrating from v2

> Path-by-path mapping from the v2 surface to v3, plus the semantic changes: keyset pagination, inline run status, persistent_id round-tripping, and idempotency.

v3 replaces v2's file-centric surface with three first-class resources — [workflows](/concepts/workflows), [document packets](/concepts/document-packets), and [runs](/concepts/runs-and-results). The extraction engine, the results envelope, authentication, and the error envelope are unchanged; what moved are the paths, the identifiers, and the read model.

<Info>
  **v2 keeps working through its deprecation window.** Every `/v2/` response now carries `Deprecation` and `Sunset` headers ([RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594)) announcing the retirement date. Nothing breaks today — but new integrations should start on v3, and existing ones should plan the move. See [Versioning & deprecation](/api-reference/introduction#versioning--deprecation) for how to alert on these headers automatically. The [v2 deprecation timeline](/api-reference/v2-migration) has the exact window dates and the per-route successor `Link` headers.
</Info>

***

## The one-sentence version

Where v2 said *file* or *collection*, v3 says **document packet**; where v2 made you poll a nested `/results/` sub-path, v3 gives every execution its own **run** you read flat — status and results in one `200`.

***

## Path-by-path mapping

### Workflows

| v2                                   | v3                                                                                | What changed                                                                                |
| ------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `POST /v2/workflows/`                | [`POST /v3/workflows/`](/api-reference-v3/workflows/create)                       | Same typed-graph body.                                                                      |
| `GET /v2/workflows/`                 | [`GET /v3/workflows/`](/api-reference-v3/workflows/list)                          | [Keyset pagination](#pagination-offset-to-keyset).                                          |
| `GET /v2/workflows/{id}/`            | [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get)             | Now returns the **full typed graph** inline, with a `persistent_id` on every field.         |
| `GET /v2/workflows/{id}/definition/` | folded into [`GET /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/get) | There is no separate `/definition/` endpoint in v3 — the workflow read *is* the definition. |
| `PUT /v2/workflows/{id}/`            | [`PATCH /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/update)        | [PUT → PATCH with `persistent_id` echo](#put-to-patch-field-identity-round-trips).          |
| `DELETE /v2/workflows/{id}/`         | [`DELETE /v3/workflows/{workflow_id}/`](/api-reference-v3/workflows/delete)       | Unchanged semantics.                                                                        |

### Uploading documents

| v2                                             | v3                                                                                                 | What changed                                                                                                                                        |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v2/workflows/{id}/files/`               | [`POST /v3/workflows/{workflow_id}/upload/`](/api-reference-v3/workflows/upload)                   | Returns `document_packet_id` instead of `file_collection_id`. Accepts 1–10 files as **one packet**. Supports [`Idempotency-Key`](#idempotency-new). |
| `POST /v2/workflows/{id}/run/`                 | [`POST /v3/workflows/{workflow_id}/upload/run/`](/api-reference-v3/workflows/upload-and-run)       | Returns a `run_id` — the handle you poll — instead of a collection id.                                                                              |
| `POST /v2/workflows/{id}/files/from-url/`      | [`POST /v3/workflows/{workflow_id}/upload/from-url/`](/api-reference-v3/workflows/upload-from-url) | Takes 1–10 HTTPS URLs; the import is **all-or-nothing** and the response is final (no pending fetch state to reason about).                         |
| `POST /v2/workflows/{id}/files/{file_id}/run/` | [`POST /v3/document-packets/{document_packet_id}/run/`](/api-reference-v3/document-packets/run)    | Flat path — the packet knows its workflow, so no `workflow_id` needed. Every call creates a **new run**.                                            |

<Note>
  v2's `text` form field (plain text instead of a file) has no dedicated v3 counterpart — upload the text as a small `.txt` file in the multipart `files` field instead.
</Note>

### Reading packets and results

| v2                                                      | v3                                                                                               | What changed                                                                                                           |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| `GET /v2/workflows/{id}/files/`                         | [`GET /v3/workflows/{workflow_id}/document-packets/`](/api-reference-v3/document-packets/list)   | Keyset-paginated; the single-packet read carries `latest_run_id`.                                                      |
| `GET /v2/workflows/{id}/files/{collection_id}/results/` | [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get)                                           | [No more 412-polling](#results-polling-412-to-inline-status) — status and results come back inline on a flat run read. |
| `GET /v2/workflows/{id}/runs/`                          | [`GET /v3/workflows/{workflow_id}/runs/`](/api-reference-v3/runs/list)                           | Keyset-paginated; items reference `document_packet_id` and are followed up via `GET /v3/runs/{run_id}/`.               |
| `DELETE /v2/files/{collection_id}/`                     | [`DELETE /v3/document-packets/{document_packet_id}/`](/api-reference-v3/document-packets/delete) | Same semantics: deletes the packet, its files, and its results.                                                        |

***

## Identifier renames

v2's `file_id` / `collection_id` / `file_collection_id` all collapse into one identifier:

| v2 name                                                                   | v3 name              |
| ------------------------------------------------------------------------- | -------------------- |
| `file_collection_id`, `collection_id`, `file_id` (as the runnable handle) | `document_packet_id` |

A v2 collection **is** a v3 document packet — same underlying object, one name. Per-file ids still exist inside a packet (`files[].id` on packet responses) for the rare case where you need to address one file of a multi-file packet. Path parameters are always snake\_case: `workflow_id`, `document_packet_id`, `run_id`.

***

## Semantic changes

### Results polling: 412 to inline status

v2's results read returned `412 Precondition Failed` while the extraction was in flight, so clients had to treat an error status as "not yet". In v3, [`GET /v3/runs/{run_id}/`](/api-reference-v3/runs/get) **always returns `200`** while the run exists:

* `status` tells you where the run is: `queued`, `in_progress`, `processed`, `error`, or `cancelled`.
* `results` is `null` until `status` reaches `processed`, then carries the results envelope inline.
* A run that ended in `error` or `cancelled` stays readable with `results: null`.

The results envelope itself — `parse`, `classifications`, `splits`, `extractions` — is **byte-compatible** with v2's; see [Response formats](/api-reference/response-formats). Only the read path moved.

```python theme={null}
# v2: poll /results/, treat 412 as "not ready"
r = requests.get(f"{BASE}/v2/workflows/{wf}/files/{col}/results/", headers=h)
if r.status_code == 412:
    ...retry...

# v3: poll the run, branch on status
r = requests.get(f"{BASE}/v3/runs/{run_id}/", headers=h)
run = r.json()
if run["status"] == "processed":
    results = run["results"]
```

### PUT to PATCH: field identity round-trips

v2 edited workflows with `PUT /v2/workflows/{id}/`, and the round-trippable graph lived on a separate `GET .../definition/`. v3 folds both into the workflow resource:

1. `GET /v3/workflows/{workflow_id}/` returns the workflow with its graph inline — every field carries its server-assigned `persistent_id`.
2. Mutate the graph, keep only `{name, description, nodes, edges}` (the read-only `id`, `created_at`, `updated_at` are rejected with `422`), and send it back via `PATCH /v3/workflows/{workflow_id}/`.
3. **Echo each field's `persistent_id` unchanged — including across renames** — and the field keeps its identity: analytics history, ground truth, and quality metrics stay attached.
4. The PATCH response echoes the stored graph back in the exact GET shape (new fields included, with their freshly assigned `persistent_id`s), so you can edit and PATCH again.

Omit `persistent_id` only for genuinely new fields — the server assigns one. A field without `persistent_id` is always treated as new, so omitting it on an existing field replaces that field with a fresh one and detaches its history. An echoed `persistent_id` that doesn't match any field in the current version is rejected with `400`. There is no `PUT` in v3.

### Pagination: offset to keyset

v2 lists used `?page=` / `?page_size=` with a `count` total. v3 lists return `{items, next_cursor}`:

* Pass `?cursor=<next_cursor>` to fetch the next page; stop when `next_cursor` is `null`.
* `?limit=` caps at 100 — values above are rejected with `400`, not clamped.
* There are **no totals or page numbers**. If you rendered `count` in a UI, switch to cursor-driven "load more" semantics.

See [Pagination](/api-reference-v3/introduction#pagination).

### Idempotency (new)

The packet-creating and run-triggering POSTs accept an `Idempotency-Key` header. Retrying with the same key replays the original response instead of creating a duplicate packet or billing a second extraction. v2 had no equivalent — if you built retry-dedup logic client-side, you can delete it. See [Idempotency](/api-reference-v3/introduction#idempotency).

### Version header

Every `/v3/` response is stamped `X-API-Version: 3.0.0` (v2 responses say `2.0.0` and additionally carry `Deprecation` / `Sunset`). If you tag metrics by API version, the header is the reliable source.

***

## What did *not* change

* **Authentication** — same API keys, same `Authorization: Bearer` header.
* **Error envelope** — same `{error, detail, error_code, retryable, request_id}` shape ([Errors](/api-reference/errors)).
* **Results envelope** — same `parse` / `classifications` / `splits` / `extractions` sections ([Response formats](/api-reference/response-formats)).
* **Rate limiting** — same two-tier limits and `x-ratelimit-*` headers.
* **Webhooks** — existing webhook subscriptions keep firing; management stays on the current endpoints ([Webhooks](/api-reference/webhooks/overview)).
* **The typed-graph workflow body** — `POST /v3/workflows/` accepts the same `{name, description, nodes, edges}` shape as `POST /v2/workflows/`.

***

## Migration checklist

1. Swap base paths `/v2/` → `/v3/` per the tables above; rename `file_id` / `collection_id` variables to `document_packet_id`.
2. Replace `/results/` polling (412-tolerant) with `GET /v3/runs/{run_id}/` polling on `status`.
3. Replace `PUT` workflow edits with GET → mutate → `PATCH`, preserving `persistent_id`s.
4. Replace `page`/`page_size` pagination with `cursor`/`limit`; drop any use of `count`.
5. Add an `Idempotency-Key` to upload and run-trigger POSTs if you retry on timeouts.
6. Wire an alert on the `Sunset` header so your v2 traffic can't outlive the window unnoticed — see [the header reference](/api-reference/introduction#versioning--deprecation).
