Webhooks
Webhooks
Subscribe to events, verify signatures, handle retries and process deliveries idempotently.
Use Webhooks when your backend needs to react to commerce changes without polling. A subscription belongs to one authorized app installation, one reviewed event and one HTTPS endpoint.
Before you start
Your published app version must declare the event, the merchant must approve it, and the installation token needs webhooks:manage. Your receiver must be public HTTPS, verify signatures over the raw body, return promptly and process events idempotently.
Subscription operations
Use GET /api/v1/webhooks, POST /api/v1/webhooks, and DELETE /api/v1/webhooks/{id} with webhooks:manage. Creation accepts one event type declared by the exact installed version and an HTTPS endpoint. Subscriptions are bound to the exact installed reviewed version and authorization generation. Recreating the same installation/event/endpoint rotates the secret and reactivates the subscription.
One-time signing secret
The create response returns data.signing_secret once, together with its last four characters. Store the full secret securely at that moment. Later list responses return only secret_last_four. Rotating a subscription replaces the previous secret, so update the receiver atomically.
Delivery format and signing
The JSON body contains id, type, occurred_at, and data. Delivery sends X-Saas-Event-Id, X-Saas-Timestamp, and X-Saas-Signature. The signature is HMAC-SHA256 formatted as v1=<hex> over:
<timestamp>.<event_id>.<exact_request_body>
Verify the exact raw body before parsing, compare signatures in constant time, enforce a timestamp tolerance, and deduplicate the stable event ID.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(rawBody, eventId, timestamp, signature, secret) {
const expected = `v1=${createHmac("sha256", secret).update(`${timestamp}.${eventId}.${rawBody}`).digest("hex")}`;
return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Python
import hashlib, hmac
def verify_webhook(raw_body, event_id, timestamp, signature, secret):
message = timestamp.encode() + b"." + event_id.encode() + b"." + raw_body
expected = "v1=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)PHP
function verifyWebhook(string $rawBody, string $eventId, string $timestamp, string $signature, string $secret): bool {
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $eventId . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}Network and retry behavior
Endpoints must use HTTPS and a public address. Delivery rejects private, loopback, link-local, reserved, and unsafe targets, resolves DNS, and pins the verified public address for the request. It does not follow redirects. The request timeout is 15 seconds. Delivery uses at most ten attempts. After a failed attempt the next one waits about 30 seconds, then 2 minutes, 10 minutes, 30 minutes, 1, 2, 4, 6 and 8 hours (each ±10% jitter), so the last attempt comes about 22 hours after the first. A Retry-After header on a 429 or 503 response lengthens the next wait, up to 30 minutes. Responses 400, 405, 410, 413, 414, 415 and 422 are permanent and end the delivery at once. Other 4xx, 5xx, timeouts and network errors are retried. A delivery that ends dead can be replayed from the developer console for 7 days. Failures do not retry forever.
Resource event contract
The merchant approval screen lists the exact events declared by the reviewed version. A subscription can receive only an event in that allowlist. Business changes and their delivery records commit or roll back together. The same resource/event is coalesced within a transaction; no-op changes do not create events. New subscriptions do not receive historical events.
| Event | Trigger | Resource |
|---|---|---|
products.created | Product added | product |
products.updated | Product business fields change | product |
products.deleted | Product deleted | product |
customers.created | Customer added | customer |
customers.updated | Customer name, phone or email changes (not emitted in the transaction that created the customer) | customer |
orders.created | Order created | order |
orders.updated | Order business fields change | order |
orders.cancelled | Order enters cancelled status | order |
orders.fulfilled | Order enters shipped or delivered, including through POST /api/v1/orders/{id}/fulfillments; shipped → delivered does not emit a second fulfilment | order |
inventory.updated | Nonzero inventory is added, quantity/reservation changes, or an inventory record is removed | inventory |
app.uninstalled | The merchant disconnects this exact app | installation |
The data object contains schema_version: 1, store_id and resource. A resource contains type and id; inventory also includes product_id, nullable variant_id and location_id. The uninstall receipt adds app_id. Notifications never contain names, contact details, addresses, payment credentials, quantities or full database rows. Reading details requires a separately approved operational read permission; an event does not invent a public API for a planned scope. A referenced resource may already be deleted when the receiver reads it.
Final uninstall receipt
A reviewed app.uninstalled subscription may receive one final receipt for its own disconnected installation. All ordinary tokens, subscriptions and queued deliveries are revoked first. The receipt uses the exact previous subscription, expires after five minutes and has at most three attempts with a 30-second retry delay. It grants no new read access. Reinstallation, endpoint/secret changes, suspension, scope removal or release disablement cancels pending receipts permanently. Restoring access does not revive them. Delivery is best effort: expiry or network failure can prevent the receipt from arriving.
Revocation edge case
Developer, app, version or installation revocation disables stale authority. Queued deliveries preserve their subscription epoch and installation generation. A one-use admission checks current authority after safe DNS resolution, and completion requires the exact unexpired claim. A request already admitted to the network cannot be recalled. Receivers must remain idempotent and should stop acting on events after merchant authorization is revoked.