# Webhook overview

> How Shoplazza webhooks work end to end: prerequisites, registering a subscription, the delivery format and headers, delivery rules, and signature verification.

# Webhook Overview

Webhooks let your app **passively receive** store events — when an order is created, a product is updated, or an app is uninstalled, Shoplazza actively POSTs the event payload to the URL you registered in advance.

:::tip
Want the event list directly? Go to [Webhook Events](/docs/app/building-blocks/webhooks/supported-webhook-events).
:::

## Prerequisites

Before your endpoint can receive a webhook:

- The merchant has installed your app on their store.
- Your app has subscribed to the event (topic) at a specific API version.

## Register a subscription

Send an authenticated `POST` to the webhooks endpoint — every request carries an `Access-Token: <access_token>` header.

`POST https://<shop>/openapi/2022-01/webhooks`

Request body:

```json5
{
  "topic": "products/create", // event to subscribe to; see Webhook Events for the full list
  "address": "https://your-app.example.com/webhook/products-create" // your HTTPS endpoint
}
```

Response — the created subscription object:

```json
{
  "id": "1234",
  "topic": "products/create",
  "address": "https://your-app.example.com/webhook/products-create",
  "format": "json",
  "created_at": "2024-01-15T08:30:00Z",
  "updated_at": "2024-01-15T08:30:00Z"
}
```

:::note
The request body differs by API version. The example above is for `2022-01`, which takes a flat body. From `2025-06` onward, the body must wrap the subscription in a `webhook` object:

```json
{ "webhook": { "topic": "products/create", "address": "https://your-app.example.com/webhook/products-create" } }
```
:::

Create, query, update, and delete subscriptions with the full [Webhook API](/api/webhooks). Each subscription object (`id`, `topic`, `address`, `format`, `created_at`, `updated_at`) is documented there.

## How it works

```mermaid
flowchart TD
    A[App calls POST /webhooks to register a subscription] --> B[Shoplazza stores the subscription: topic, address]
    B --> C[Event fires: Shoplazza POSTs the payload to your address]
    C --> D[Your endpoint verifies, processes, and returns a status within 5s]
    D --> E{Response?}
    E -- 2xx --> G[Success]
    E -- 5xx --> R[Retry]
    E -- 4xx / timeout / connection failure --> X[No retry, message dropped]
    R -- redelivered with growing delay --> C
    R -. still failing after ~11 tries over ~24h .-> X
```

## Delivery format

When an event fires, Shoplazza sends an HTTP `POST` with a JSON body and a set of headers.

### Request headers

| Header | Description | Example |
|---|---|---|
| `X-Shoplazza-Topic` | The event that triggered this delivery. | `orders/create` |
| `X-Shoplazza-Hmac-Sha256` | Base64-encoded HMAC-SHA256 signature of the request body, keyed by your app's Client Secret. | |
| `X-Shoplazza-Shop-Domain` | Domain of the store that generated the event. | `example.myshoplaza.com` |
| `X-Shoplazza-Api-Version` | API version used to serialize the payload. | `2025-06` |
| `X-Shoplazza-Deduplication-ID` | Deduplication ID for this delivery; unchanged across retries of the same event. | |

### Request body

The body is the resource payload for the topic. For example, [`products/update`](/docs/app/building-blocks/webhooks/supported-webhook-events/products/update):

```json5
{
  "product": {
    "id": "325431fd-102b-4374-9769-2b3c171f28e8",
    "title": "ICOICE Ocean Green | 1 Year",
    "vendor": "ICOICE",
    "published": true,
    "inventory_quantity": 78386,
    "created_at": "2023-08-10T06:58:08Z",
    "updated_at": "2026-06-22T10:54:12Z"
    // ...more fields: handle, tags, image, images, options, variants
  }
}
```

See the [products/update event page](/docs/app/building-blocks/webhooks/supported-webhook-events/products/update) for the full payload structure.

## Delivery rules

- **Timeout**: Your endpoint must return a `2xx` status within **5 seconds**.
- **Success**: Any `2xx` status is treated as success.
- **Retry**: Only a `5xx` response triggers a retry. In production, a failed delivery is retried up to ~11 times over ~24 hours with growing intervals: 1m, 1m, 5m, 10m, 30m, 1h, 2h, then every 4h.
- **Exhausted**: After retries are exhausted, the delivery is dropped. The subscription is **not** removed.
- **Duplicates**: The same event may arrive more than once. Make your handler idempotent — for example, skip an event whose `X-Shoplazza-Deduplication-ID` you have already processed.

:::warning
Timeouts and connection failures are **not** retried — only an explicit `5xx` response is. Return `2xx` quickly and process the event asynchronously, so a slow handler doesn't cause you to miss events.
:::

## Verify the signature

After receiving a POST request delivered by Shoplazza, your app **must** verify the signature before trusting the payload.

Every webhook request carries a base64-encoded `X-Shoplazza-Hmac-Sha256` header, generated by running HMAC-SHA256 over the **raw request body** with your app's **Client Secret** and base64-encoding the result. To verify, compute the same value and compare it against the header using a time-safe comparison; a match means the payload is trustworthy. **Verify before your app responds to the webhook.**

:::note
The difference from the OAuth callback (Scenario 1): Webhook signs the **raw body** and outputs **base64**; OAuth signs the sorted query string and outputs hex.
:::

For algorithm steps and multi-language code (Ruby, Node.js), see [HMAC Signature Verification · Scenario 2](/docs/app/building-blocks/authentication/signature-verification).

## Related

- [Webhook Events](/docs/app/building-blocks/webhooks/supported-webhook-events) — Full list of topics and payload examples
- [HMAC Signature Verification](/docs/app/building-blocks/authentication/signature-verification) — General signature algorithm
- [Webhook API (OpenAPI)](/api/webhooks) — Subscription CRUD API
