Workflow runs
Run a workflow on a document, follow it, read its results and PDFs, and cancel it.
Create a workflow run
/api/v1.1/workflow-runs/Sends one document through a workflow. Returns at once with the run in pending; follow it with webhooks or by polling the run. A call refused with a 4xx creates no run and uses no credits.
Body
workflow_idstringrequiredThe workflow to run.
document_b64stringThe file, base64-encoded: a PDF, an office document (
.xlsx,.xls,.docx,.pptx) or one image (PNG, JPG, TIFF). Images become a PDF. Send this ordocument_pages_b64.document_pages_b64array of stringBase64 images merged, in order, into one PDF.
document_namestringThe extension is set from the file's real type. Default
"document".metadataobjectYour own keys, returned on the run and in every webhook.
webhook_urlstringSend this run's webhook events to this URL only, instead of your configured endpoints. Signed with your endpoints' secret, or unsigned (with a warning) when you have none.
case_idstringThe dossier this document joins. Create it first with
POST /cases/; a run never opens one.party_refsarray of stringThe participants the document belongs to, by
party_ref. Several declare a jointly held document. Needscase_id. See Sending documents.
Errors
- 400
MISSING_WORKFLOW_IDworkflow_idis missing. - 400
MISSING_DOCUMENTNeitherdocument_b64nordocument_pages_b64was sent. - 400
CONFLICTING_INPUTBothdocument_b64anddocument_pages_b64were sent. - 400
INVALID_DOCUMENTThe content is not valid base64, or not a PDF, office document or image Holofin reads. - 400
PAGE_LIMIT_EXCEEDEDThe document has more than 200 pages. - 400
INVALID_WEBHOOK_URLwebhook_urlis malformed, nothttp(s), or resolves to a private address. - 400
UNSUPPORTED_FIELDThe body carries a dossier key (case_fields,party_fields,config_id,party_ref,role,party_type). Send it toPOST /cases/. - 400
APPLICATIONS_DISABLEDcase_idwas sent but Dossiers are not enabled. - 400
INVALID_CASE_IDcase_idis empty or longer than 255 characters. - 400
UNKNOWN_CASE_IDcase_idnames no dossier. Create it withPOST /cases/first. - 400
PARTY_WITHOUT_CASEparty_refswas sent withoutcase_id. - 400
INVALID_PARTY_REFA participant reference is not a distinct, non-empty string of at most 255 characters. - 400
UNKNOWN_PARTY_REFA reference names no participant of the dossier; the message lists them. - 402
INSUFFICIENT_CREDITSNo credits remaining. - 402
NO_ACTIVE_SUBSCRIPTIONThe organization has no active plan. - 402
PLAN_NOT_PAIDThe plan is not active yet. - 404
WORKFLOW_NOT_FOUNDNo workflow with this ID in your organization. - 429
SANDBOX_LIMIT_EXCEEDEDThe document needs more sandbox credits than remain. Backing off does not help.
To file the document under a dossier, add its case_id, and party_refs when the dossier holds several participants:
{
"workflow_id": "wf_abc123xyz",
"document_b64": "JVBERi0xLjQKJeLjz9MKNCAwIG9i...",
"case_id": "L-2026-0417",
"party_refs": ["REF-MARIE"]
}Dossier field values go to POST /cases/, not here.
curl -X POST "https://app.holofin.ai/api/v1.1/workflow-runs/" \
-H "X-API-Key: $HOLOFIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wf_abc123xyz",
"document_b64": "JVBERi0xLjQKJeLjz9MKNCAwIG9i...",
"document_name": "invoice-2024-001.pdf",
"metadata": {
"client_reference": "invoice-2024-001"
}
}'import os
import requests
response = requests.post(
"https://app.holofin.ai/api/v1.1/workflow-runs/",
headers={"X-API-Key": os.environ["HOLOFIN_API_KEY"]},
json={
"workflow_id": "wf_abc123xyz",
"document_b64": "JVBERi0xLjQKJeLjz9MKNCAwIG9i...",
"document_name": "invoice-2024-001.pdf",
"metadata": {
"client_reference": "invoice-2024-001",
},
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://app.holofin.ai/api/v1.1/workflow-runs/", {
method: "POST",
headers: {
"X-API-Key": process.env.HOLOFIN_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"workflow_id": "wf_abc123xyz",
"document_b64": "JVBERi0xLjQKJeLjz9MKNCAwIG9i...",
"document_name": "invoice-2024-001.pdf",
"metadata": {
"client_reference": "invoice-2024-001"
}
}),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());{
"run_id": "wr_abc123xyz789",
"workflow_id": "wf_abc123xyz",
"workflow_name": "Invoice Processing",
"document_id": "doc_def456uvw",
"extraction_id": null,
"document_name": "invoice-2024-001.pdf",
"status": "pending",
"created_at": "2024-01-15T10:30:00Z",
"metadata": {
"client_reference": "invoice-2024-001"
}
}Get a workflow run
/api/v1.1/workflow-runs/{run_id}The run's status and results. Results fill in while it runs: extraction_result gains a part as each extractor finishes.
Path parameters
run_idstringrequiredThe run's ID, from the create call.
Response
run_idstringThe run's ID.
workflow_idstringThe workflow that ran.
workflow_namestringThe workflow's display name.
document_idstringThe uploaded file.
document_namestringextraction_idstring | nullThe extraction's ID (
wea_...), null until the extraction node has run. The same value asextraction_idin the Partner SSO token and the extraction JSON export.statusstringpendinguntil it starts,running,pausedat a human review node, thencompleted,failedorcancelled.created_atstringmetadataobjectThe
metadatasent on creation, returned as is.document_pdf_urlstringThe run document PDF endpoint. Needs the
X-API-Keyheader.progressobjectcurrent_step,total_stepsandpercentage.started_atstring | nullcompleted_atstring | nullduration_msinteger | nullclassification_resultobject | nullclass,confidenceandreasoning. Null without a classifier node.segmentation_resultobject | nullsections, one entry per document found (section_id,pages,partition_key), andpages, the same split page by page. Null without a segmenter node.extraction_resultobject | nulldocument_parts, one entry per document found, each with itsdata,checks,validation_statusand apdf_url(the part PDF).duplicated_pageslists pages left out as duplicates. Fills in as each extractor finishes. See Schema part types.fraud_analysis_resultobject | nullrisk_level,risk_score,risk_factorsandtrust_factors. Each factor'ssourcessays where its evidence comes from:forensic,template,contentorseal.statusisskipped, with askip_reason, when the organization's fraud filters left the document out. Null without a fraud node. See Fraud analysis.case_idstring | nullThe dossier the document is filed under, if any.
no_route_matchedbooleanTrue when the run ended without extraction because no route handles the document's type. The run did not fail: add a route for the type, or send the document to another workflow.
unmatched_classificationsarray of string | nullThe types that had no route, when
no_route_matchedis true.errorobject | nullOn a failed run,
code(NODE_EXECUTION_FAILEDorWORKFLOW_FAILED) andmessage, plusnode_idandnode_typefor a node failure.
Errors
- 404
RUN_NOT_FOUNDNo run with this ID in your organization.
curl "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789" \
-H "X-API-Key: $HOLOFIN_API_KEY"import os
import requests
response = requests.get(
"https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789",
headers={"X-API-Key": os.environ["HOLOFIN_API_KEY"]},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789", {
headers: {
"X-API-Key": process.env.HOLOFIN_API_KEY,
},
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());{
"run_id": "wr_abc123xyz789",
"workflow_id": "wf_abc123xyz",
"workflow_name": "Bank Statement Processing",
"document_id": "doc_def456uvw",
"extraction_id": "wea_abc123xyz789",
"document_name": "statement_january_2024.pdf",
"document_pdf_url": "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/pdf",
"status": "completed",
"progress": {
"current_step": 4,
"total_steps": 4,
"percentage": 100
},
"started_at": "2024-01-15T10:30:01Z",
"completed_at": "2024-01-15T10:30:15Z",
"duration_ms": 14000,
"classification_result": {
"class": "bank_statement",
"confidence": 0.98,
"reasoning": "Bank logo, account number and a transaction list."
},
"segmentation_result": {
"sections": [
{
"section_id": "bank_statement",
"pages": [
1,
2
],
"partition_key": "FR7630004020360001026054720"
}
],
"pages": [
{
"page": 1,
"section": "bank_statement",
"partition": "FR7630004020360001026054720"
},
{
"page": 2,
"section": "bank_statement",
"partition": "FR7630004020360001026054720"
}
]
},
"extraction_result": {
"document_parts": [
{
"part_type": "bank-statement",
"key": "segment_1",
"label": "FR7630004020360001026054720",
"validation_status": "OK",
"validation_codes": [],
"checks": [
{
"type": "extraction",
"code": "BALANCE_EQUATION",
"label": "Balance equation",
"status": "passed",
"severity": "error"
}
],
"pages": [
1,
2
],
"pdf_url": "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/parts/segment_1/pdf",
"data": {
"bank_name": "BNP Paribas",
"currency": "EUR",
"account_number": "FR7630004020360001026054720",
"client_name": "ACME Corporation SARL",
"start_balance": 2450.75,
"end_balance": 5825.25,
"start_date": "2024-01-01",
"end_date": "2024-01-31",
"transactions": [
{
"transaction_date": "2024-01-02",
"description": "VIREMENT SALAIRE",
"amount": 3500.0,
"page": 1,
"row": 1
}
]
}
}
]
},
"fraud_analysis_result": {
"status": "completed",
"risk_level": "warning",
"risk_score": 42,
"risk_factors": [
{
"id": "suspicious_timestamp",
"label": "Suspicious Timestamp",
"severity": "medium",
"details": "Unusual gap between creation and modification times.",
"sources": [
"forensic"
]
}
],
"trust_factors": []
},
"case_id": null,
"no_route_matched": false,
"unmatched_classifications": null,
"error": null,
"metadata": {
"client_reference": "statement-2024-001"
}
}Download the run's document
/api/v1.1/workflow-runs/{run_id}/pdfThe file sent on creation, available as soon as the run exists. This is the run's document_pdf_url. Redirects (302) to a signed link valid for about 10 minutes; any client that follows redirects receives the PDF. With format=json, returns the link instead.
Path parameters
run_idstringrequiredThe run's ID, from the create call.
Query parameters
formatstringjsonreturns the signed link in a JSON body instead of redirecting.
Response
urlstringAnyone with the link can download the file until it expires.
expires_atstring
Errors
- 404
RUN_NOT_FOUNDNo run with this ID in your organization. - 404
DOCUMENT_FILE_UNAVAILABLEThe document file is no longer stored, for example after its retention period. - 415
UNSUPPORTED_DOCUMENT_TYPEA PDF was requested for a source document that is not a PDF, such as a spreadsheet. - 500
PDF_RESOLUTION_FAILEDThe run or part PDF could not be produced.
The signed url needs no API key: anyone holding it can download the file until expires_at.
curl -L "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/pdf" \
-H "X-API-Key: $HOLOFIN_API_KEY" \
-o document.pdfimport os
import requests
response = requests.get(
"https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/pdf",
headers={"X-API-Key": os.environ["HOLOFIN_API_KEY"]},
)
response.raise_for_status()
with open("document.pdf", "wb") as f:
f.write(response.content)import { writeFile } from "node:fs/promises";
const response = await fetch("https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/pdf", {
headers: {
"X-API-Key": process.env.HOLOFIN_API_KEY,
},
});
if (!response.ok) throw new Error(await response.text());
await writeFile("document.pdf", Buffer.from(await response.arrayBuffer()));{
"url": "https://storage.holofin.ai/documents/doc_def456uvw.pdf?signature=...",
"expires_at": "2026-08-01T10:40:00Z"
}Download a document part
/api/v1.1/workflow-runs/{run_id}/parts/{part_key}/pdfThe pages of one part as a single PDF, non-contiguous pages included. Each entry of document_parts carries this URL as pdf_url. Same redirect and format=json behaviour as the run's document.
Path parameters
run_idstringrequiredThe run's ID, from the create call.
part_keystringrequiredThe part's
keyindocument_parts.
Query parameters
formatstringjsonreturns the signed link in a JSON body instead of redirecting.
Response
urlstringAnyone with the link can download the file until it expires.
expires_atstring
Errors
- 404
RUN_NOT_FOUNDNo run with this ID in your organization. - 404
PART_NOT_FOUNDNo part with this key on the run. - 404
DOCUMENT_FILE_UNAVAILABLEThe document file is no longer stored, for example after its retention period. - 409
RUN_NOT_TERMINALExtraction has not finished, so the parts do not exist yet. - 415
UNSUPPORTED_DOCUMENT_TYPEA PDF was requested for a source document that is not a PDF, such as a spreadsheet. - 500
PDF_RESOLUTION_FAILEDThe run or part PDF could not be produced.
curl -L "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/parts/segment_1/pdf" \
-H "X-API-Key: $HOLOFIN_API_KEY" \
-o segment_1.pdfimport os
import requests
response = requests.get(
"https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/parts/segment_1/pdf",
headers={"X-API-Key": os.environ["HOLOFIN_API_KEY"]},
)
response.raise_for_status()
with open("segment_1.pdf", "wb") as f:
f.write(response.content)import { writeFile } from "node:fs/promises";
const response = await fetch("https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/parts/segment_1/pdf", {
headers: {
"X-API-Key": process.env.HOLOFIN_API_KEY,
},
});
if (!response.ok) throw new Error(await response.text());
await writeFile("segment_1.pdf", Buffer.from(await response.arrayBuffer()));{
"url": "https://storage.holofin.ai/documents/doc_def456uvw_segment_1.pdf?signature=...",
"expires_at": "2026-08-01T10:40:00Z"
}List workflow runs
/api/v1.1/workflow-runs/Runs that have finished (completed, failed or cancelled), newest first. Items carry no extraction data: fetch a run by ID for it.
Query parameters
workflow_idstringOnly this workflow's runs.
statusstringOne of
completed,failed,cancelled.case_idstringOnly runs whose document is in this dossier.
created_afterstringcreated_beforestringlimitintegerPage size, clamped to 1-100. Default
100.next_tokenstringOpaque cursor from the previous page. Omit for the first page.
Response
runsarray of objectpaginationobjectpage_size,has_next, andnext_tokenwhilehas_nextis true. Passnext_tokenback for the next page.
Errors
- 400
INVALID_FILTERcreated_afterorcreated_beforeis not ISO 8601.
curl -G "https://app.holofin.ai/api/v1.1/workflow-runs/" \
-H "X-API-Key: $HOLOFIN_API_KEY" \
--data-urlencode "workflow_id=wf_abc123xyz" \
--data-urlencode "limit=20"import os
import requests
response = requests.get(
"https://app.holofin.ai/api/v1.1/workflow-runs/",
headers={"X-API-Key": os.environ["HOLOFIN_API_KEY"]},
params={
"workflow_id": "wf_abc123xyz",
"limit": 20,
},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://app.holofin.ai/api/v1.1/workflow-runs/?workflow_id=wf_abc123xyz&limit=20", {
headers: {
"X-API-Key": process.env.HOLOFIN_API_KEY,
},
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());{
"runs": [
{
"run_id": "wr_abc123xyz789",
"workflow_id": "wf_abc123xyz",
"workflow_name": "Bank Statement Processing",
"document_id": "doc_def456uvw",
"extraction_id": "wea_abc123xyz789",
"case_id": "L-2026-0417",
"document_name": "statement_january_2024.pdf",
"status": "completed",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:30:15Z",
"duration_ms": 14000,
"validation_status": "OK"
}
],
"pagination": {
"page_size": 20,
"has_next": true,
"next_token": "MjAyNC0wMS0xNVQxMDozMDoxNVp8d3JfYWJj"
}
}List a run's node executions
/api/v1.1/workflow-runs/{run_id}/node-runs/Each node's configuration, status and output. Extractor output is not here: read it from the run. Start and end nodes are left out.
Path parameters
run_idstringrequiredThe run's ID, from the create call.
Response
run_idstringnode_runsarray of objecttotal_countinteger
Errors
- 404
RUN_NOT_FOUNDNo run with this ID in your organization.
What each node returns
| Node | config | results |
|---|---|---|
classifier | classifier_name, classes (each name, description) | class, confidence (0 to 1), reasoning |
segmenter | segmenter_name, custom_rules, prompt, sections (each section_id, section_description, partition_key, partition_description) | confidence, reasoning, sections and pages, the same views as the run's segmentation_result |
extractor | extractor_name, extraction_type | Not here: read extraction_result on the run. |
curl "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/node-runs/" \
-H "X-API-Key: $HOLOFIN_API_KEY"import os
import requests
response = requests.get(
"https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/node-runs/",
headers={"X-API-Key": os.environ["HOLOFIN_API_KEY"]},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/node-runs/", {
headers: {
"X-API-Key": process.env.HOLOFIN_API_KEY,
},
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());{
"run_id": "wr_abc123xyz789",
"node_runs": [
{
"node_type": "classifier",
"status": "completed",
"started_at": "2024-01-15T10:30:01Z",
"completed_at": "2024-01-15T10:30:04Z",
"duration_ms": 3120,
"agent_id": "ca_EACHWbXq3cRtPvGQ",
"is_global_agent": false,
"config": {
"classifier_name": "Financial documents",
"classes": [
{
"name": "bank_statement",
"description": "..."
}
]
},
"results": {
"class": "bank_statement",
"confidence": 0.98,
"reasoning": "Bank logo, account number, transactions."
}
}
],
"total_count": 1
}Cancel a workflow run
/api/v1.1/workflow-runs/{run_id}/cancelStops a pending or running run before its next node starts. A node already running finishes first.
Path parameters
run_idstringrequiredThe run's ID, from the create call.
Response
run_idstringstatusstringOne of
cancelled.cancelled_atstring | null
Errors
- 404
RUN_NOT_FOUNDNo run with this ID in your organization. - 409
CANNOT_CANCELThe run is alreadycompleted,failedorcancelled. Treat it as success.
curl -X POST "https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/cancel" \
-H "X-API-Key: $HOLOFIN_API_KEY"import os
import requests
response = requests.post(
"https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/cancel",
headers={"X-API-Key": os.environ["HOLOFIN_API_KEY"]},
)
response.raise_for_status()
print(response.json())const response = await fetch("https://app.holofin.ai/api/v1.1/workflow-runs/wr_abc123xyz789/cancel", {
method: "POST",
headers: {
"X-API-Key": process.env.HOLOFIN_API_KEY,
},
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());{
"run_id": "wr_abc123xyz789",
"status": "cancelled",
"cancelled_at": "2024-01-15T10:30:05Z"
}