# 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 webhook deliveries don't count against your monthly **request** quota.

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                                                                                                                                                                                         | 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              | `X-DZ-Signature: t=<ts>,v1=hex(hmac_sha256(secret, ts + "." + rawBody))` over the **raw body**, so you can verify it. Also sends an `X-DZ-Token: <secret>` header for no-code tools that only do header auth. | Same format: `X-DZ-Signature: t=<ts>,v1=hex(hmac_sha256(secret, ts + "." + rawBody))` over the **raw body**, with the `secret` returned once by `POST /v1/webhooks`. You can verify it. Webhooks registered before per-webhook signing keep an older signature you cannot verify: see [Signature](#signature). |
| Which orders it covers | `order.created` from **every** source (storefront, landing page, manual dashboard create, API) plus 6 status events, including `order.processing`                                                             | 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.

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](https://dzbuild.com/api-docs/webhooks/events.md).

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`/`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; 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. `since` filters on the order's creation time, so the sweep finds orders whose `order.created` you missed; for a missed status change, re-read the orders you still track and compare their `status`.

### What counts as a "success"

* HTTP 200, 201, 202, 204 (any 2xx) — success.
* HTTP 4xx other than `408` and `429` (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 via `POST /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, `408` and `429`: 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:

1. Split the header on `,` and read the `t` and `v1` values.
2. Compute the HMAC-SHA256 of `t + "." + raw_body` with your webhook secret, where `raw_body` is the exact bytes you received, and hex-encode it.
3. Compare the result with `v1` in constant time.
4. Reject the delivery when `t` is 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](https://dzbuild.com/api-docs/webhooks/verifying-signatures.md).

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

1. **Verify the signature before you act.** Reject any delivery whose `v1` does not match, before you parse the JSON or touch an order.
2. **Serve HTTPS.** `POST /v1/webhooks` refuses any `url` that is not `https://`, and certificates are checked strictly.
3. **Check the timestamp is within 5 minutes** of your server's clock — cheap replay protection.
4. **Use raw body bytes** for the HMAC. Don't re-serialize the JSON.
5. **Be idempotent.** The same delivery can arrive more than once: after a 5xx, a `408`, a `429` or a timeout it is sent again with the same body. Dedupe only on the `delivery_id` inside 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                                                                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`       | The event type (full list in [Event catalog](https://dzbuild.com/api-docs/webhooks/events.md)).                                                |
| `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](https://dzbuild.com/api-docs/webhooks/events.md) 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 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](#retry-behaviour)).

## What's next

* [Registering](https://dzbuild.com/api-docs/webhooks/registering.md) — `POST /v1/webhooks` with full body, response, examples.
* [Event catalog](https://dzbuild.com/api-docs/webhooks/events.md) — every event with sample data payload.
* [Verifying signatures](https://dzbuild.com/api-docs/webhooks/verifying-signatures.md) — code in 4 languages.
