Skip to main content
v3 replaces v2’s file-centric surface with three first-class resources — workflows, document packets, and runs. The extraction engine, the results envelope, authentication, and the error envelope are unchanged; what moved are the paths, the identifiers, and the read model.
v2 keeps working through its deprecation window. Every /v2/ response now carries Deprecation and Sunset headers (RFC 8594) announcing the retirement date. Nothing breaks today — but new integrations should start on v3, and existing ones should plan the move. See Versioning & deprecation for how to alert on these headers automatically. The v2 deprecation timeline has the exact window dates and the per-route successor Link headers.

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

v2v3What changed
POST /v2/workflows/POST /v3/workflows/Same typed-graph body.
GET /v2/workflows/GET /v3/workflows/Keyset pagination.
GET /v2/workflows/{id}/GET /v3/workflows/{workflow_id}/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}/There is no separate /definition/ endpoint in v3 — the workflow read is the definition.
PUT /v2/workflows/{id}/PATCH /v3/workflows/{workflow_id}/PUT → PATCH with persistent_id echo.
DELETE /v2/workflows/{id}/DELETE /v3/workflows/{workflow_id}/Unchanged semantics.

Uploading documents

v2v3What changed
POST /v2/workflows/{id}/files/POST /v3/workflows/{workflow_id}/upload/Returns document_packet_id instead of file_collection_id. Accepts 1–10 files as one packet. Supports Idempotency-Key.
POST /v2/workflows/{id}/run/POST /v3/workflows/{workflow_id}/upload/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/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/Flat path — the packet knows its workflow, so no workflow_id needed. Every call creates a new run.
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.

Reading packets and results

v2v3What changed
GET /v2/workflows/{id}/files/GET /v3/workflows/{workflow_id}/document-packets/Keyset-paginated; the single-packet read carries latest_run_id.
GET /v2/workflows/{id}/files/{collection_id}/results/GET /v3/runs/{run_id}/No more 412-polling — status and results come back inline on a flat run read.
GET /v2/workflows/{id}/runs/GET /v3/workflows/{workflow_id}/runs/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}/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 namev3 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}/ 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. Only the read path moved.
# 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_ids), 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.

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.

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).
  • Results envelope — same parse / classifications / splits / extractions sections (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).
  • The typed-graph workflow bodyPOST /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_ids.
  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.