Skip to main content

Webhooks overview

How DZBuild webhooks work — what events fire, how delivery + retries behave, security guarantees, and what your endpoint must do.

Written by Support

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

/dashboard/webhooks — no code

POST /v1/webhooks — API only

Who can use it

Unlimited plan and up

Any store with a pilot-enrolled API key

Endpoints per store

1 on Unlimited, 3 on Enterprise

No limit is enforced

Signature

X-DZ-Signature: t=<ts>,v1=hex(hmac_sha256(secret, ts + "." + rawBody)) over the raw body — you can verify it. Also sends an X-DZ-Token: <secret> header for no-code tools that only do header auth.

Signed with a key you are not given — merchants cannot verify it today. See Signature.

Which orders it covers

order.created from every source (storefront, landing page, manual dashboard create, API) plus 6 status events, including order.processing

Only orders created or updated through the API

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

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 only see API traffic

order.created fires only for orders created via POST /v1/orders. Storefront checkouts, landing-page orders and manual dashboard orders fire nothing here. Same for status changes — 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 → re-queued, retried every minute
                              ├─ 4xx or 3xx → one attempt, no retry
                              └─ no response (timeout / DNS / TLS) → one attempt, no retry

Retry behaviour

⚠️ Warning — Retries in v1 don't behave like a normal backoff

Read this before you design around retries.

  • HTTP 5xx — the delivery is re-POSTed once per minute, indefinitely, until your endpoint returns 2xx or you delete the webhook. The interval never grows and the delivery is never given up on, so don't design around a backoff ladder — there isn't one.

  • Timeout, DNS failure, TLS failure — attempted exactly once, then abandoned. No retry, and the delivery is never picked up again.

  • HTTP 4xx and 3xx — attempted once, never retried. Redirects are not followed, so a 301/302 counts 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 — returning 5xx signs you up for a POST every 60 seconds forever.

  • Don't rely on a retry to cover a slow endpoint. A timeout is a single lost delivery. Persist the body to your own queue and ack immediately.

  • Reconcile with a poll. Because failures are silently abandoned, run a periodic GET /v1/orders?since=... sweep as a safety net.

What counts as a "success"

  • HTTP 200, 201, 202, 204 (any 2xx) — success.

  • HTTP 4xx (400, 401, 403, 404, 422 …) — not retried. Fix your endpoint and re-test via POST /v1/webhooks/{id}/test.

  • HTTP 3xx — not retried. We don't follow redirects; point the webhook at the final URL.

  • Timeout, DNS failure, TLS failure — not retried either. We verify TLS certificates strictly, so a self-signed cert fails here.

  • HTTP 5xx — retried, but see the loop warning above.

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: <numeric delivery id>

Signature

⚠️ Warning — API v1 signatures are not verifiable by merchants yet

X-DZ-Signature is not derived from the per-webhook secret returned by POST /v1/webhooks, and that secret is never given back to you in a form you could sign with — so verification code written against it rejects 100% of genuine deliveries.

Treat X-DZ-Signature as opaque until per-webhook signing ships. If you need a signature you can actually check, use the merchant Webhooks addon at /dashboard/webhooks, which signs each endpoint with that endpoint's own secret.

What you must do

  1. Re-read before you act. Since the signature isn't verifiable, treat the payload as a notification rather than as authenticated data — fetch the record with GET /v1/orders/{id} using your API key before you ship, charge, or fulfil anything.

  2. Make the URL unguessable. A long random path segment or a shared token in the query string is your practical authentication today.

  3. Check the timestamp is within 5 minutes of your server's clock — cheap replay protection.

  4. Use raw body bytes if you hash anything — don't re-serialize the JSON.

  5. Be idempotent — the same logical event MAY be delivered more than once (5xx re-queues). Dedupe on delivery_id or on 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 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

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

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 also stable across retries, but it does not equal the delivery_id in the body. Dedupe on one of them consistently — don't mix.

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 is re-POSTed every 60 seconds indefinitely (see Retry behaviour).

What's next

Did this answer your question?