Webhooks
Last updated: 2026-10-15
Webhooks
Webhooks send extracted data to your application or automation workflow when a document task finishes. For example, add each extracted invoice to a spreadsheet, update your accounting system, or notify your team when processing fails. Suparse sends an HTTPS POST with a JSON body and a signature to verify its origin.
Before you start
You need a public HTTPS URL that accepts POST requests:
- Automation workflow: create an incoming-webhook trigger and copy its active receiving URL. Your workflow must support request headers and the original raw body for signature verification. If it cannot, put a small verifying receiver in front of it.
- Your application: deploy a server-side route using the signature-verification example below. For local development, expose your receiver through a public HTTPS tunnel; Suparse cannot call
localhost.
Keep these credentials separate:
| Credential | Purpose |
|---|---|
Endpoint signing secret (whsec_…) | Your receiver verifies that a request came from Suparse. Keep it server-side. |
| Optional custom request header | Suparse authenticates to your receiver, if it requires a token. This does not perform signature verification. |
| Suparse API key | Your integration fetches results when an event contains a results URL. Create it for the task's team; see Getting started. |
Set up an endpoint
-
Open Dashboard → Webhooks and select the team that owns the documents and API credentials for this integration. Team admins can create and manage endpoints.
-
Choose Add endpoint, enter a name, and paste your receiver's public HTTPS URL. Local, private, and link-local destinations are not accepted.
-
Subscribe to
task.completedand, if you want failure notifications,task.failed. Start without upload-source or template filters; narrow them after confirming your first real event. -
Leave custom request headers empty unless your receiver needs them. For example:
{ "Authorization": "Bearer <your-receiver-token>", "X-Integration": "finance" }Header values must be strings. You can configure up to 10 custom headers. Suparse manages standard request headers, including
Content-Type,User-Agent, andX-Suparse-*, so they cannot be overridden. The dashboard masks custom values after saving; when editing, leave the header field blank to keep existing values or enter{}to clear them. -
Save the endpoint. Copy the signing secret shown after it is created and store it in your secret manager. The dashboard shows the secret only once. Rotating it also displays the new value once, and takes effect immediately.
-
Ensure the endpoint is enabled, then choose Send test. Confirm a successful
2xxdelivery in Delivery history. This sendsendpoint.test, nottask.completed.
Endpoints belong to the selected team. A task must belong to that team to appear in its webhook events or result requests.
Connect the event to your workflow
For an invoice-to-spreadsheet workflow, use these steps after verifying and durably accepting the request:
- Route by
type: acknowledgeendpoint.testwithout creating business records; sendtask.failedto your error or notification branch. - For
task.completed, checkdata.result_mode. Usedata.documentsforinline; forreference, fetch all pages fromdata.results_urlas described below. - Loop over the documents. Map each document's
extraction_data.invoice_numberandextraction_data.totalto spreadsheet columns. Your actual field names depend on your template. An empty document list creates no rows. - Upload a real invoice after enabling the endpoint. Confirm that the expected rows appear and check Delivery history. A synthetic test does not check task filters or real extraction fields.
Use the envelope's id to prevent duplicate handling within this integration, data.task.id to correlate the original upload, and each document's id to identify an individual extracted document. A split upload can produce several documents in one event. Your automation must deduplicate deliveries too; repeated requests must not create repeated rows.
Once Suparse receives 2xx, recovery from later workflow errors is your integration's responsibility.
Events and request format
Suparse sends an HTTP POST with Content-Type: application/json. A task represents the original upload. When Suparse splits a file, it sends one task event after the split documents finish processing, rather than a separate event for each split document. A completed event can include one result entry for each processed document. Tasks waiting for a password or classification approval do not emit a task event until that work is resolved.
Create and enable the endpoint with the desired subscriptions before processing documents. Adding an endpoint, enabling it, or subscribing to an event does not backfill tasks that have already finished.
Suparse records one terminal event decision per task. Reprocessing the same task after that decision does not produce a new event; a new upload creates a new task. Delivery order across tasks is not guaranteed.
The JSON envelope has these common fields:
| Field | Meaning |
|---|---|
webhook_version | Payload contract version. Currently "1". |
id | Stable event ID. Use it as your idempotency key within each configured endpoint or integration. |
type | task.completed, task.failed, or endpoint.test. |
created_at | Event creation time in UTC. |
team_id | ID of the Suparse team that owns the task. |
data | Event-specific task details and, for completed events, results. |
The request includes these headers:
| Header | Meaning |
|---|---|
X-Suparse-Event | Event type, such as task.completed. |
X-Suparse-Event-Id | Same ID as the envelope's id. |
X-Suparse-Delivery-Id | ID for this endpoint delivery. It stays the same across automatic attempts and changes for a manual redelivery. |
X-Suparse-Attempt | One-based attempt number for this delivery. |
X-Suparse-Timestamp | Unix timestamp in seconds used to sign this request. |
X-Suparse-Signature | HMAC-SHA256 signature in the form v1=<hex digest>. |
User-Agent | Suparse-Webhooks/1. |
Route using the signed body's type. Allow extra JSON fields, and explicitly handle or alert on unsupported webhook_version values. Acknowledge event types your integration intentionally ignores without triggering business actions.
task.completed
Completed events include a task summary and one of two result forms. An inline payload contains documents directly. If the results make the request body larger than 1 MiB, Suparse sends a reference payload instead.
Example inline event:
{
"webhook_version": "1",
"id": "e4f86d56-b6ca-4fc1-88e8-616af731e05d",
"type": "task.completed",
"created_at": "2026-10-15T12:30:00.000Z",
"team_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"task": {
"id": "11111111-2222-4333-8444-555555555555",
"status": "completed",
"outcome": "extracted",
"ingest_source": "upload",
"created_at": "2026-10-15T12:25:00.000Z",
"completed_at": "2026-10-15T12:30:00.000Z",
"original_file": {
"name": "invoice-batch.pdf",
"content_type": "application/pdf",
"page_count": 2
}
},
"result_mode": "inline",
"documents": [
{
"id": "22222222-3333-4444-8555-666666666666",
"file_name": "invoice-batch.pdf",
"page_start": 1,
"page_end": 1,
"page_count": 1,
"template_id": "33333333-4444-4555-8666-777777777777",
"extraction_data": {
"invoice_number": "INV-1042",
"total": 125.5
}
}
]
}
}The keys in extraction_data depend on the selected template. ingest_source, original_file.content_type, and original_file.page_count may be null. In a result document, page_start, page_end, page_count, and template_id may also be null.
When result_mode is reference, the envelope and task summary stay the same, but data.documents is replaced by document_count and results_url. The data object has this form (task is abbreviated):
{
"task": {
"id": "55555555-6666-4777-8888-999999999999",
"status": "completed",
"outcome": "extracted"
},
"result_mode": "reference",
"document_count": 125,
"results_url": "https://api.suparse.com/api/v1/tasks/55555555-6666-4777-8888-999999999999/results?template_group_ids=33333333-4444-4555-8666-777777777777"
}Use the results_url supplied in the event rather than constructing a task URL yourself. The URL includes the selected template-group filters, if any. Fetch it with an API key in X-API-Key or a bearer token for the same team:
GET /api/v1/tasks/55555555-6666-4777-8888-999999999999/results?template_group_ids=33333333-4444-4555-8666-777777777777&limit=100
X-API-Key: <your-suparse-api-key>The response contains a documents array and next_cursor. This example shows one document from a page; the cursor value is illustrative:
{
"documents": [
{
"id": "22222222-3333-4444-8555-666666666666",
"file_name": "invoice-archive.pdf",
"page_start": 1,
"page_end": 1,
"page_count": 1,
"template_id": "33333333-4444-4555-8666-777777777777",
"extraction_data": {
"invoice_number": "INV-1042",
"total": 125.5
}
}
],
"next_cursor": "opaque-next-page-cursor"
}The page size can be from 1 to 100. When more results are available, next_cursor is an opaque string; otherwise it is null. Request the next page with that cursor and keep the same task, template_group_ids, and other filters from the results_url. The cursor is tied to those filters; changing or dropping a filter can make it invalid. Stop when next_cursor is null.
Reference results are live task results, not a snapshot stored in the webhook event. Later changes to a task or its results can change what the URL returns. Retrieval uses the authorization and team context of your API key or bearer token; a deleted or inaccessible task cannot be fetched.
task.failed
Failed events include the task summary and an error. They do not contain a result_mode or documents array.
{
"webhook_version": "1",
"id": "a691d7aa-387a-4c7e-8f3f-10bdc7557810",
"type": "task.failed",
"created_at": "2026-10-15T12:30:00.000Z",
"team_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"task": {
"id": "44444444-5555-4666-8777-888888888888",
"status": "failed",
"ingest_source": "email",
"created_at": "2026-10-15T12:25:00.000Z",
"completed_at": "2026-10-15T12:30:00.000Z",
"original_file": {
"name": "receipt.pdf",
"content_type": "application/pdf",
"page_count": 1
}
},
"error": {
"code": "processing_failed",
"message": "Document processing failed."
}
}
}The error.code is processing_failed or insufficient_credits. For example, the latter has the message Insufficient credits to process this task.
endpoint.test
Choose Send test in the dashboard to send a synthetic event. It exercises the configured URL, custom headers, and signature, but does not represent a real task or check the endpoint's event and task filters.
{
"webhook_version": "1",
"id": "b2c1df61-0d40-4db3-b877-9b8282b42242",
"type": "endpoint.test",
"created_at": "2026-10-15T12:30:00.000Z",
"team_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"test": true,
"task": {
"id": "00000000-0000-4000-8000-000000000001",
"status": "completed"
},
"result_mode": "inline",
"documents": [
{
"id": "00000000-0000-4000-8000-000000000002",
"file_name": "sample-invoice.pdf",
"page_start": 1,
"page_end": 1,
"page_count": 1,
"template_id": "00000000-0000-4000-8000-000000000003",
"extraction_data": {
"invoice_number": "SAMPLE-100",
"total": 42.5
}
}
]
}
}Verify the signature
Verify each request before trusting or processing its body. The signing secret begins with whsec_; decode the remaining Base64URL text to obtain the 32-byte signing key. Suparse signs the exact raw request body bytes with HMAC-SHA256 using this message:
<X-Suparse-Timestamp>.<X-Suparse-Delivery-Id>.<raw request body>The X-Suparse-Signature header contains v1= followed by the lowercase hexadecimal HMAC. The JSON body must not be parsed and re-serialized before verification; whitespace and byte encoding are part of the signature.
This Node.js/Express example demonstrates verification, not a complete receiver. You must implement saveOrEnqueueEventOnce before running it. That operation must atomically store or enqueue the event with a unique key, treat an already accepted key as success, and throw if durable acceptance fails. An in-memory set is not sufficient across restarts or multiple server instances.
Set SUPARSE_WEBHOOK_SECRET to the endpoint's signing secret. Register this route before any JSON body parser; it needs the original bytes. The body limit is 1 MiB. The five-minute replay window is an example; keep your server clock synchronized.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const secretText = process.env.SUPARSE_WEBHOOK_SECRET;
if (!secretText?.startsWith("whsec_")) {
throw new Error("SUPARSE_WEBHOOK_SECRET must be a Suparse signing secret");
}
const signingKey = Buffer.from(secretText.slice("whsec_".length), "base64url");
if (signingKey.length !== 32) {
throw new Error("The Suparse signing secret must decode to 32 bytes");
}
app.post("/webhooks/suparse/invoices", express.raw({ type: "application/json", limit: 1024 * 1024 }), async (req, res) => {
if (!Buffer.isBuffer(req.body)) return res.sendStatus(400);
const timestamp = req.get("X-Suparse-Timestamp") ?? "";
const deliveryId = req.get("X-Suparse-Delivery-Id") ?? "";
const signature = req.get("X-Suparse-Signature")?.match(/^v1=([0-9a-f]{64})$/i);
const timestampSeconds = Number(timestamp);
if (
!/^\d+$/.test(timestamp) ||
!Number.isSafeInteger(timestampSeconds) ||
Math.abs(Math.floor(Date.now() / 1000) - timestampSeconds) > 300 ||
!deliveryId ||
!signature
) {
return res.sendStatus(401);
}
const expected = createHmac("sha256", signingKey)
.update(`${timestamp}.${deliveryId}.`, "utf8")
.update(req.body)
.digest();
const received = Buffer.from(signature[1], "hex");
if (received.length !== expected.length || !timingSafeEqual(received, expected)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString("utf8"));
} catch {
return res.sendStatus(400);
}
if (typeof event?.id !== "string" || typeof event?.type !== "string") {
return res.sendStatus(400);
}
// Use a key scoped to this configured endpoint/integration.
try {
await saveOrEnqueueEventOnce(`invoices:${event.id}`, event);
} catch {
return res.sendStatus(500);
}
return res.sendStatus(204);
});
app.listen(3000);Process accepted events asynchronously using the workflow above. Test duplicate handling and both result modes before production: Send test only sends an inline payload. Use a local reference-event fixture with a mocked paginated results response to exercise that branch; unsigned fixtures should not pass your public receiver's signature check.
Acknowledge and recover deliveries
Suparse treats any 2xx response as an acknowledgement. A request that receives no response, times out, or receives a retryable response may be sent again even if your service already started processing it. Use the event envelope's id as an idempotency key scoped to a configured endpoint or integration; a manual redelivery keeps that event ID but receives a new delivery ID. Return a 2xx for an event you have already accepted.
Suparse waits up to 30 seconds for a response. Return 2xx only after storing the event or placing it on a durable queue. A redirect is not an acknowledgement, so configure the final HTTPS URL directly.
Suparse retries network failures, timeouts, 408, 409, 425, 429, and 5xx responses. Other non-2xx responses, including redirects and other 4xx responses, fail without automatic retry. A valid Retry-After on 429 or 503 can extend the retry delay. Automatic delivery makes up to eight attempts, with increasing delays of about 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours, 1 day, and 2 days between attempts. Test events are sent once and are not automatically retried.
Open an endpoint's Delivery history in the dashboard to review its delivery status, HTTP status, error details, attempt history, and exact request body. Pending deliveries are waiting for the next attempt. Failed deliveries are terminal; review the last HTTP status and attempt history to diagnose them. You can manually redeliver a retained task event from the history after fixing your receiver. The endpoint must be enabled; the redelivery uses the endpoint's current URL, headers, and signing secret, while keeping the original event payload. Test events cannot be manually redelivered; use Send test again.
Event history and saved payloads remain available for 30 days after all deliveries for that event have finished. A manual redelivery extends this window. Save events in your own system if you need them for longer.
Multiple endpoints
The same task event can reach multiple endpoints with the same envelope id, while template filters give each endpoint a different documents array. There is no signed endpoint ID header. Give each endpoint a distinct receiver path and deduplicate by that path or integration plus the event id. Do not deduplicate by delivery ID: manual redelivery creates a new one.
Endpoint changes and secret rotation
Changing an endpoint URL, custom headers, or enabled state cancels pending deliveries; a request already in progress may still finish. Disabling an endpoint stops new task events and does not backfill tasks completed while it is disabled. To rotate a secret safely, disable the endpoint, rotate the secret, update your receiver, enable the endpoint, and send a test. Rotation takes effect immediately for new requests, though a request already in progress may still arrive with the previous secret. If your receiver can, accept both secrets briefly during the handoff. After re-enabling, you can manually redeliver retained cancelled or failed task events from delivery history; the redelivery uses the current endpoint settings and the original event payload.
Event and template filters
Subscribe to task.completed, task.failed, or both. Optional filters narrow deliveries by the original task's upload source (email or upload) and template groups. An emailed task still matches email when split into several documents.
- Completed events: “All templates” includes every result. Specific-template endpoints receive events only when at least one result matches, and include only matching results. Without a template filter,
documentscan be empty;task.outcomeisextractedorno_matching_documents, and either outcome may have no results. - Failed events: a specific-template filter requires at least one accessible document assigned to a selected template group. For split tasks, this checks the split documents; otherwise, it checks the original. The matching document need not be the one that failed. Failures before template assignment only reach endpoints without a template filter.
Filter changes affect future task events. Queued deliveries and manual redeliveries retain their original payload and document selection. Send test bypasses these filters.
Troubleshooting
| Symptom | What to check |
|---|---|
| Signature verification fails | Use the current endpoint secret, preserve the exact raw body, allow a parser limit of at least 1 MiB, and check that the timestamp is within your replay window. During rotation, an in-flight request may still use the previous secret. |
| No task events arrive | Confirm the right team is selected, the endpoint is enabled, and its event, upload-source, and template filters match. Filters apply to future events; enabling an endpoint does not backfill tasks processed while it was disabled. A test event bypasses task filters. |
| A delivery is marked failed | Review the HTTP status and attempts in Delivery history. Any 2xx acknowledges the request; most 4xx and redirects are permanent failures, while the retryable statuses listed above are retried. |
| Reference results cannot be fetched | Authenticate with credentials for the task's team, keep the supplied template filters and cursor, and check that the task is still available to that team. Reference results are live and are subject to task deletion and access changes. |