Every state change the platform announces, grouped into channels below.
Developers / Webhooks
Signed HTTP callbacks for everything that happens in your store.
Orders, fulfillment, payments, disputes, subscriptions, customers, inventory, and email marketing all announce themselves. Subscribe an endpoint once and react in your own systems in near real time.
A store webhook subscribes one URL to one channel and receives every event on it.
Failed deliveries retry with exponential backoff before giving up.
Subscribing
Store webhooks
Create endpoints in the admin dashboard under Integrations / Webhooks. Each endpoint row subscribes one URL to one channel; add multiple rows to cover several channels.
Every endpoint gets its own signing secret, shown once at creation and rotatable at any time from the same screen. Verify it on every delivery.
Extensions
Installed extensions subscribe per event name instead: list the events in the manifest's events array and the platform delivers to the extension's webhook URL, signed with the per-install secret. Individual subscriptions can be toggled through the extensions API.
The delivery envelope
Every delivery is an HTTP POST with a JSON body. Store-webhook deliveries include the integrationId of the endpoint row; extension deliveries omit it.
{
"event": "order.shipped",
"eventId": "0d9f4c1e-6c3a-4f6e-9b1a-2f8f4f1c9d2b",
"storeId": 12,
"integrationId": 7,
"schemaVersion": 1,
"payload": {
"shippingId": 9001,
"orderId": 5501,
"storeId": 12,
"status": "shipped"
},
"timestamp": "2026-08-12T09:00:01.244Z"
}Headers
X-Storefront-Timestamp— ISO-8601 time the delivery attempt was signed. Bound into the signature, so it cannot be swapped without breaking verification.X-Storefront-Signature— hex HMAC-SHA256 over the timestamp, a dot, and the raw request body, keyed with your signing secret.X-Storefront-Event-Id— stable id of the logical event, identical across every delivery and retry of that event. Also echoed in the body. Dedup on it.X-Storefront-Delivery-Id— identifies one event-to-endpoint delivery across all of its retry attempts.User-Agent—Storefront-Webhook/1.0for store webhooks,Storefront-Extension/1.0for extension deliveries.
Delivery is at-least-once: retries re-sign with a fresh timestamp, so a valid signature never implies a new event. Store the eventId values you have already processed and skip repeats.
Verifying signatures
Compute the HMAC over the exact raw body bytes — parse the JSON only after the comparison passes. Reject deliveries older than a few minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyStorefrontWebhook(rawBody, headers, signingSecret) {
const timestamp = headers['x-storefront-timestamp'];
const signature = headers['x-storefront-signature'];
if (!timestamp || !signature) return false;
// Reject stale deliveries (replay protection). 5 minutes matches the
// official verifier's default tolerance.
if (Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60 * 1000) return false;
const expected = createHmac('sha256', signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
if (expected.length !== signature.length) return false;
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
}Extension authors can use the verifyWebhookSignature helper from @storefront/extension-sdk instead — the same scheme with the freshness window built in.
Delivery and retries
- Five attempts per delivery with exponential backoff, starting at two seconds.
- A 4xx response is terminal — the delivery is not retried. 5xx responses, network errors, and timeouts are retried.
- Attempts time out after 5 seconds for store webhooks and 10 seconds for extension deliveries.
- Acknowledge with any 2xx as fast as possible and do the real work asynchronously.
- Ordering is not guaranteed across events — rely on the timestamps and ids in the payload rather than arrival order.
Event catalog
Grouped by store-webhook channel. Extensions subscribe to the individual event names.
Orders
PURCHASEorder.createdA new order was created, including orders still awaiting payment.
Payload example
{
"orderId": 5501,
"order": {
"id": 5501,
"storeId": 12,
"status": "confirmed",
"priceAmountMinor": "12900",
"currency": "USD",
"customerId": 31,
"customerEmail": "jane@example.com",
"createdAt": "2026-08-11T14:03:22.101Z",
"confirmedAt": "2026-08-11T14:03:22.101Z",
"cancelledAt": null,
"trackingId": null,
"items": [
{
"productId": 88,
"name": "Poster",
"quantity": 1,
"priceMinor": "12900",
"priceCurrency": "USD"
}
]
}
}order.confirmedAn order's payment settled and the order was confirmed.
Payload example
{
"orderId": 5501,
"confirmedByUserId": 7,
"order": {
"id": 5501,
"storeId": 12,
"status": "confirmed",
"priceAmountMinor": "12900",
"currency": "USD"
}
}order.updatedAn order changed — status, tracking, or other fields; the changes object names what moved.
Payload example
{
"orderId": 5501,
"changes": {
"status": "shipped",
"trackingNumber": "1Z999AA10123456784"
},
"order": {
"id": 5501,
"storeId": 12,
"status": "shipped",
"trackingId": "1Z999AA10123456784"
}
}order.cancelledAn order was cancelled and its stock released.
Payload example
{
"orderId": 5501,
"cancelledByUserId": 7,
"order": {
"id": 5501,
"storeId": 12,
"status": "cancelled",
"cancelledAt": "2026-08-11T15:20:00.000Z"
}
}checkout.completedA hosted checkout session finished successfully.
Payload example
{
"orderId": 5501,
"paymentStatus": "paid",
"amountTotalMinor": "12900",
"currency": "USD",
"providerPaymentIntentId": "pi_3PqXo42e"
}Refund
REFUNDorder.refundedAn order payment was refunded, in part or in full.
Payload example
{
"orderId": 5501,
"refundId": 301,
"refundedAmountMinor": "12900",
"refundedCurrency": "USD",
"isFullyRefunded": true
}Subscription canceled
SUBSCRIPTION_CANCELsubscription.cancelledA subscription reached its end and is now canceled.
Payload example
{
"subscriptionId": 9001,
"customerId": 31,
"productId": 88,
"canceledAt": "2026-08-11T00:00:00.000Z"
}Subscription cancel soon
SUBSCRIPTION_CANCEL_SOONsubscription.cancel_soonA customer scheduled cancellation at the end of the current period.
Payload example
{
"subscriptionId": 9001,
"customerId": 31,
"productId": 88,
"currentPeriodEnd": "2026-09-01T00:00:00.000Z"
}Customer created
CUSTOMER_CREATEDcustomer.createdA new customer account was created — registration, OAuth signup, or guest promotion.
Payload example
{
"customerId": 31,
"storeId": 12,
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"createdAt": "2026-08-11T14:00:00.000Z"
}Products
PRODUCTproduct.createdA product was added to the catalog.
Payload example
{
"productId": 88,
"product": {
"id": 88,
"storeId": 12,
"slug": "poster",
"name": "Poster",
"priceMinor": "12900",
"priceCurrency": "USD",
"stock": 20,
"isAvailable": true
}
}product.updatedA product changed. Bulk edits fire one event per touched product, marked with bulk true and the list of changed fields.
Payload example
{
"productId": 88,
"bulk": true,
"changes": [
"priceMinor"
],
"product": {
"id": 88,
"storeId": 12,
"slug": "poster",
"name": "Poster",
"priceMinor": "13900",
"priceCurrency": "USD",
"stock": 20,
"isAvailable": true
}
}product.deletedA product was removed; the payload carries the last-known snapshot.
Payload example
{
"productId": 88,
"product": {
"id": 88,
"storeId": 12,
"slug": "poster",
"name": "Poster"
}
}inventory.restockedTracked stock went from zero back to positive.
Payload example
{
"storeId": 12,
"productId": 88,
"productOptionId": null,
"stock": 25
}inventory.out_of_stockTracked stock hit zero for a product or an option.
Payload example
{
"storeId": 12,
"productId": 88,
"productOptionId": null
}inventory.low_stockStock fell below the product's configured low-stock threshold. Opt in per product; no threshold means no event.
Payload example
{
"storeId": 12,
"productId": 88,
"productOptionId": null,
"stock": 4,
"threshold": 5
}Store
STOREstore.updatedStore identity settings changed, such as the custom domain.
Payload example
{
"storeId": 12,
"changes": {
"domain": "shop.example.com"
},
"store": {
"id": 12,
"name": "Example Shop",
"slug": "example",
"domain": "shop.example.com"
}
}Shipping
SHIPPINGorder.shippedThe order's shipment entered a shipped state for the first time.
Payload example
{
"shippingId": 9001,
"orderId": 5501,
"storeId": 12,
"status": "shipped",
"trackingNumber": "1Z999AA10123456784",
"trackingUrl": "https://tracking.example.com/1Z999AA10123456784",
"shippedAt": "2026-08-12T09:00:00.000Z"
}order.deliveredThe shipment was delivered.
Payload example
{
"shippingId": 9001,
"orderId": 5501,
"storeId": 12,
"status": "delivered",
"trackingNumber": "1Z999AA10123456784",
"trackingUrl": null,
"shippedAt": "2026-08-12T09:00:00.000Z"
}shipping.status_changedAny shipping status transition, with before and after values.
Payload example
{
"shippingId": 9001,
"orderId": 5501,
"storeId": 12,
"statusBefore": "shipped",
"statusAfter": "in_transit",
"status": "in_transit",
"trackingNumber": "1Z999AA10123456784",
"trackingUrl": null,
"shippedAt": "2026-08-12T09:00:00.000Z"
}shipping.tracking_updatedA tracking number or tracking URL was set or changed.
Payload example
{
"shippingId": 9001,
"orderId": 5501,
"storeId": 12,
"status": "shipped",
"trackingNumber": "1Z999AA10123456784",
"trackingUrl": "https://tracking.example.com/1Z999AA10123456784",
"shippedAt": null
}Subscriptions
SUBSCRIPTIONsubscription.createdA subscription checkout completed and the subscription became active.
Payload example
{
"subscriptionId": 9001,
"customerId": 31,
"productId": 88,
"status": "active",
"providerSubscriptionId": "sub_1PqXo4"
}subscription.renewedA recurring invoice was paid and a renewal order created. Fires from the second paid invoice onward.
Payload example
{
"subscriptionId": 9001,
"customerId": 31,
"productId": 88,
"orderId": 5610,
"invoiceId": 441,
"amountMinor": "1999",
"currency": "USD",
"periodEnd": "2026-09-11T00:00:00.000Z"
}subscription.payment_failedA renewal charge failed. Fires on the first observation of each failed invoice.
Payload example
{
"subscriptionId": 9001,
"customerId": 31,
"productId": 88,
"invoiceId": 442,
"failureReason": "card_declined",
"attemptCount": 2
}subscription.resumedA pending cancellation was undone — the counter-event to subscription.cancel_soon.
Payload example
{
"subscriptionId": 9001,
"customerId": 31,
"productId": 88,
"status": "active",
"currentPeriodEnd": "2026-09-01T00:00:00.000Z"
}Payments
PAYMENTpayment.succeededA payment settled, including delayed-notification methods confirmed asynchronously.
Payload example
{
"paymentId": 771,
"orderId": 5501,
"storeId": 12,
"amountMinor": "12900",
"currency": "USD",
"providerPaymentIntentId": "pi_3PqXo42e"
}payment.failedA payment terminally failed. Retryable attempts are not announced.
Payload example
{
"paymentId": 771,
"orderId": 5501,
"storeId": 12,
"amountMinor": "12900",
"currency": "USD",
"providerPaymentIntentId": "pi_3PqXo42e",
"failureMessage": "The shopper abandoned the 3DS challenge."
}Disputes
DISPUTEdispute.createdA chargeback was opened against a payment or a subscription invoice.
Payload example
{
"paymentId": 771,
"orderId": 5501,
"storeId": 12,
"disputeStatus": "open",
"providerPaymentIntentId": "pi_3PqXo42e",
"source": "payment"
}dispute.updatedThe provider updated an open dispute.
Payload example
{
"paymentId": 771,
"orderId": 5501,
"storeId": 12,
"disputeStatus": "under_review",
"providerPaymentIntentId": "pi_3PqXo42e",
"source": "payment"
}dispute.closedThe dispute closed; disputeStatus tells you won or lost.
Payload example
{
"paymentId": 771,
"orderId": 5501,
"storeId": 12,
"disputeStatus": "won",
"providerPaymentIntentId": "pi_3PqXo42e",
"source": "payment"
}Customer changes
CUSTOMERcustomer.updatedAn admin edited a customer profile; changes lists the fields.
Payload example
{
"customerId": 31,
"storeId": 12,
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"changes": [
"firstName"
]
}customer.deletedA customer account was deleted. The payload is the last snapshot of the row.
Payload example
{
"customerId": 31,
"storeId": 12,
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe"
}Email marketing
EMAIL_MARKETINGsubscriber.addedAn address joined the mailing list — public signup, resubscribe, or admin add.
Payload example
{
"subscriptionId": 1201,
"storeId": 12,
"email": "jane@example.com"
}subscriber.confirmedA subscriber completed double-opt-in or was confirmed by an admin.
Payload example
{
"subscriptionId": 1201,
"storeId": 12,
"email": "jane@example.com"
}subscriber.unsubscribedA subscriber left the list, from the public link or an admin.
Payload example
{
"subscriptionId": 1201,
"storeId": 12,
"email": "jane@example.com"
}campaign.sentA campaign's dispatch started; the audience is resolved and counted.
Payload example
{
"campaignId": 55,
"storeId": 12,
"name": "Fall drop",
"subject": "New arrivals",
"recipientCount": 1240
}campaign.completedA campaign finished sending, with final sent and failed counters.
Payload example
{
"campaignId": 55,
"storeId": 12,
"name": "Fall drop",
"status": "sent",
"recipientCount": 1240,
"sentCount": 1236,
"failedCount": 4
}Checkout abandonment
CHECKOUT_ABANDONMENTcart.abandonedA shopper's cart was flagged abandoned.
Payload example
{
"cartId": 3301,
"storeId": 12,
"customerId": 31
}checkout.expiredA hosted checkout session expired unpaid and the order was rolled back.
Payload example
{
"orderId": 5501,
"storeId": 12,
"providerPaymentIntentId": "pi_3PqXo42e"
}