# Event catalog

Every webhook payload is wrapped:

```
{

  "event":       "<event name>",

  "store_id":    13,

  "occurred_at": "2026-04-30T21:18:21+00:00",

  "data":        { /* event-specific, documented below */ },

  "delivery_id": "9f2c41ab77e05d18"

}
```

This page documents the `data` field for each event.

## Order events

API v1 order events skip dashboard and courier changes

An order status event fires when the status is changed through the API, with `PATCH /v1/orders/{id}` or `POST /v1/orders/{id}/cancel`, and when someone taps the confirm or cancel button on a Telegram new-order notification. Those two buttons fire `order.confirmed` and `order.cancelled` for storefront and landing-page orders too, which never sent `order.created`. Confirming or shipping from the DZBuild dashboard, bulk send-to-delivery, `POST /v1/orders/{id}/send-to-delivery` and automatic courier tracking updates all fire **nothing** here.

There are also 7 order statuses but only **5** status events: transitions into `pending` and into `processing` emit nothing at all. So an order can move `pending → processing → shipped` and you will see a single `order.shipped`.

If you need events that cover every order source and every status change, use the merchant Webhooks addon at `/dashboard/webhooks` instead — see the comparison in the [Webhooks overview](https://dzbuild.com/api-docs/webhooks/overview.md).

### `order.created`

Fires **only** for orders created through `POST /v1/orders`. Storefront checkouts, landing-page orders and manual dashboard orders do not fire this event — wire fulfilment to it and you will miss the overwhelming majority of a merchant's orders.

```
{

  "data": {

    "order_id":       6894,

    "order_number":   "ORD-13-20260317-1A2B3C4D",

    "customer_phone": "0555000000",

    "total":          1000

  }

}
```

Those four keys are the entire payload. Fetch `GET /v1/orders/{id}` if you need anything else.

### `order.confirmed`

Fires when status moves to `confirmed`. With the default **Stock deduction** setting (at confirmation), stock is decremented at this moment. On a store set to **As soon as the order arrives**, an order placed after that change had its stock deducted when it was created, and confirming it deducts nothing more.

```
{

  "data": {

    "order_id":   6894,

    "old_status": "pending",

    "new_status": "confirmed"

  }

}
```

### `order.shipped`

```
{ "data": { "order_id": 6894, "old_status": "processing", "new_status": "shipped" } }
```

`old_status` can be a state you were never told about — the move into `processing` in this example fired no event of its own.

### `order.delivered`

```
{ "data": { "order_id": 6894, "old_status": "shipped", "new_status": "delivered" } }
```

### `order.cancelled`

Fires when an order moves to `cancelled` from `pending`, `confirmed` or `processing` — those are the only states cancellation is allowed from. `POST /v1/orders/{id}/cancel` on a `shipped` or `delivered` order returns `400 bad_request` and no event fires. If the order was already in a committed-stock state, stock is restored before this event fires.

```
{ "data": { "order_id": 6894, "old_status": "confirmed", "new_status": "cancelled" } }
```

### `order.returned`

```
{ "data": { "order_id": 6894, "old_status": "delivered", "new_status": "returned" } }
```

## Payment events

### `payment.received`

**Reserved — not currently emitted.** The name is accepted in the `events` array at registration and appears in `allowed_events`, but nothing fires it. Payment status changes will not reach your endpoint; read `GET /v1/orders/{id}` if you need it.

## Signup / event tracking

### `signup.counted`

Fires when a `/v1/signups` call lands and was actually counted (not a duplicate).

```
{

  "data": {

    "key_id":  "dzpub_live_0123456789abc",

    "source":  "landing-page-1",

    "country": "DZ"

  }

}
```

For privacy reasons we do NOT echo back `email`, `phone`, or `external_user_id` in the webhook — your own systems already have those values. The webhook is the "this signup was counted, please mirror it to your CRM" signal.

### `event.recorded`

**Reserved — not currently emitted.** `POST /v1/events` records the event and increments usage, but fires no webhook.

## Product events

### `product.stock_low`

**Reserved — not currently emitted.** There is no low-stock push today.

The underlying field is real, though: `GET /v1/products/{id}` returns `low_stock_alert` alongside `stock_quantity` under `inventory`, so you can poll for the condition yourself.

## Internal / test

### `webhook.test`

Fired by `POST /v1/webhooks/{id}/test`. Lets you check your endpoint is reachable without waiting for a real event.

```
{ "data": { "ts": 1717112657 } }
```

It cannot be subscribed to — including `webhook.test` in the `events` array at registration returns `400 bad_request` `"unknown event: webhook.test. Allowed: …"`. A test is delivered to the webhook you call it on regardless of that webhook's subscription list.

## Headers (every event)

```
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>
```

`X-DZ-Delivery-Id` is a numeric delivery id (e.g. `4127`). It is stable across every retry of that delivery, and it is **not** the same value as the 16-hex `delivery_id` in the JSON body. The signature does not cover this header, so dedupe on the body's `delivery_id`.

## Versioning

We add new events under v1 freely (additive). When we change the **shape of an existing event's `data`**, that's a v2-required change and gets a new path prefix. So your code can rely on:

* `event` is stable.
* New top-level fields may appear in `data`.
* Existing field types and meanings won't change without a v2.
* Order of `data` keys is **not** guaranteed.
