Workflow runs

Run a workflow on a document, follow it, read its results and PDFs, and cancel it.

Create a workflow run

POST/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_idstringrequired

    The workflow to run.

  • document_b64string

    The file, base64-encoded: a PDF, an office document (.xlsx, .xls, .docx, .pptx) or one image (PNG, JPG, TIFF). Images become a PDF. Send this or document_pages_b64.

  • document_pages_b64array of string

    Base64 images merged, in order, into one PDF.

  • document_namestring

    The extension is set from the file's real type. Default "document".

  • metadataobject

    Your own keys, returned on the run and in every webhook.

  • webhook_urlstring

    Send 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_idstring

    The dossier this document joins. Create it first with POST /cases/; a run never opens one.

  • party_refsarray of string

    The participants the document belongs to, by party_ref. Several declare a jointly held document. Needs case_id. See Sending documents.

Errors

  • 400MISSING_WORKFLOW_IDworkflow_id is missing.
  • 400MISSING_DOCUMENTNeither document_b64 nor document_pages_b64 was sent.
  • 400CONFLICTING_INPUTBoth document_b64 and document_pages_b64 were sent.
  • 400INVALID_DOCUMENTThe content is not valid base64, or not a PDF, office document or image Holofin reads.
  • 400PAGE_LIMIT_EXCEEDEDThe document has more than 200 pages.
  • 400INVALID_WEBHOOK_URLwebhook_url is malformed, not http(s), or resolves to a private address.
  • 400UNSUPPORTED_FIELDThe body carries a dossier key (case_fields, party_fields, config_id, party_ref, role, party_type). Send it to POST /cases/.
  • 400APPLICATIONS_DISABLEDcase_id was sent but Dossiers are not enabled.
  • 400INVALID_CASE_IDcase_id is empty or longer than 255 characters.
  • 400UNKNOWN_CASE_IDcase_id names no dossier. Create it with POST /cases/ first.
  • 400PARTY_WITHOUT_CASEparty_refs was sent without case_id.
  • 400INVALID_PARTY_REFA participant reference is not a distinct, non-empty string of at most 255 characters.
  • 400UNKNOWN_PARTY_REFA reference names no participant of the dossier; the message lists them.
  • 402INSUFFICIENT_CREDITSNo credits remaining.
  • 402NO_ACTIVE_SUBSCRIPTIONThe organization has no active plan.
  • 402PLAN_NOT_PAIDThe plan is not active yet.
  • 404WORKFLOW_NOT_FOUNDNo workflow with this ID in your organization.
  • 429SANDBOX_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());
202 · Accepted
{
  "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

GET/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_idstringrequired

    The run's ID, from the create call.

Response

  • run_idstring

    The run's ID.

  • workflow_idstring

    The workflow that ran.

  • workflow_namestring

    The workflow's display name.

  • document_idstring

    The uploaded file.

  • document_namestring
  • extraction_idstring | null

    The extraction's ID (wea_...), null until the extraction node has run. The same value as extraction_id in the Partner SSO token and the extraction JSON export.

  • statusstring

    pending until it starts, running, paused at a human review node, then completed, failed or cancelled.

  • created_atstring
  • metadataobject

    The metadata sent on creation, returned as is.

  • document_pdf_urlstring

    The run document PDF endpoint. Needs the X-API-Key header.

  • progressobject

    current_step, total_steps and percentage.

  • started_atstring | null
  • completed_atstring | null
  • duration_msinteger | null
  • classification_resultobject | null

    class, confidence and reasoning. Null without a classifier node.

  • segmentation_resultobject | null

    sections, one entry per document found (section_id, pages, partition_key), and pages, the same split page by page. Null without a segmenter node.

  • extraction_resultobject | null

    document_parts, one entry per document found, each with its data, checks, validation_status and a pdf_url (the part PDF). duplicated_pages lists pages left out as duplicates. Fills in as each extractor finishes. See Schema part types.

  • fraud_analysis_resultobject | null

    risk_level, risk_score, risk_factors and trust_factors. Each factor's sources says where its evidence comes from: forensic, template, content or seal. status is skipped, with a skip_reason, when the organization's fraud filters left the document out. Null without a fraud node. See Fraud analysis.

  • case_idstring | null

    The dossier the document is filed under, if any.

  • no_route_matchedboolean

    True 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 | null

    The types that had no route, when no_route_matched is true.

  • errorobject | null

    On a failed run, code (NODE_EXECUTION_FAILED or WORKFLOW_FAILED) and message, plus node_id and node_type for a node failure.

Errors

  • 404RUN_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());
200
{
  "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

GET/api/v1.1/workflow-runs/{run_id}/pdf

The 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_idstringrequired

    The run's ID, from the create call.

Query parameters

  • formatstring

    json returns the signed link in a JSON body instead of redirecting.

Response

  • urlstring

    Anyone with the link can download the file until it expires.

  • expires_atstring

Errors

  • 404RUN_NOT_FOUNDNo run with this ID in your organization.
  • 404DOCUMENT_FILE_UNAVAILABLEThe document file is no longer stored, for example after its retention period.
  • 415UNSUPPORTED_DOCUMENT_TYPEA PDF was requested for a source document that is not a PDF, such as a spreadsheet.
  • 500PDF_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.pdf
import 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()));
200 · With format=json
{
  "url": "https://storage.holofin.ai/documents/doc_def456uvw.pdf?signature=...",
  "expires_at": "2026-08-01T10:40:00Z"
}

Download a document part

GET/api/v1.1/workflow-runs/{run_id}/parts/{part_key}/pdf

The 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_idstringrequired

    The run's ID, from the create call.

  • part_keystringrequired

    The part's key in document_parts.

Query parameters

  • formatstring

    json returns the signed link in a JSON body instead of redirecting.

Response

  • urlstring

    Anyone with the link can download the file until it expires.

  • expires_atstring

Errors

  • 404RUN_NOT_FOUNDNo run with this ID in your organization.
  • 404PART_NOT_FOUNDNo part with this key on the run.
  • 404DOCUMENT_FILE_UNAVAILABLEThe document file is no longer stored, for example after its retention period.
  • 409RUN_NOT_TERMINALExtraction has not finished, so the parts do not exist yet.
  • 415UNSUPPORTED_DOCUMENT_TYPEA PDF was requested for a source document that is not a PDF, such as a spreadsheet.
  • 500PDF_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.pdf
import 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()));
200 · With format=json
{
  "url": "https://storage.holofin.ai/documents/doc_def456uvw_segment_1.pdf?signature=...",
  "expires_at": "2026-08-01T10:40:00Z"
}

List workflow runs

GET/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_idstring

    Only this workflow's runs.

  • statusstring

    One of completed, failed, cancelled.

  • case_idstring

    Only runs whose document is in this dossier.

  • created_afterstring
  • created_beforestring
  • limitinteger

    Page size, clamped to 1-100. Default 100.

  • next_tokenstring

    Opaque cursor from the previous page. Omit for the first page.

Response

  • runsarray of object
  • paginationobject

    page_size, has_next, and next_token while has_next is true. Pass next_token back for the next page.

Errors

  • 400INVALID_FILTERcreated_after or created_before is 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());
200
{
  "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

GET/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_idstringrequired

    The run's ID, from the create call.

Response

  • run_idstring
  • node_runsarray of object
  • total_countinteger

Errors

  • 404RUN_NOT_FOUNDNo run with this ID in your organization.

What each node returns

Nodeconfigresults
classifierclassifier_name, classes (each name, description)class, confidence (0 to 1), reasoning
segmentersegmenter_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
extractorextractor_name, extraction_typeNot 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());
200
{
  "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

POST/api/v1.1/workflow-runs/{run_id}/cancel

Stops a pending or running run before its next node starts. A node already running finishes first.

Path parameters

  • run_idstringrequired

    The run's ID, from the create call.

Response

  • run_idstring
  • statusstring

    One of cancelled.

  • cancelled_atstring | null

Errors

  • 404RUN_NOT_FOUNDNo run with this ID in your organization.
  • 409CANNOT_CANCELThe run is already completed, failed or cancelled. 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());
200
{
  "run_id": "wr_abc123xyz789",
  "status": "cancelled",
  "cancelled_at": "2024-01-15T10:30:05Z"
}