Listen for order events
Subscribe to order and fulfillment webhooks to drive your app as an order moves through its lifecycle. Signature verification, retries, and headers are covered once in Webhook Overview — this recipe focuses on order events.
When to use this
Syncing order status, triggering fulfillment, or reconciling refunds.
Prerequisites
- A working public app with OAuth — follow Develop a standalone app. Every request below carries an
Access-Token: {token}header, using the token you saved for each shop. - Read Webhook Overview first.
Order lifecycle events
| Topic | Fires when | Reference |
|---|---|---|
orders/create | An order is placed | orders/create |
orders/paid | An order is paid | orders/paid |
orders/fulfilled | An order is fully fulfilled | orders/fulfilled |
orders/partially_fulfilled | Part of an order is fulfilled | orders/partially_fulfilled |
orders/cancelled | An order is cancelled | orders/cancelled |
orders/refunded | An order is refunded | orders/refunded |
fulfillments/create | A fulfillment is created | fulfillments/create |
fulfillments/update | A fulfillment is updated | fulfillments/update |
Subscribe
Register one subscription per topic — call this once for each of the order and fulfillment topics listed above. From 2026-01, the request body wraps the subscription in a webhook object.
POST /openapi/2026-01/webhooks
Request body:
{
"webhook": {
"topic": "orders/paid", // one call per topic
"address": "https://your-app.example.com/webhook/orders-paid"
}
}
Returns the created subscription object. See Webhook Overview for version differences and for cleaning up stale subscriptions.
Receive, verify, and route
Verify over the raw body, then route by topic to the actions in the other recipes.
const express = require('express');
const crypto = require('crypto');
const app = express();
function verifyWebhook(rawBody, hmacHeader, clientSecret) {
const digest = crypto.createHmac('sha256', clientSecret).update(rawBody).digest('base64');
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(hmacHeader));
}
// Apply express.raw() on the webhook route only — verification needs the raw bytes.
app.post('/webhook/:event', express.raw({ type: '*/*' }), async (req, res) => {
if (!verifyWebhook(req.body, req.get('X-Shoplazza-Hmac-Sha256'), CLIENT_SECRET)) {
return res.sendStatus(401);
}
const shop = req.get('X-Shoplazza-Shop-Domain');
const topic = req.get('X-Shoplazza-Topic');
const payload = JSON.parse(req.body.toString('utf8'));
// orders/* carry order; fulfillments/* carry fulfillment (with order_id)
const orderId = payload.order?.id || payload.fulfillment?.order_id;
// ... look up the shop token, fetch the order (see Manage orders), react by topic ...
res.sendStatus(200);
});
Verify against the raw, unparsed body. Apply express.raw() on the webhook route only — a global JSON parser changes the bytes and verification always fails.