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.
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 |