Docs/API reference/Webhooks
Reference

Webhooks

Get called when a pipeline stage starts or finishes instead of polling. Every delivery is signed with HMAC-SHA256 over the raw body and retried with exponential backoff.

How a webhook fires ​

A webhook is a workspace-level endpoint definition. It receives events in two ways:

  • Stage bindings. A pipeline stage can name an entry webhook, called when the stage starts (<stage>.entered), and an exit webhook, called when it finishes (<stage>.exited). In Studio these are set in the Webhooks section of a stage inspector.
  • Event subscriptions. A webhook's events list opts it in to the job events in the catalogue below. These go only to webhooks that the job's pipeline references in a stage binding; a webhook with no events receives its stage-binding events only.

A job captures its pipeline's webhooks when it is submitted, so changing a binding affects jobs submitted afterwards.

Create a webhook ​

POST/v1/webhooksRequires webhook:write

Webhooks can also be created in Studio under Settings → Integrations → Webhooks.

ParameterTypeDescription
name (required)stringLabel shown in the webhook list and the stage picker. Unique within the workspace — a duplicate returns 409 WEBHOOK_NAME_CONFLICT.
url (required)stringHTTPS endpoint. Private, loopback and link-local addresses are rejected.
auth_type (required)stringHow IDPForge authenticates to your endpoint: NONE, API_KEY or BASIC.
authobjectFor API_KEY: api_key, plus header_name (default X-API-Key). For BASIC: username and password.
eventsstring[]Job events to receive in addition to stage bindings, for example ["job.failed"].
headersobjectStatic headers sent with every delivery. Names starting with X-IDPForge- are reserved and rejected.
descriptionstringFree text.

The 201 Created response includes hmac_secret, the signing secret for this webhook. It is returned once; later reads show ***. Store it with the rest of your secrets. POST /v1/webhooks/{id}/rotate issues a new secret, also returned once.

bash
curl -X POST https://api.us.idpforge.cloud/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",
    "auth_type": "API_KEY",
    "auth": { "header_name": "X-API-Key", "api_key": "..." },
    "events": ["job.failed", "job.review_required"]
  }'

Event catalogue ​

Stage events ​

Delivered to the webhook bound to that stage's entry or exit.

EventFires when
source.exitedThe file is validated and stored, and the job is queued.
initialization.entered / initialization.exitedInitialization — validation, DPI normalisation, rasterisation — starts / finishes.
parse.entered / parse.exitedParse starts / finishes for a document.
split_classify.entered / split_classify.exitedSplit and classify starts / finishes for a document; on exit, child documents exist and are routed.
extract.entered / extract.exitedExtraction starts / finishes for a document.
post_processing.entered / post_processing.exitedPost-processing starts / finishes. Fires only when the stage is enabled.
destination.entered / destination.exitedResult delivery starts / completes. destination.exited also goes to webhooks that list it in events, so it works as a job-complete signal.

Parse, split and classify, extract and post-processing fire once per document and set document_id. Studio offers entry and exit bindings on the initialization, parse, split_classify, extract and post_processing stages. A binding for source.entered or the edit stage is accepted but never fires.

Job events ​

Delivered to webhooks that list the event in events.

EventFires when
job.failedThe job reached failed.
job.completedThe job finished as completed_with_errors — at least one stage failed, but a partial result exists. A clean finish is signalled by destination.exited.
job.review_requiredA document in the job was parked for human review.
job.blockedA failure that needs an operator parked the job.
error_group.openedA blocked job started a new error group in the error monitor.
error_group.escalatedAutomatic retries for an error group ran out and the group became blocked.
error_group.resolvedA re-run resolved every job in an error group.

Job events carry an empty data object — read the job with GET /v1/jobs/{job_id} for the details.

Payload ​

Each delivery is a POST with a JSON body. Fields with no value are omitted.

FieldTypeDescription
idstringDelivery id — the same value as the X-IDPForge-Delivery-Id header. Use it to deduplicate.
typestringEvent type, for example extract.exited.
timestampintegerWhen the event occurred, in Unix epoch seconds (UTC).
job_idstringThe job. Always present.
document_idstringThe document, on per-document stage events.
stagestringThe stage, on stage events.
transitionstringentered or exited, on stage events.
result_urlstringWhere to fetch the stage's output, on *.exited events once output exists.
dataobjectOn *.exited events, data.job is a snapshot of the job in the GET /v1/jobs/{job_id} shape, without result. Otherwise empty.

The data.job snapshot above is abridged.

json
{
  "id": "0191f3a4-1b2c-7d3e-8f40-5a6b7c8d9e0f",
  "type": "extract.exited",
  "timestamp": 1789107238,
  "job_id": "0191f3a2-7c4e-7b1d-9a55-3e0c8f2d6b41",
  "document_id": "0191f3a3-9e8d-7c6b-a5f4-3e2d1c0b9a87",
  "stage": "extract",
  "transition": "exited",
  "data": {
    "job": {
      "job_id": "0191f3a2-7c4e-7b1d-9a55-3e0c8f2d6b41",
      "status": "processing",
      "execution_mode": "async",
      "priority": "standard"
    }
  }
}

Verifying the signature ​

Every delivery carries these headers, plus the credential for your auth_type and any static headers:

HeaderValue
X-IDPForge-Signaturesha256= followed by the lowercase hex HMAC-SHA256 of the raw request body, keyed with the webhook's signing secret.
X-IDPForge-Delivery-IdThe delivery id, equal to the payload's id. Unchanged across retries.
User-AgentIDPForge-Webhook/1.0

The signature covers the raw body only — there is no timestamp header and nothing else is signed. Use the signing secret exactly as it was returned; do not base64-decode it. Compute the HMAC over the bytes you received and compare in constant time.

Use the raw body

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

The body's timestamp is covered by the signature, but a retry resends the original body, so a late retry carries the original time. Deduplicate on the delivery id rather than rejecting deliveries by age.

python
import hashlib
import hmac

def verify(secret: str, signature_header: str | None, raw_body: bytes) -> bool:
    if not signature_header:
        return False
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
ts
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret: string, signatureHeader: string | null, rawBody: Buffer): boolean {
  if (!signatureHeader) return false;
  const expected = Buffer.from(
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"),
  );
  const received = Buffer.from(signatureHeader);
  return received.length === expected.length && timingSafeEqual(expected, received);
}

Retries ​

A delivery succeeds on any 2xx response. Anything else is retried: 3xx (redirects are not followed), 4xx, 5xx, a connection failure, or no response within 30 seconds (10 seconds to connect).

A delivery gets up to 10 attempts. The wait before each retry doubles, from 4 seconds to about 17 minutes, so a delivery that keeps failing is abandoned roughly 35 minutes after the first attempt. Every retry resends the same body, signature and delivery id. Return 2xx as soon as you have stored the event, and do the slow work afterwards.

Was this page helpful?
Last updated 11 Sept 2026