Skip to content

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.

Register a subscription with the URL to deliver to and the events you care about. This needs the webhooks:create scope.

Terminal window
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).

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.

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"
}
}

Every request carries an X-ElasticStage-Signature header:

X-ElasticStage-Signature: t=1753005120,v1=6c8f...e2a1
  • t — 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);
}

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.