Webhooks
DocuClipper POSTs to your URL when an extraction job finishes — with the full extracted data in the body. You usually don't need to call any follow-up endpoint.
All webhook endpoints live under /api/v1/agent/webhooks/* and require a Personal Access Token (PAT) — see Authentication.
Registering a webhook
curl -X POST "https://www.docuclipper.com/api/v1/agent/webhooks" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.example.com/webhook",
"events": ["bank_statement.extraction.completed", "document.extraction.failed"]
}'Response:
{
"id": "42",
"url": "https://your-server.example.com/webhook",
"events": ["bank_statement.extraction.completed", "document.extraction.failed"],
"enabled": true,
"version": "2024-01-01",
"createdAt": "2026-05-26T15:00:00.000Z",
"secret": "2c5ff280d7c85fa01017d5a910eb0560e1fdfbc392bdcd4b09fb6f3c5c584dfb",
"secretShownOnce": true
}The secret is shown once at creation — store it now. Use it to verify the HMAC signature on every incoming delivery. If you lose it, rotate via POST /api/v1/agent/webhooks/:id/regenerate-secret.
Event types
Pass these in the events array. GET /api/v1/agent/webhooks/events returns the canonical list.
| Event | When |
|---|---|
| document.uploaded | Document finished uploading |
| document.extraction.completed | Generic completion event (fires alongside the doctype-specific event below) |
| bank_statement.extraction.completed | Bank-statement / check-image job succeeded (jobType=ExtractData) |
| invoice.extraction.completed | Invoice job succeeded (jobType=Invoice) |
| form.extraction.completed | Tax-form job succeeded (jobType=Form) |
| document.extraction.failed | Extraction failed — always subscribe to this so you don't silently miss errors |
| account.limit.approaching | Contract page usage approaching free / paid limit |
| fraud.detected | Fraud-detection signal raised on a document |
Delivery shape
Headers sent on every delivery:
Content-Type: application/json
X-DocuClipper-Event: bank_statement.extraction.completed
X-DocuClipper-Event-Id: evt_1779809462611_k9l75btue
X-DocuClipper-Version: 2024-01-01
X-DocuClipper-Signature: 94afe0a00e521c7a17a8a2b9da55fa621918c3df5e2f05cbf249e025f21462e9Body shape (bank-statement example):
{
"event": {
"id": "evt_1779809462611_k9l75btue",
"name": "bank_statement.extraction.completed",
"version": "2024-01-01"
},
"job": { "id": "12290" },
"user": { "id": "23" },
"webhook": { "id": "42" },
"data": {
"2666907": { /* document id */
"2915192377": { /* account number (bank/check) */
"bankMode": { "transactions": [ /* … */ ], "totalCredits": "…", "totalDebits": "…" },
"metadata": [ /* startBalance, endBalance, isReconciled, … */ ]
},
"metadata": []
}
}
}For per-doctype response shapes, see the dedicated pages: bank, check, invoice, tax form.
Long files. A file over 250 pages is converted in parts, sometimes as several jobs. You still get one event, under the job.id you created, when every part is done. data covers every part that succeeded, and job.upload lists the parts with their status (Succeeded or PartiallySucceeded for the whole upload). If a failed part is converted again later, a second event follows under the same job.id: every event is a full snapshot of the upload, so replace what you stored for that job.id rather than adding to it. See Async processing.
Verifying signatures
Compute HMAC-SHA256 of the raw request body with your webhook secret and compare the hex digest to X-DocuClipper-Signature in constant time. Always use the raw bytes — re-serializing the JSON will mismatch.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// IMPORTANT: capture the raw body before JSON.parse, the signature is over bytes
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const sig = req.header("X-DocuClipper-Signature") ?? "";
const expected = createHmac("sha256", process.env.WEBHOOK_SECRET).update(req.body).digest();
const received = Buffer.from(sig, "hex");
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body.toString());
// Acknowledge quickly; do heavy work in a background task.
res.status(200).send("ok");
handle(event);
});Retries & failure modes
- Non-2xx responses (or no response within 30s) are retried with exponential backoff. After 3 failed attempts the delivery is marked
dead-letterand stops retrying. - Return
2xxas fast as you can and do the heavy work in a background task / queue. Long handlers cause timeouts → retries → duplicates. - Use
X-DocuClipper-Event-Idto dedupe at-least-once delivery on your side. - Inspect delivery history at
GET /api/v1/agent/webhooks/:id/deliveries. Retry a failed delivery withPOST /api/v1/agent/webhooks/deliveries/:id/retry.
Managing webhooks
| Endpoint | Use |
|---|---|
| GET /webhooks | List your contract's webhooks |
| POST /webhooks | Create a webhook (returns secret once) |
| GET /webhooks/:id | Fetch one webhook (no secret) |
| PUT /webhooks/:id | Update URL / events |
| DELETE /webhooks/:id | Revoke / clean up |
| POST /webhooks/:id/test | Fire a sample delivery to your URL |
| POST /webhooks/:id/regenerate-secret | Rotate the signing secret |
| GET /webhooks/:id/deliveries | Recent delivery log |
| POST /webhooks/deliveries/:id/retry | Replay a failed delivery |