> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive HTTP callbacks when things happen in your store — orders, payments, shipments, catalog, customers, store settings and marketplace sync.

Commerce Engine delivers events to HTTPS endpoints you register through the Admin API. Every event family in this section is a separate webhook you can subscribe to; each page documents the exact payload model under `payload.properties`.

<CardGroup cols={2}>
  <Card title="Register a webhook" icon="plus" href="/docs/admin/api-reference/webhooks/create-webhook">
    `POST /webhooks` on the Admin API — choose the events, get the signing secret once.
  </Card>

  <Card title="Browse events" icon="list" href="/docs/api-reference/webhooks/orders/order-created">
    Orders, Payments, Shipping, Invoices, Carts, Catalog, Coupons & promotions, Customers, Store, Marketplace.
  </Card>
</CardGroup>

## Request format

Every delivery is a `POST` with a JSON body shaped as `WebhookEnvelope`. The event-specific model sits at `payload.properties`; everything around it is common to all events.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "order.created",
  "event_id": "01J9…",          // same across retries — use as your idempotency key
  "delivery_id": "01J9…",       // unique per attempt
  "timestamp": "2026-09-21T07:04:58.869123+00:00",
  "is_test": false,
  "payload": {
    "event": "order.created",
    "event_id": "01J9…",
    "store_id": "your_store_id",
    "store_type": "b2c",
    "user_id": "usr_…",
    "admin_user_id": null,
    "timestamp": "2026-09-21T07:04:58.512345Z",
    "properties": { "order_number": "ORD-1001", "status": "confirmed", "…": "…" }
  }
}
```

| Header | Value |
| - | - |
| `X-CE-Event` | Event name, identical to `event` in the body |
| `X-CE-Signature` | `sha256=<hex>` — HMAC-SHA256 of the raw request body, keyed with your webhook secret |
| `X-CE-Event-ID` | Same as `event_id` |
| `X-CE-Delivery-ID` | Same as `delivery_id` |
| `User-Agent` | `CommerceEngine-Webhooks/1.0` |

Any `custom_headers` you set on the subscription are added to every request.

## Verify the signature

Compute the HMAC over the **raw** body bytes — do not re-serialise the JSON — and compare with a constant-time function before you parse anything.

<CodeGroup>
  ```typescript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import crypto from "node:crypto";

  export function verifyCommerceEngineSignature(rawBody: string, signatureHeader: string | null, secret: string): boolean {
    if (!signatureHeader?.startsWith("sha256=")) return false;
    const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    const received = signatureHeader.slice("sha256=".length);
    return (
      expected.length === received.length &&
      crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(received, "hex"))
    );
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import hashlib, hmac

  def verify_commerce_engine_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
      if not signature_header or not signature_header.startswith("sha256="):
          return False
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature_header[len("sha256="):])
  ```
</CodeGroup>

<Warning>
  The secret is returned **once** when you create the subscription. Rotate it with [`POST /webhooks/{id}/rotate-secret`](/docs/admin/api-reference/webhooks/rotate-webhook-secret); deliveries are signed with the current secret only, so update your endpoint before rotating.
</Warning>

## Respond and retry

* Return any `2XX` as soon as you have persisted the event — do the real work asynchronously.
* Non-`2XX` responses and timeouts (30 s) are retried three times: after **5 s**, **30 s** and **5 minutes**. Every attempt carries a new `delivery_id` and the same `event_id`.
* Return `410 Gone` to disable the subscription permanently.
* Deliveries are at-least-once and may arrive out of order. De-duplicate on `event_id` and use the timestamps to resolve ordering.

## Subscriptions

* A store can have up to **5 enabled** webhooks. Each subscription lists the events it receives; unknown event names are ignored.
* Test any subscribed event with `POST /webhooks/{id}/test` — the body is a minimal payload with `is_test: true`. List the event names the platform knows about with [`GET /webhooks/event-types`](/docs/admin/api-reference/webhooks/list-all-event-types).
* Every delivery attempt is logged; inspect them with [`GET /webhooks/deliveries`](/docs/admin/api-reference/webhooks/list-all-deliveries-log), retry one with [`POST …/deliveries/{delivery_id}/retry`](/docs/admin/api-reference/webhooks/retry-webhook-delivery), or replay an event with [`POST /webhooks/{id}/replay-event/{event_id}`](/docs/admin/api-reference/webhooks/replay-event-to-webhook).

### Marketplace-scoped subscriptions

A subscription created with a `marketplace_id` only receives events whose `payload.properties.marketplace_listing[]` contains that marketplace — this is how a marketplace follows a seller store's listings, inventory, warehouses, `MS:`-prefixed shipment copies and seller invoices. The marketplace → seller direction (`seller.order.*`, `seller.shipment.*`) uses an ordinary, unscoped subscription on the marketplace store, because those payloads carry the target in `seller_id` instead.

<Note>
  Payload models are shared with the REST APIs wherever the shapes match; where an event publishes a snapshot that differs from the API model (for example the compact tombstone sent by `*.deleted` events), the event page documents the actual fields.
</Note>
