Skip to main content

Run a Spec

Upload documents and run a Spec pipeline in a single call with POST /v1/run: mixed file and URL inputs, optional batch_id and metadata tagging, 202 with a poll URL.

The Run endpoint, POST /v1/run, is the one-call front door for Spec pipelines: send one or more documents together with a spec_id, and the platform ingests every input, waits for OCR, compiles the Spec's saved rail into a pipeline, and starts it — extraction, resolution, validation, and delivery exactly as composed in the Spec editor. It replaces the two-step "upload documents, then POST /v1/pipelines" flow for integrations that push files as they arrive.

Inputs are flexible: attach up to 20 files as multipart parts, pass remote file_urls for documents the platform should download, or mix both in one call. At least one input is required. The endpoint always answers 202 Accepted immediately with a run_id and a poll_url — ingestion and the pipeline run continue asynchronously, so the caller never blocks on OCR or LLM work.

Two optional tagging fields ride along and are stamped on every document the call ingests: batch_id, an opaque grouping key for correlating the run with an external job or batch, and metadata, a flat key-value object for anything else the caller wants to carry through. Both are echoed back by GET /v1/run/:id, and both surface on the ingested documents (batch_id / metadata on the document reads).

For a multi-file call, file_metadata tags individual inputs instead of stamping the same bag on every document: a JSON string keyed by each file's uploaded filename (or the exact file_urls entry), each value a flat object with the same shape and limits as metadata. Per file, its bag is merged over the call-level metadata — per-file wins on a shared key — and the resolved, per-input EFFECTIVE bag is what actually lands on the document and is echoed back in documents[].metadata on both this response and GET /v1/run/:id.

The 202 response carries a documents[] array — one entry per input, in the order you sent them — so you always know which platform document each upload became. Each entry has document_id, filename, size_bytes, and source (file or file_url). document_id is the canonical document: for a byte-identical re-upload it resolves to the existing document already on the platform rather than minting a new one, and the entry additionally carries deduplicated: true plus linked_document_id, the id of the thin per-upload link row created for this call. A first-time upload carries deduplicated: false and no linked_document_id.

The ingestion_target field controls how deep ingestion goes after OCR. The default, ingest, makes every document askable: once a document's OCR markdown is ready, Stage-2 field capture runs asynchronously in the background, extracting every field into the workspace corpus that POST /v1/ask answers over and feeding the field registry. Capture follows the answers, it never delays them: the Spec pipeline starts the moment OCR markdown is ready, without waiting for capture. Pass extract to restore markdown-only ingestion: no background capture runs, and the documents do not join the askable corpus.

Unlike POST /v1/process, this endpoint does not deduplicate submissions — every call starts a fresh run. Byte-identical documents are still content-deduplicated at the ingestion layer (the run reads the canonical document's data).
POST/v1/run

Multipart form fields

spec_id*uuidThe Spec whose saved rail runs. Find it in the Spec editor URL or via GET /v1/schemas.
filesfile[]Zero or more document files (max 20 per call; per-file size capped by your tier). At least one of files / file_urls is required.
file_urlsstring[]Zero or more remote document URLs the platform downloads (SSRF-guarded).
namestringOptional display name for the run (max 200 chars).
batch_idstringOptional opaque grouping key (max 200 chars), stamped on every ingested document and echoed by GET /v1/run/:id.
metadatastringOptional JSON string of a FLAT object { key: string | number | boolean | null }. Nested objects/arrays are rejected 400. Limits: ≤50 keys, key ≤128 chars, string value ≤1024 chars. Persisted on the run and stamped on every ingested document.
file_metadatastringOptional JSON string of a map { inputKey: { key: string | number | boolean | null } } for tagging individual files in a multi-file call. inputKey is the file's uploaded filename (matching what documents[] echoes back) or the exact file_urls entry. Per key, the file's bag is merged over the call-level metadata — per-file wins, and null overrides rather than deletes. A key matching no input 400s as invalid_metadata; a key matching several same-named files applies to all of them. Capped at 20 entries and 64 KB of decoded JSON.
pipeline_modestringOptional: "new" or "append". Overrides the Spec's configured pipeline mode for this call. append adds the documents to the newest eligible existing pipeline on the Spec instead of creating one, so bulk uploads accumulate into a single growing table. Omitted → the Spec's own setting (default new).
dispatchbooleanSet false to skip every Dispatch node of the Spec for this request: nothing is delivered for its documents and each node records a skipped dispatch. See Run Delivery Controls. Default: true
bindingsstringOptional JSON string { dispatchNodeId: destinationId | null } (max 64 entries). A destination id re-points every binding of that node for this request; null skips the node. Combining dispatch=false with a non-null value is a 400 invalid_delivery_controls.
ingestion_targetstringingest (default) or extract. ingest kicks async Stage-2 field capture after OCR so the documents become askable via POST /v1/ask; the Spec pipeline starts on the OCR markdown either way and never waits for capture. extract is markdown-only: no capture, documents are not askable. Default: ingest

Append mode

By default every /v1/run call creates its own pipeline. When the Spec's Source stage sets pipeline mode: append (or the call passes pipeline_mode=append), the platform instead appends the call's documents to the newest eligible existing pipeline for the Spec — so pushing 1,000 documents over many calls produces one growing table instead of one pipeline per call. A pipeline is eligible while its configuration still matches the Spec (schema fields, policies, validation gates, assembly and review config) and it is under the append size cap; a config edit or a full pipeline simply rotates to a fresh pipeline, which becomes the next calls' append target. No call ever fails because appending was not possible.

Under append mode, completion is per request, not per pipeline: the run.completed webhook fires when *this request's own documents* finish (its structured_data and records[] are scoped to them, and the payload carries the request's run_id), and GET /v1/run/:id reports status and progress for the request's documents only. The shared pipeline fires no pipeline-level run.* webhook of its own. Per-document document.structured deliveries are unaffected.

Append mode changes only how runs target pipelines — extraction, resolution, validation, and delivery behave identically. Set it once on the Spec's Source stage for connector-style integrations that push files continuously; use the per-call pipeline_mode override to force a fresh pipeline (e.g. a clean monthly table) without touching the Spec.

curl

curl -s -X POST https://api.talonic.com/v1/run \
  -H "Authorization: Bearer tlnc_your_api_key" \
  -F "spec_id=1fc7807e-e1aa-4504-b796-5709986e78ed" \
  -F "batch_id=ERP-2026-07-14-001" \
  -F 'metadata={"source_system":"sap","priority":1}' \
  -F 'file_metadata={"invoice-0847.pdf":{"cost_center":"AP-14"}}' \
  -F "files=@invoice-0847.pdf" \
  -F "file_urls=https://example.com/contracts/msa-2026.pdf"

Response

Response fields (202)

run_iduuidThe run request id — the polling handle.
spec_iduuidThe Spec being run.
statusstringAlways processing on acceptance.
input_countintegerTotal inputs accepted (files + file_urls).
poll_urlstringRelative URL to poll: /v1/run/:id.
documents[]arrayOne entry per input, in input order.
documents[].document_iduuidThe canonical document id — the id output rows (webhook records[], data products) carry for this document.
documents[].filenamestring | nullThe uploaded filename (file input), or the acquired document's filename (file_url input).
documents[].size_bytesinteger | nullFile size in bytes.
documents[].sourcestring"file" or "file_url" — which input kind produced this document.
documents[].deduplicatedbooleantrue when this upload content-matched an existing document; document_id then points at that existing (canonical) document.
documents[].linked_document_iduuidPresent only when deduplicated is true — the id of the thin link row created for this specific upload.
documents[].metadataobjectThis input's EFFECTIVE metadata for this request — the call-level metadata merged with its file_metadata entry (per-file wins). Present only when set. Under byte-identical dedup this can differ from GET /v1/documents/:id's metadata, which keeps the canonical document's tags from its first submission — this echo is the authoritative source for what THIS call tagged.
warnings[]arrayPresent only when delivery controls produced warnings: { code, message } with code no_binding_for_node, node_not_in_live_pipeline, or node_not_on_published_version. The run is accepted either way.

Response (202)

{
  "run_id": "a8716d18-978d-4d19-8ca5-8b3784ca857c",
  "spec_id": "1fc7807e-e1aa-4504-b796-5709986e78ed",
  "status": "processing",
  "input_count": 2,
  "poll_url": "/v1/run/a8716d18-978d-4d19-8ca5-8b3784ca857c",
  "documents": [
    {
      "document_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
      "filename": "invoice-0847.pdf",
      "size_bytes": 184320,
      "source": "file",
      "deduplicated": false,
      "metadata": { "source_system": "sap", "priority": 1, "cost_center": "AP-14" }
    },
    {
      "document_id": "9f1e2d3c-4b5a-6789-0123-456789abcdef",
      "filename": "msa-2026.pdf",
      "size_bytes": 92160,
      "source": "file_url",
      "deduplicated": true,
      "linked_document_id": "b7a6c5d4-e3f2-1098-7654-3210fedcba98"
    }
  ]
}
For push-style consumption, subscribe to the run.completed / run.failed webhooks (POST /v1/webhooks), or configure a delivery binding on the document.structured signal to receive each document's structured fields as its own webhook payload the moment it finishes.

Errors

Error responses

400missing_spec_id / no_input / invalid_metadata / invalid_delivery_controlsspec_id absent or malformed, no file or file_url supplied, metadata is not a valid flat JSON object, file_metadata fails validation (a bad per-file bag, the merged bag exceeding the caps, or a key that matches no file/file_url — the error names the offending key), or dispatch/bindings fail validation (see Run Delivery Controls).
401unauthorizedMissing or invalid API key.
402insufficient_creditsCredit balance too low for the ingest work — no documents are ingested and no run is created.
413file_too_largeA file exceeds your tier's per-file size cap.
429rate_limit_exceededDaily submission quota for the run namespace exhausted.

Frequently asked questions

How is /v1/run different from /v1/pipelines?+
POST /v1/pipelines runs a Spec over documents that already exist on the platform; POST /v1/run ingests the documents AND runs the Spec in the same call. Use /v1/run when your integration pushes files as they arrive, and /v1/pipelines when documents were uploaded earlier (e.g. via connectors or /v1/sources).
How do I get the results?+
The primary path is the Spec's Dispatch nodes: each node decides when results ship (on run completion, per document as a stage finishes, or once per request) and its bindings deliver them to your destinations. You can also poll GET /v1/run/:id/results, subscribe to the run.completed webhook, or bind the document.structured signal to receive each document's structured fields as it finishes.
Can I stop one request from delivering, or send it somewhere else?+
Yes. Pass dispatch=false to skip every Dispatch node for this request, or bindings={"<nodeId>": "<destinationId>"} to re-point a node for this request only (null skips that node). The Spec and its bindings are not changed. See Run Delivery Controls.
What are batch_id and metadata for?+
They are caller-owned tags. batch_id is an opaque grouping key (your ERP job number, import batch, ticket id); metadata is a flat key-value object for anything else. Both are stamped on every document the call ingests and echoed back when polling the run, so you can correlate platform output with your own systems without a lookup table.
Does polling count against my rate limit?+
Polling GET /v1/run/:id is metered under the general read namespace, not the run submission namespace — frequent polling never burns your daily submission quota.
What happens if I run out of credits?+
The call is rejected up front with 402 insufficient_credits before any document is ingested — no partial run is created. Top up and resubmit the same call.
What does ingestion_target do?+
It sets how deep ingestion goes after OCR. The default, ingest, kicks asynchronous Stage-2 field capture once a document's OCR markdown is ready, which puts the document into the corpus POST /v1/ask answers over and feeds the field registry. The Spec pipeline starts on the OCR markdown in both modes and never waits for capture, so the default costs you no run latency. Pass extract for the old markdown-only behavior; those documents will not be askable.
How do I match run.completed results back to my uploads?+
Use document_id. Each entry in the submit response's documents[] array carries the canonical document_id for that input, and each row in the run.completed webhook's records[] array carries the same document_id (plus filename) for the output it produced — join on that field to attribute a structured row back to the file or URL you sent. For a deduplicated upload (deduplicated: true), document_id points at the pre-existing canonical document, and linked_document_id identifies the link row created for this specific call.
Should I use append mode for bulk uploads?+
Yes, when you want one accumulated table: with pipeline_mode=append (or the Spec's Source stage set to append) every call adds its documents to the same pipeline, and each request still gets its own run.completed webhook scoped to its own documents. Keep the default (new) when each call should produce an isolated pipeline — e.g. one pipeline per customer folder.
How do I tag individual files differently in one call?+
Use file_metadata instead of (or alongside) metadata: a JSON string mapping each file's filename, or each file_urls entry, to its own flat metadata object. Per file, that bag is merged over the call-level metadata — per-file wins on a shared key — and the resolved bag lands on the document and is echoed in documents[].metadata. A key that does not match any file or URL in the call fails the whole request with a 400 naming the bad key, so a typo never silently tags nothing.