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 webhook deliveries don't count against your monthly request quota.
ℹ️ Info — There are two different DZBuild webhook systems
Pick the right one before you build.
Merchant Webhooks addon | API v1 webhooks (this section) | |
How you set it up |
|
|
Who can use it | Unlimited plan and up | Stores on an active Enterprise plan, with a merchant API key (installed-app tokens are refused) |
Endpoints per store | 1 on Unlimited, 3 on Enterprise | No limit is enforced |
Signature |
| Same format: |
Which orders it covers |
| Orders created or updated through the API, plus confirmations and cancellations made with the buttons on Telegram new-order notifications, storefront and landing-page orders included |
Delivered from | Cloudflare IP ranges | DZBuild's own egress addresses — ask support for the current list |
Extras | Delivery log UI, secret regeneration, HTTPS-only target validation, auto-disable after 10 consecutive failures | HTTPS-only target validation |
If all you need is reliable order notifications, the addon is the better product. Use API v1 webhooks when your integration already talks to the REST API.
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. Every order created through POST /v1/orders queues a delivery, and the queue drains continuously, so latency is typically under a minute. Zero polling, zero quota waste.
⚠️ Warning — API v1 webhooks skip storefront checkouts and dashboard changes
order.created fires only for orders created via POST /v1/orders. Storefront checkouts, landing-page orders and manual dashboard orders fire nothing here. Status changes made in the dashboard fire nothing either; only changes made through the API and the confirm and cancel buttons on Telegram new-order notifications do. See the Event catalog.
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
An API v1 write happens
│
▼
A delivery is queued for every webhook subscribed to that event
│
▼
The queue drains continuously — typically under a minute
│
▼
Signed body ─────► POST your URL (5 s connect, 10 s total)
│
├─ 2xx: mark delivered, done
├─ 5xx, 408, 429, timeout, DNS or TLS failure: retried with backoff
├─ any other 4xx, or a 3xx: dead-lettered at once, no retry
└─ 5 failed attempts: moved to the dead-letter queue, no more retries
Retry behaviour
HTTP 5xx, 408 and 429, timeout, DNS failure, TLS failure are retried. The first retry comes 1 minute after the failure, then 5 minutes, 30 minutes and 2 hours. After the 5th failed attempt the delivery moves to the dead-letter queue and is not sent again.
Any other HTTP 4xx, and every 3xx, is attempted once: the delivery goes to the dead-letter queue at once and is never retried. Redirects are not followed, so a
301/302counts as a failure.
There is no auto-disable. The webhook's failure_count increments once per failed attempt and resets to 0 on any success; status stays active.
Practical consequences:
Return 2xx fast. If you can't process a payload, still return 2xx and drop it; a 5xx costs you four more deliveries of the same body over about 2.5 hours.
Don't rely on retries for a slow endpoint. Five failures inside ~2.5 hours drop the delivery. Persist the body to your own queue and ack immediately.
Reconcile with a poll. Because a delivery is dropped after five failures, run a periodic
GET /v1/orders?since=...sweep as a safety net.sincefilters on the order's creation time, so the sweep finds orders whoseorder.createdyou missed; for a missed status change, re-read the orders you still track and compare theirstatus.
What counts as a "success"
HTTP 200, 201, 202, 204 (any 2xx) — success.
HTTP 4xx other than
408and429(400, 401, 403, 404, 422 and so on): not retried, the delivery goes straight to the dead-letter queue. Fix your endpoint and re-test viaPOST /v1/webhooks/{id}/test.HTTP 3xx: not retried, dead-lettered at once. We don't follow redirects; point the webhook at the final URL.
Timeout, DNS failure, TLS failure: retried like a 5xx. We verify TLS certificates strictly, so a self-signed cert fails every attempt.
HTTP 5xx,
408and429: retried on the schedule above, 5 attempts in total.
Security model
What we send
Content-Type: application/json User-Agent: dzbuild-webhook/1 X-DZ-Timestamp: <unix seconds> X-DZ-Signature: t=<unix seconds>,v1=<hex hmac-sha256> X-DZ-Delivery-Id: <numeric delivery id>
Signature
X-DZ-Signature is computed with the secret that POST /v1/webhooks returned when you registered the webhook:
X-DZ-Signature: t=<unix seconds>,v1=<hex hmac-sha256>expected = hex( hmac_sha256( WEBHOOK_SECRET, t + "." + raw_body ) )if (!constant_time_equal(expected, v1)) reject 401 if (abs(now - t) > 300) reject 401 # ±5 min replay window
To verify a delivery:
Split the header on
,and read thetandv1values.Compute the HMAC-SHA256 of
t + "." + raw_bodywith your webhook secret, whereraw_bodyis the exact bytes you received, and hex-encode it.Compare the result with
v1in constant time.Reject the delivery when
tis more than 300 seconds away from your clock.
Each attempt is signed when it is sent, so a retry carries a new t and a new v1 over the same body. Code in four languages is on Verifying signatures.
⚠️ Warning — Webhooks registered before per-webhook signing
A webhook created before DZBuild started signing with the per-webhook secret still gets the old signature: a bare hex value with no t= part, computed with a key DZBuild does not give out. You cannot verify it. Delete that webhook and register it again; the new registration returns a secret that signs every delivery.
What you must do
Verify the signature before you act. Reject any delivery whose
v1does not match, before you parse the JSON or touch an order.Serve HTTPS.
POST /v1/webhooksrefuses anyurlthat is nothttps://, and certificates are checked strictly.Check the timestamp is within 5 minutes of your server's clock — cheap replay protection.
Use raw body bytes for the HMAC. Don't re-serialize the JSON.
Be idempotent. The same delivery can arrive more than once: after a 5xx, a
408, a429or a timeout it is sent again with the same body. Dedupe only on thedelivery_idinside the signed body.
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 send API v1 deliveries from Cloudflare IP ranges, so allow-listing those blocks every delivery. If you need an IP allow-list, contact support for the current egress addresses — they can change. (The merchant Webhooks addon is the opposite: its deliveries do come from Cloudflare ranges.)
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": "9f2c41ab77e05d18"
}
Field | Notes |
| The event type (full list in Event catalog). |
| Your store id — useful if you have multiple webhooks pointed at the same handler. |
| When the event happened in our system, ISO 8601 with TZ. |
| Event-specific payload. See Event catalog for each event's shape. |
| 16-hex string, unique per delivery (one webhook × one event). It is byte-identical on every retry — that's what makes it usable for dedup. |
The X-DZ-Delivery-Id header is a different value: a numeric delivery id, e.g. 4127. It is stable across retries, but the signature does not cover it, so anyone who can reach your URL can send any value there. Dedupe only on the delivery_id in the signed body.
Quota
Webhook deliveries are not metered and not capped today. A webhooks_per_month figure is reported by GET /v1/usage and GET /v1/quotas, but nothing increments it and nothing enforces it. Delivery attempts also don't consume your requests_per_month API quota.
That's not a licence to be slow: a 5xx endpoint gets the same body up to four more times over about 2.5 hours (see Retry behaviour).
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.