DocuClipper logo

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

bash
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:

json
{
  "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.

EventWhen
document.uploadedDocument finished uploading
document.extraction.completedGeneric completion event (fires alongside the doctype-specific event below)
bank_statement.extraction.completedBank-statement / check-image job succeeded (jobType=ExtractData)
invoice.extraction.completedInvoice job succeeded (jobType=Invoice)
form.extraction.completedTax-form job succeeded (jobType=Form)
document.extraction.failedExtraction failed — always subscribe to this so you don't silently miss errors
account.limit.approachingContract page usage approaching free / paid limit
fraud.detectedFraud-detection signal raised on a document

Delivery shape

Headers sent on every delivery:

http
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: 94afe0a00e521c7a17a8a2b9da55fa621918c3df5e2f05cbf249e025f21462e9

Body shape (bank-statement example):

json
{
  "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.

javascript
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-letter and stops retrying.
  • Return 2xx as fast as you can and do the heavy work in a background task / queue. Long handlers cause timeouts → retries → duplicates.
  • Use X-DocuClipper-Event-Id to dedupe at-least-once delivery on your side.
  • Inspect delivery history at GET /api/v1/agent/webhooks/:id/deliveries. Retry a failed delivery with POST /api/v1/agent/webhooks/deliveries/:id/retry.

Managing webhooks

EndpointUse
GET /webhooksList your contract's webhooks
POST /webhooksCreate a webhook (returns secret once)
GET /webhooks/:idFetch one webhook (no secret)
PUT /webhooks/:idUpdate URL / events
DELETE /webhooks/:idRevoke / clean up
POST /webhooks/:id/testFire a sample delivery to your URL
POST /webhooks/:id/regenerate-secretRotate the signing secret
GET /webhooks/:id/deliveriesRecent delivery log
POST /webhooks/deliveries/:id/retryReplay a failed delivery