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.

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