Build
Events & webhooks
Everything that happens in a store is a domain event, recorded transactionally with the change itself and delivered to your endpoints as HMAC-signed webhooks. Ten event types cover the storefront-relevant lifecycle.
The catalog
| Event | Type | Description |
|---|---|---|
| order.created | event | Checkout created a pending order. |
| order.paid | event | The provider webhook confirmed payment. |
| order.refunded | event | A refund settled (partial or full). |
| inventory.updated | event | Sellable stock changed for a variant. |
| product.created | event | A product became visible to the storefront. |
| product.updated | event | Product or variant fields changed. |
| product.deleted | event | A product left the catalog. |
| content.updated | event | A region was published or a page/menu changed. |
| theme.published | event | A theme version went live. |
| app.installed | event | A merchant installed your app. |
Delivery
▸ POST https://your-app.example.com/hooks/commerceos▸ x-webhook-event: order.paid▸ x-webhook-signature: 3f9a1c… # hex hmac-sha256 over the exact body▸ x-webhook-attempt: 1{ "id": "184", "event": "order.paid", "createdAt": "2026-07-07T14:16:02.000Z", "data": { "orderId": "4593eb78-…", "storeId": "91506f23-…", … }}◂ 2xx from you = delivered · anything else retries with backoff
Events are written in the same database transaction as the change that caused them (an outbox), then delivered asynchronously. That means at-least-once delivery: you can receive a delivery twice, but you can never miss one that committed.
Verify, then dedupe
import { createHmac, timingSafeEqual } from 'node:crypto'; function verify(rawBody: Buffer, signature: string, secret: string) { const expected = createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && timingSafeEqual(a, b);}// compare against x-webhook-signature BEFORE parsing;// dedupe on the delivery id — deliveries are at-least-once
Verify before you parse. Compute the HMAC over the raw request body with your per-endpoint secret and compare in constant time. Then dedupe on the event id — the platform applies the same discipline to payment-provider webhooks it receives, and it is the difference between at-least-once delivery and double-shipping an order.
Good citizenship
- Return 2xx fast; do real work on your own queue.
- Failures retry with exponential backoff — idempotent handlers make retries free.
- Use
content.updatedandproduct.updatedto invalidate storefront caches instead of polling.