Webhook setup #
Register a webhook, verify the signature, and replace polling with push notifications.
This guide takes you from "I'm polling /job/{id} in a loop" to "I receive
verified callbacks the moment a job completes." For the conceptual model
see Webhooks; for endpoint reference see
Webhooks API.
Step 1 · Stand up an HTTPS endpoint
The platform refuses to register a webhook unless the URL:
- Uses HTTPS (
http://is rejected). - Resolves to a public IP (loopback, RFC1918, and cloud-metadata ranges are blocked).
- Is ≤ 2083 characters.
If your handler runs in a private VPC, expose a reverse proxy with a public
DNS name. For local development, use a tunnel (Cloudflare Tunnel, ngrok,
Tailscale Funnel); localhost and tunnel-internal addresses are blocked.
A minimal handler in a few frameworks:
Step 2 · Register the webhook
POST /webhooks/ returns the id you'll reference and the secret_key
shown exactly once. Store the secret in your secret manager immediately.
curl -u "$DD_KEY:$DD_SECRET" \
-X POST https://api.datadistillers.com/api/v1/webhooks/ \
-H 'Content-Type: application/json' \
-d '{
"url": "https://api.example.com/dd-webhook",
"description": "Production extraction events",
"events": ["extraction.completed", "extraction.failed"]
}'
Response (201 Created):
{
"id": "wh_p7q8r9",
"url": "https://api.example.com/dd-webhook",
"events": ["extraction.completed", "extraction.failed"],
"is_active": true,
"created_at": "2026-05-03T09:00:00Z",
"updated_at": "2026-05-03T09:00:00Z",
"secret_key": "whsec_b2c3d4e5f6…"
}
secret_key is in this response and never again. Lose it and you'll need
to rotate to issue a new one.
Step 3 · Verify the signature on every delivery
The signature is HMAC-SHA256 over t={timestamp}.{raw_body}, hex-encoded.
Header format: t={timestamp},v1={hmac_hex}.
A drop-in verifier:
HMAC is computed over the bytes the server sent. If you parse JSON and then re-stringify, key ordering or whitespace can shift and the signature won't match. Always capture the raw body before parsing.
Step 4 · Attach the webhook to an extraction
Three ways the webhook gets selected for a given job. Highest precedence first:
webhook_idon the request: overrides everything, per-call.webhook_urlon the request: one-shot HTTPS URL; secret returned in submit response.- API-key default: set in dashboard. Fires on every job for the key unless overridden.
{
"filename": "invoice.pdf",
"artifact_type": "application/pdf",
"artifact_size": 184320,
"template_id": "tpl_invoice_v3",
"webhook_id": "wh_p7q8r9"
}
For most teams: register one webhook, set it as the API-key default, omit
webhook_id from every submit. Override per-request only when routing to a
tenant-specific endpoint.
Step 5 · Handle retries idempotently
Failed deliveries (anything not 2xx) retry on this schedule:
| Attempt | Delay |
|---|---|
| 1 | 0 |
| 2 | ~30s |
| 3 | ~5m |
| 4 | ~30m |
After attempt 4, delivery is abandoned. Use X-Webhook-Id (same value
across all retries, format {job_id}:{event_type}) as the idempotency key:
key = request.headers['X-Webhook-Id']
with db.tx():
if db.exists('webhook_events', key):
return 200, '' # already processed; ack
db.insert('webhook_events', key)
process(payload) # your business logic, exactly once
Returning 2xx promptly (under a few seconds) is more important than processing inline. Push the work into a queue and ack; that way slow downstream systems don't rack up retries.
Step 6 · Rotate the secret periodically
POST /webhooks/{id}/rotate-secret issues a new signing secret and keeps
the previous one valid for 24 hours.
curl -u "$DD_KEY:$DD_SECRET" \ -X POST https://api.datadistillers.com/api/v1/webhooks/wh_p7q8r9/rotate-secret
Response:
{
"id": "wh_p7q8r9",
"secret_key": "whsec_NEW_value_…",
"url": "https://api.example.com/dd-webhook",
"events": ["extraction.completed", "extraction.failed"]
}
During the overlap window, your verifier should accept either secret:
def verify_either(current, previous, header, body):
return verify(current, header, body) or (
previous and verify(previous, header, body))
Roll out the new secret to your handler, wait at least one delivery, and the old secret's overlap will expire automatically.
Debugging deliveries
Two endpoints for triage:
# All deliveries for a webhook (paginated, filterable by status/event) curl -u "$DD_KEY:$DD_SECRET" \ 'https://api.datadistillers.com/api/v1/webhooks/wh_p7q8r9/deliveries?status_filter=failed&limit=20' # All delivery attempts for a single job curl -u "$DD_KEY:$DD_SECRET" \ https://api.datadistillers.com/api/v1/job/job_8f3c2e1a/webhook-logs
Each entry shows the HTTP status your endpoint returned, the round-trip latency, the attempt number (1–4), and the first 4 KB of the response body. That's usually enough to pinpoint a 5xx, a timeout, or a verification-loop bug returning 401.