Docs/API reference/Webhooks
Reference

Webhooks

Subscribe to pipeline events instead of polling. Every delivery is signed, retried with backoff, and idempotent on the receiver.

Create a webhook

POST/v1/webhooksRequires webhooks:write

Only https URLs are accepted in production. The signing secret is returned once in the create response and never again.

ParameterTypeDescription
name (required)stringHuman label shown in Admin → Webhooks.
url (required)stringHTTPS endpoint. Non-TLS is rejected.
eventsstring[]Event ids, or ["*"] for all. Defaults to all.
authenumnone, basic or apikey, plus the matching credential block.
bash
curl https://api.idpforge.ai/v1/webhooks \
  -H "X-API-Key: $IDP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "AP team — pipeline events",
    "url": "https://hooks.acme.com/ap-events",
    "events": ["job.completed", "review.approved"],
    "auth": "apikey",
    "apikey": { "location": "header", "name": "X-API-Key", "key": "..." }
  }'
ts
const hook = await idp.webhooks.create({
  name: "AP team — pipeline events",
  url: "https://hooks.acme.com/ap-events",
  events: ["job.completed", "review.approved"],
  auth: "apikey",
  apikey: { location: "header", name: "X-API-Key", key: process.env.HOOK_KEY! },
});
// hook.signingSecret — returned once, store it now

Event catalogue

EventFires when
job.startedA submission is accepted and the first stage begins.
job.completedEvery stage finished, successfully or not.
job.failedA stage errored and the job stopped.
document.extractedExtract produced fields for one document.
doc.quarantinedIntake rejected a file — AV, size, or type.
review.approvedAn operator cleared every exception on a document.
review.rejectedA verifier sent a correction back.
review.escalatedA document was escalated out of its queue.
pipeline.deployedA new pipeline version was published.
json
// delivered payload
{
  "event": "job.completed",
  "delivery_id": "dlv_2f91a7",
  "occurred_at": "2026-07-29T14:05:02Z",
  "workspace_id": "ws_3c71",
  "webhook_id": "whk_91be",
  "data": {
    "job_id": "job_8H2K3FQ",
    "pipeline_id": "std-invoice",
    "status": "completed",
    "documents": ["DOC-100245"]
  }
}

Verifying the signature

Every delivery carries X-IDPForge-Signature and X-IDPForge-Timestamp. Compute the HMAC over timestamp + "." + raw_body using your signing secret and compare in constant time. Reject anything older than five minutes.

Use the raw body

Parse the JSON after verifying. Re-serializing before you check the signature will fail on key order and whitespace.

Retries

Non-2xx responses are retried eight times with exponential backoff over roughly 24 hours. Persistent failure flips the webhook to failing and notifies workspace admins. Deliveries carry a stable delivery_id — use it to dedupe.

python
import hmac, hashlib, time

def verify(secret, headers, raw_body):
    ts  = headers["X-IDPForge-Timestamp"]
    sig = headers["X-IDPForge-Signature"].split("=")[1]
    if abs(time.time() - int(ts)) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{ts}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)
ts
import { createHmac, timingSafeEqual } from "crypto";

function verify(secret: string, headers: Headers, raw: Buffer) {
  const ts = headers.get("x-idpforge-timestamp")!;
  const sig = headers.get("x-idpforge-signature")!.split("=")[1];
  if (Math.abs(Date.now() / 1000 - +ts) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${ts}.`).update(raw).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
Was this page helpful?
Last updated 19 Aug 2026