Webhooks
Holofin can POST to your endpoint when a workflow run completes, fails or pauses for review, when a reviewer confirms its results, and when a dossier changes state. This page covers registering endpoints, the payloads, signatures and retries.
Overview
A webhook is an HTTP POST that Holofin sends to an endpoint you register whenever an event it subscribes to occurs, so you do not have to poll the API.
How Webhooks Work
- Configure endpoint: Register your webhook URL in organization settings
- Select events: Choose which events you want to receive
- Receive notifications: When events occur, we send POST requests to your endpoint
- Process events: Your application handles the webhook payload
- Acknowledge receipt: Return a 2xx status code to confirm delivery within 30 seconds
Per-Run Override URL
Registered endpoints receive events for every run in your organization. When you want the events for a single run delivered somewhere else, pass a webhook_url when you create the run with POST /workflow-runs/:
Create a run with a per-run webhook URL
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...",
"webhook_url": "https://your-app.example.com/hooks/run-42"
}'Behavior:
- Replaces registered endpoints. For that run, events go only to the override URL; your registered endpoints receive nothing for it. All other runs are unaffected.
- All events for the run. The override receives the full set:
workflow-run.*,node-run.*,extraction.*, and the laterworkflow-run.confirmed/workflow-run.republishedevents. Event-subscription filters do not apply. - Same envelope, security, and retries. Payloads use the same envelope and flow through the same retry schedule.
- Signing reuses your endpoint secret. Deliveries are signed with the
X-Holofin-Signatureheader using the secret of your existing webhook endpoint, so the same verification code works. If your organization has no endpoint configured, there is no secret to reuse: deliveries are sent unsigned and the create response includes awarningsentry. - Validated up front. The URL must be an
http(s)URL reachable from the public internet. Malformed URLs and private/reserved addresses are rejected immediately with400 INVALID_WEBHOOK_URL(SSRF protection), and the run is not created.
Managing Endpoints (API)
You can register and manage webhook endpoints programmatically instead of using the dashboard, for example to provision one webhook per project. All routes are authenticated with your API key via the X-API-Key header and are scoped to the organization that owns the key.
http/https (private and reserved addresses are rejected to prevent SSRF). The signing secret is returned once, in the create response. Store it; it is not shown again.
Register an endpoint
Send a JSON body with the target url. name (defaults to "API webhook"), description and events are optional. An empty or omitted events list subscribes the endpoint to all event types; otherwise pass a subset of the subscribable events.
Use the returned secret to verify the X-Holofin-Signature header on incoming deliveries (see Webhook Security). On subsequent GET responses the secret field is omitted.
Update, disable or delete
PATCH accepts any subset of url, name, description, events and status. Set "status": "disabled" to pause deliveries without deleting the endpoint, and "status": "active" to resume (which also clears the failure streak). DELETE returns 204 No Content.
Errors are returned as {"error": {"code": "...", "message": "..."}} with codes such as MISSING_URL, INVALID_URL, INVALID_EVENTS, INVALID_STATUS, WEBHOOK_LIMIT_REACHED (HTTP 409) and NOT_FOUND (HTTP 404).
Event Types
Workflow events notify you about the status of document processing workflows.
Dossier events notify you when a dossier changes state as documents and values arrive. Like every other event, they are delivered only to the endpoints subscribed to them.
Webhook Payload
All webhook payloads follow a consistent structure:
For workflow-run.completed events, the payload includes:
validation_status- Composite status reflecting both consistency-check outcome and manual review state, in priority order:"VALIDATED": a reviewer has confirmed the extraction"INVALIDATED": a reviewer has reset / rejected a previous confirmation"PASSED": consistency checks passed, no manual review yet"WARNING": consistency checks raised warnings, no manual review yet"ERROR": consistency checks raised errors, no manual review yet"NO_VALIDATION": no validators were configurednull: no extraction aggregator (e.g., classification-only workflow)
no_route_matched-truewhen the run completed without any extraction because the detected document type had no matching route in the workflow (a branching step fell through to its "all others" path and no extractor ran). The workflow itself ran without error; its routing does not handle the document's type.unmatched_classifications- Whenno_route_matchedistrue, the detected classification(s) or segment type(s) that had no route (e.g.["carte-nationale-didentit"]).nullotherwise.
For workflow-run.paused events, the payload includes additional context about why the workflow paused:
pause_reason- Why the workflow paused (e.g., "approval" for human review)paused_at_node- The node ID where the workflow is waiting
For workflow-run.failed events, the payload includes:
error- Error message describing what went wrong
For workflow-run.confirmed events, the payload includes:
confirmed_by- Email of the user who confirmed the extractionconfirmed_at- ISO 8601 UTC timestamp when the extraction was confirmedcomment- Optional comment provided during confirmationextraction_data- Full extraction data (same format asextraction.processedpayload). Includes the per-partchecksrule inventory, so payload size grows with the number of checks a document type runs (a fully validated financial statement carries ~40 entries per part). Also carries a run-leveldocument_pdf_urlfield (the original document PDF) and apdf_urlfield on everydocument_partsentry (that part's pages as one PDF); fetching either requires theX-API-Keyheader, and?format=jsonon them yields a time-limited direct link.
For workflow-run.republished events, the payload includes:
status- Always"republished"republished_by- Email of the user who triggered the republishrepublished_at- ISO 8601 UTC timestamp of the republish actionoriginally_confirmed_by- Email of the user who first confirmed the extraction (may benullif the original reviewer's account no longer exists)originally_confirmed_at- ISO 8601 UTC timestamp of the original confirmation (may benull)comment- Optional comment provided when republishingextraction_data- Full extraction data (same format asworkflow-run.confirmed, includingdocument_pdf_urland per-partpdf_url, both requiring theX-API-Keyheader to fetch); typically reflects edits applied since the original confirmation
Use republished_at together with originally_confirmed_at to detect re-publishes and decide whether to upsert or version downstream records. Republishes can fire multiple times for the same run.
Node Event Payload
Node events (node-run.completed, node-run.failed) provide granular updates as each workflow node executes. For node-run.completed events, the payload includes a result field with node-specific output data:
The result field structure varies by node type:
For node-run.failed events, the payload includes:
error- Error message describing what went wrong in the node
Dossier Event Payload
Dossier events (case.updated, case.completed) carry the dossier itself: data.object is the same projection that GET /api/v1.1/cases/{case_id} answers with. The field-by-field reference lives on the Dossiers page and is not duplicated here. Three envelope keys ride along inside the object:
event- The event type, repeated inside the objectprevious_status- Thecase_statusbefore this change, lowered likecase_statusitself, ornullwhen there was nonetimestamp- ISO 8601 UTC timestamp of the evaluation that fired the event
Dossier events are organization-level, not run-level: a per-run override URL never receives them, and they always go to your registered endpoints, honoring each endpoint's event subscription. When a dossier transitions to complete, both events fire for that same change with distinct event ids.
Envelope Fields
Data Object Fields
Webhook Security
Holofin signs every webhook request with HMAC so you can check that it came from Holofin and was not modified.
Signature Verification
Each webhook request includes a signature in the X-Holofin-Signature header. This signature is generated using HMAC-SHA256 with your webhook secret.
Retries & Reliability
Failed deliveries are retried automatically:
- Success criteria: Any 2xx HTTP status code is considered successful
- Retry schedule: Failed webhooks are retried with exponential backoff plus jitter to prevent thundering herd
- Max attempts: Up to 7 retry attempts over 48 hours
- Status change: Endpoints marked as "FAILING" after 5 consecutive failures (but continue receiving events)
Retry Schedule
Best Practices
- Always verify signaturesValidate the HMAC signature on every webhook to ensure requests are from Holofin.
- Handle events idempotentlyUse the event ID to ensure you process each event only once, even if it's delivered multiple times.
- Respond quicklyReturn a 2xx status code within 30 seconds. Process the event asynchronously if your handler needs more time.
- Monitor failuresCheck your webhook logs regularly to identify and fix any delivery issues.
Testing Webhooks
You can test your webhook integration using the test button in the webhook settings.