Webhooks
Webhooks are real-time push notifications from DZBuild to your server when something happens on your store. Use them instead of polling — less load, lower latency, and (importantly) webhook deliveries don't count against your monthly request quota.
Why webhooks
Compare:
Polling — your code calls GET /v1/orders?since=... every minute. 1440 calls/day, 1440 round-trips, the API quota burns evenly, and the latency from "order created" to "your code knows" is 60 seconds.
Webhooks — you register https://yourapp/webhooks once. We POST to it every time an order is created. Latency is ~2-5 seconds. Zero polling. Zero quota waste.
The only time polling beats webhooks is when:
- Your endpoint can't be reached from the internet (then poll from inside your network).
- You don't have a server (use polling from a scheduled lambda / cron job).
How delivery works
Order/event happens on origin
│
▼
INSERT row in api_v1_webhook_deliveries
│
▼
Cron drains every minute (next_retry_at <= NOW())
│
▼
HMAC-sign body ─────► POST your URL with 10 s timeout
│ │
│ ├─ 2xx → mark delivered, done
│ ├─ 5xx → retry: 1m, 5m, 30m, 2h, 12h
│ ├─ 4xx → ack as poison; do NOT retry
│ └─ no response (timeout / DNS) → retry
│
└─ 5 failures → move to dead-letter queue (DLQ)
Retry schedule
After each failed attempt, next_retry_at is set to now() + delay[attempt]:
| Attempt | Delay |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
| 6th attempt | DLQ — manual intervention |
Total retry window is ~14.5 hours. After that the delivery is dead-lettered and your webhook's failure_count increments. Five consecutive dead letters auto-disable the webhook (status changes to dead); contact support to reset.
What counts as a "success"
- HTTP 200, 201, 202, 204 (any 2xx) — success.
- HTTP 4xx (400, 401, 403, 404, 422 …) — not retried. We treat 4xx as "your endpoint definitively rejects this payload"; further retries won't help. Fix your endpoint and re-test via
POST /v1/webhooks/{id}/test. - HTTP 5xx, timeout, DNS failure, TLS failure — retried.
If you want us to retry on a specific 4xx (rare), respond with 503 Service Unavailable instead of 4xx.
Security model
What we send
Content-Type: application/json
User-Agent: dzbuild-webhook/1
X-DZ-Timestamp: <unix seconds>
X-DZ-Signature: <hex hmac-sha256>
X-DZ-Delivery-Id: <id>
Signature
X-DZ-Signature = hex( hmac_sha256( WEBHOOK_SECRET, X-DZ-Timestamp + "." + sha256(body) ) )
The WEBHOOK_SECRET is what you got back when you registered the webhook (POST /v1/webhooks). We never expose it again — store it.
What you must do
- Verify the signature on every request (see Verifying signatures).
- Check the timestamp is within 5 minutes of your server's clock — rejects replays.
- Use raw body bytes for hashing — don't re-serialize the JSON.
- Be idempotent —
X-DZ-Delivery-Idis unique per attempt, but the same logical event MAY be delivered more than once (network blips, retries-after-success). Dedupe on your side usingdelivery_idor the event's own ids.
What we DON'T do
- We don't authenticate outbound with mTLS. If your endpoint requires it, set up a reverse proxy that strips/adds mTLS in front of your handler.
- We don't IP-lockout. We send from CF Workers — the source IP is in CF's ranges. If you want to whitelist, use Cloudflare's IP list.
Payload envelope
Every webhook body has the same outer shape:
{
"event": "order.confirmed",
"store_id": 13,
"occurred_at": "2026-04-30T21:18:21+00:00",
"data": { "order_id": 6894, "old_status": "pending", "new_status": "confirmed" },
"delivery_id": "abc123def456"
}
| Field | Notes |
|---|---|
event | The event type (full list in Event catalog). |
store_id | Your store id — useful if you have multiple webhooks pointed at the same handler. |
occurred_at | When the event happened in our system, ISO 8601 with TZ. |
data | Event-specific payload. See Event catalog for each event's shape. |
delivery_id | Unique per attempt. Use this for idempotency on your side. |
Quota
Every delivery attempt — success, 4xx, retry — counts as 1 against webhooks_per_month. A single event that retries 5 times costs 5. So:
- Keep your endpoint reliable (returns 2xx fast) → 1 attempt per event.
- Failures cost more → fix bugs quickly to avoid burning quota.
What's next
- Registering —
POST /v1/webhookswith full body, response, examples. - Event catalog — every event with sample data payload.
- Verifying signatures — code in 4 languages.