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.

Event types38

Every state change the platform announces, grouped into channels below.

Channels14

A store webhook subscribes one URL to one channel and receives every event on it.

Delivery attempts5

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.0 for store webhooks, Storefront-Extension/1.0 for 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

PURCHASE
order.created

A 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.confirmed

An 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.updated

An 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.cancelled

An 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.completed

A hosted checkout session finished successfully.

Payload example
{
  "orderId": 5501,
  "paymentStatus": "paid",
  "amountTotalMinor": "12900",
  "currency": "USD",
  "providerPaymentIntentId": "pi_3PqXo42e"
}

Refund

REFUND
order.refunded

An order payment was refunded, in part or in full.

Payload example
{
  "orderId": 5501,
  "refundId": 301,
  "refundedAmountMinor": "12900",
  "refundedCurrency": "USD",
  "isFullyRefunded": true
}

Subscription canceled

SUBSCRIPTION_CANCEL
subscription.cancelled

A 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_SOON
subscription.cancel_soon

A 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_CREATED
customer.created

A 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

PRODUCT
product.created

A 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.updated

A 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.deleted

A product was removed; the payload carries the last-known snapshot.

Payload example
{
  "productId": 88,
  "product": {
    "id": 88,
    "storeId": 12,
    "slug": "poster",
    "name": "Poster"
  }
}
inventory.restocked

Tracked stock went from zero back to positive.

Payload example
{
  "storeId": 12,
  "productId": 88,
  "productOptionId": null,
  "stock": 25
}
inventory.out_of_stock

Tracked stock hit zero for a product or an option.

Payload example
{
  "storeId": 12,
  "productId": 88,
  "productOptionId": null
}
inventory.low_stock

Stock 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

STORE
store.updated

Store 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

SHIPPING
order.shipped

The 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.delivered

The 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_changed

Any 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_updated

A 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

SUBSCRIPTION
subscription.created

A subscription checkout completed and the subscription became active.

Payload example
{
  "subscriptionId": 9001,
  "customerId": 31,
  "productId": 88,
  "status": "active",
  "providerSubscriptionId": "sub_1PqXo4"
}
subscription.renewed

A 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_failed

A 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.resumed

A 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

PAYMENT
payment.succeeded

A payment settled, including delayed-notification methods confirmed asynchronously.

Payload example
{
  "paymentId": 771,
  "orderId": 5501,
  "storeId": 12,
  "amountMinor": "12900",
  "currency": "USD",
  "providerPaymentIntentId": "pi_3PqXo42e"
}
payment.failed

A 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

DISPUTE
dispute.created

A 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.updated

The provider updated an open dispute.

Payload example
{
  "paymentId": 771,
  "orderId": 5501,
  "storeId": 12,
  "disputeStatus": "under_review",
  "providerPaymentIntentId": "pi_3PqXo42e",
  "source": "payment"
}
dispute.closed

The 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

CUSTOMER
customer.updated

An 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.deleted

A 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_MARKETING
subscriber.added

An address joined the mailing list — public signup, resubscribe, or admin add.

Payload example
{
  "subscriptionId": 1201,
  "storeId": 12,
  "email": "jane@example.com"
}
subscriber.confirmed

A subscriber completed double-opt-in or was confirmed by an admin.

Payload example
{
  "subscriptionId": 1201,
  "storeId": 12,
  "email": "jane@example.com"
}
subscriber.unsubscribed

A subscriber left the list, from the public link or an admin.

Payload example
{
  "subscriptionId": 1201,
  "storeId": 12,
  "email": "jane@example.com"
}
campaign.sent

A 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.completed

A 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_ABANDONMENT
cart.abandoned

A shopper's cart was flagged abandoned.

Payload example
{
  "cartId": 3301,
  "storeId": 12,
  "customerId": 31
}
checkout.expired

A hosted checkout session expired unpaid and the order was rolled back.

Payload example
{
  "orderId": 5501,
  "storeId": 12,
  "providerPaymentIntentId": "pi_3PqXo42e"
}