Webhooks
Webhooks let your backend receive a small, signed notification when an async extraction job reaches a terminal state, instead of polling for it. A webhook is a notification channel, not a bulk result transport: the body is a compact summary, and you still fetch the full payload from the result endpoint.
For the request, status, and result mechanics, see
Asynchronous Extraction. All extraction
endpoints authenticate with Authorization: api-key <environment-api-key> (see
Authentication & Errors).
What V1 Supports
Section titled “What V1 Supports”V1 supports:
- multiple webhook endpoints per environment
- two scope types:
scoped(selected templates) andglobal - notifications for terminal states only:
extraction.job.completedextraction.job.failed
- filter-driven delivery, grouped by matched endpoint ownership
V1 does not support:
- per-request callback URLs
- selecting a webhook endpoint directly in the extraction request
- webhook delivery on sync extraction
- full extraction payloads inside the webhook body
Use Webhook Delivery
Section titled “Use Webhook Delivery”To receive webhooks, create an async job with deliveryMode=webhook:
{ "templateName": "invoice", "deliveryMode": "webhook"}If deliveryMode is omitted, delivery defaults to poll. Webhook delivery is
available only on the canonical async route POST /api/v1/extraction-jobs, not
on sync or legacy async extraction.
Set Up Webhooks In The App
Section titled “Set Up Webhooks In The App”Webhook endpoints are configured in the product UI, per environment. There is no public API for managing webhook configuration: you create, edit, and validate webhook endpoints in the app, and the app checks that each target URL is valid and reachable when you save it.
To start using webhooks:
- Open the app and go to your extraction webhook settings.
- Add a webhook endpoint pointing at the receiver URL on your backend.
- Choose its scope (see below) and save. The app validates the URL on save.
- Securely store the signing secret the app issues for that webhook.
- Submit async jobs with
deliveryMode=webhook.
Once a webhook is active, delivery works automatically; you do not configure anything per request.
What you configure
Section titled “What you configure”-
Scope: each webhook is either
scoped: it owns selected templates, orglobal: it covers every template not already claimed by a scoped webhook.
An environment may have many active scoped webhooks and at most one active global webhook.
-
Signing secret: issued when a webhook is created or rotated. Your receiver uses it to verify authenticity. Store the full secret on the backend only.
-
Outbound auth (optional): the app can attach an auth header to every delivery (
None,Authorization: Bearer <token>, orX-API-Key: <token>). -
Subscribed events:
extraction.job.completedandextraction.job.failed.
How Routing Works
Section titled “How Routing Works”You do not send a webhookId in extraction requests. Routing is derived from
template ownership.
For templateName requests:
- the API resolves the requested templates first
- if a requested template does not exist, the request fails as a template error
- a scoped webhook that owns the template wins
- otherwise the active global webhook is used if one exists
- if no route matches, or one request would resolve to more than one endpoint, job creation fails
For filterName requests:
- admission validates that all extractable templates in the filter are covered by active webhook configuration
- completed deliveries are grouped by matched endpoint ownership, so one filter job may emit one completed webhook per matched endpoint
otherstays in the parent result and does not emit a webhook in V1
Webhook Payloads
Section titled “Webhook Payloads”Completed event
Section titled “Completed event”{ "id": "delivery_id", "type": "extraction.job.completed", "createdAt": "2026-03-16T09:23:09.152Z", "job": { "id": "job_id", "status": "completed", "resultUrl": "https://api.example.com/api/v1/extraction-jobs/job_id/result" }}For filter-driven jobs, a completed payload may also include grouped document summaries for the endpoint:
{ "routing": { "webhookId": "webhook_id", "scopeType": "scoped", "templates": [ { "id": "template_invoice", "name": "invoice" }, { "id": "template_bank_statement", "name": "bank statement" } ] }, "documents": [ { "documentName": "Invoice", "templateId": "template_invoice", "templateName": "invoice", "sourcePages": [1] }, { "documentName": "Bank Statement", "templateId": "template_bank_statement", "templateName": "bank statement", "sourcePages": [2, 3] } ]}Failed event
Section titled “Failed event”{ "id": "delivery_id", "type": "extraction.job.failed", "createdAt": "2026-03-16T09:23:09.152Z", "job": { "id": "job_id", "status": "failed", "statusUrl": "https://api.example.com/api/v1/extraction-jobs/job_id" }}Webhook Headers
Section titled “Webhook Headers”Clients should expect these headers:
X-Optica-EventX-Optica-DeliveryX-Optica-TimestampX-Optica-SignatureContent-Type: application/json- optional outbound auth header if configured on that webhook endpoint:
Authorization: Bearer <token>X-API-Key: <token>
Consistency rules:
- body
idmust matchX-Optica-Delivery - body
typemust matchX-Optica-Event - body
job.idmust match the async extraction job id
Signature Verification
Section titled “Signature Verification”The signing secret is used by the receiver to verify webhook authenticity.
The signed content is:
${timestamp}.${rawBody}Verification steps:
- capture the raw request body exactly as received
- read
X-Optica-Timestamp - read
X-Optica-Signature - compute
HMAC_SHA256(secret, timestamp + "." + rawBody) - compare it to the signature after removing the
v1=prefix
Important:
- do not parse and re-serialize the JSON before verification
- verify against the raw body bytes
- store the secret on the backend only
Example local verification command:
printf '%s' "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "$SECRET"Receiver Behavior
Section titled “Receiver Behavior”A production receiver should:
- verify the signature first
- optionally reject stale timestamps
- optionally enforce the configured bearer or API key header at the edge or proxy
- dedupe on the webhook delivery id
- return
2xxquickly - process asynchronously
- for
completed, fetch the full extraction result fromjob.resultUrl
Webhook Errors
Section titled “Webhook Errors”These webhook-specific errors are returned at job creation. For job status and
result errors (JOB_NOT_FOUND, RESULT_NOT_READY, REVIEW_REQUIRED,
JOB_FAILED, RESULT_EXPIRED, RATE_LIMIT), see
Asynchronous Extraction.
400 INVALID_DELIVERY_MODEdeliveryModewas not a supported value400 WEBHOOK_ASYNC_ONLYwebhook delivery was requested on sync extraction400 LEGACY_ASYNC_POLL_ONLYwebhook delivery was requested on the legacy async endpoint400 FILTER_HAS_NO_EXTRACTABLE_TEMPLATESthe filter contains no extractable templates400 WEBHOOK_NOT_CONFIGUREDno active webhook route exists400 WEBHOOK_NOT_CONFIGURED_FOR_TEMPLATEa request template is not covered by a scoped or global webhook400 WEBHOOK_TEMPLATE_ROUTING_CONFLICTone request resolved to more than one webhook endpoint
Recommended Integration
Section titled “Recommended Integration”- configure scoped webhooks in the app for templates that need dedicated routing
- optionally configure one global webhook for all remaining templates
- submit async extraction through
POST /api/v1/extraction-jobs - use
deliveryMode=webhookfor event-driven completion - treat the webhook as a signed summary notification
- fetch the real payload from the result endpoint
- keep polling and status handling as a fallback
- when using
filterName, expect grouped completed deliveries per matched endpoint
This keeps webhook payloads small, retries cheap, and receiver behavior predictable, with full results fetched only when needed.