Webhooks
Subscribe to pipeline events instead of polling. Every delivery is signed, retried with backoff, and idempotent on the receiver.
Create a webhook
webhooks:writeOnly https URLs are accepted in production. The signing secret is returned once in the create response and never again.
| Parameter | Type | Description |
|---|---|---|
name (required) | string | Human label shown in Admin → Webhooks. |
url (required) | string | HTTPS endpoint. Non-TLS is rejected. |
events | string[] | Event ids, or ["*"] for all. Defaults to all. |
auth | enum | none, basic or apikey, plus the matching credential block. |
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": "..." }
}'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 nowEvent catalogue
| Event | Fires when |
|---|---|
job.started | A submission is accepted and the first stage begins. |
job.completed | Every stage finished, successfully or not. |
job.failed | A stage errored and the job stopped. |
document.extracted | Extract produced fields for one document. |
doc.quarantined | Intake rejected a file — AV, size, or type. |
review.approved | An operator cleared every exception on a document. |
review.rejected | A verifier sent a correction back. |
review.escalated | A document was escalated out of its queue. |
pipeline.deployed | A new pipeline version was published. |
// 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.
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.
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)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));
}