Webhooks
Webhooks let your integration react to changes — an order shipping, a limited
edition selling out — without polling. They are part of the Store API and are
managed under /v1/webhooks.
Subscribe
Section titled “Subscribe”Register a subscription with the URL to deliver to and the events you care about.
This needs the webhooks:create scope.
curl -X POST https://api.staging.elasticstage.com/store/v1/webhooks \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/hooks/elasticstage", "events": ["order.shipped", "product.out_of_stock"] }'The response includes a signing secret — store it securely; you need it to verify deliveries. You can rotate the secret and send test events via the subscription endpoints (see the reference).
Events
Section titled “Events”Every delivery names a discrete business event. The event name carries the meaning, so payloads are lean.
| Event | Fired when |
|---|---|
order.created |
An order is placed. |
order.processing |
An order enters fulfilment. |
order.shipped |
An order ships (includes the tracking snapshot). |
order.tracking_updated |
Tracking details are added or corrected. |
order.delivered |
An order is delivered. |
order.cancelled |
An order is cancelled. |
order.payment_failed |
Payment for an order fails. |
product.available |
A product becomes purchasable. |
product.unavailable |
A product is no longer purchasable. |
product.inventory_changed |
A limited edition’s stock level changes. |
product.in_stock |
A limited edition goes from sold out to in stock. |
product.out_of_stock |
A limited edition sells out. |
Payload envelope
Section titled “Payload envelope”Every delivery is a JSON body with a stable envelope plus the per-event data:
{ "id": "evt_01J...", "api_version": "2026-06-01", "created_at": "2026-07-20T10:32:00.000Z", "event": "order.shipped", "data": { "order_id": "ord_01J...", "carrier": "Royal Mail", "tracking_number": "AB123456789GB", "tracking_url": "https://track.example.com/AB123456789GB", "updated_at": "2026-07-20T10:32:00.000Z" }}Verify the signature
Section titled “Verify the signature”Every request carries an X-ElasticStage-Signature header:
X-ElasticStage-Signature: t=1753005120,v1=6c8f...e2a1t— the delivery’s UNIX timestamp (seconds).v1— an HMAC-SHA256, hex-encoded, of the string`${t}.${rawBody}`keyed with your subscription’s signing secret.
To verify: recompute the HMAC over `${t}.${rawBody}` using the raw request
body (not a re-serialized object), compare it to v1 in constant time, and
reject deliveries whose timestamp is outside a tolerance window (300 seconds is
what elasticStage uses).
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the exact request body string. secret: your subscription secret.export function verify(secret, rawBody, header, toleranceSeconds = 300) { const t = /t=(\d+)/.exec(header)?.[1]; const v1 = /v1=([0-9a-f]+)/.exec(header)?.[1]; if (!t || !v1) return false;
const now = Math.floor(Date.now() / 1000); if (Math.abs(now - Number(t)) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex");
const a = Buffer.from(v1, "hex"); const b = Buffer.from(expected, "hex"); return a.length === b.length && timingSafeEqual(a, b);}Acknowledge
Section titled “Acknowledge”Return any 2xx status to mark a delivery successful. Non-2xx responses (or
timeouts) are retried, so make your handler idempotent — key on the envelope
id, which is stable across retries of the same event.