Webhooks
Webhooks & HMAC
Subscribe from Apps → Develop apps → your app or with webhookSubscriptionCreate. Each topic requires its read scope.
| Topic | Scope | When |
|---|---|---|
orders/create | read_orders | An order is placed (online store or admin). |
orders/paid | read_orders | An order becomes fully paid. |
orders/edited | read_orders | Items on an order are added, removed or changed in quantity. |
orders/fulfilled | read_orders | All items of an order are fulfilled. |
orders/cancelled | read_orders | An order is cancelled. |
refunds/create | read_orders | A refund is issued. |
products/create | read_products | A product is created. |
products/update | read_products | A product is updated. |
products/delete | read_products | A product is deleted (payload: { id }). |
Delivery
POSTwith a JSON body — the same shape as the GraphQLOrder/Productobjects.- Headers:
X-Sellify-Topic,X-Sellify-Hmac-Sha256,X-Sellify-Shop-Domain,X-Sellify-Webhook-Id(use it to deduplicate),X-Sellify-API-Version. - Respond with any
2xxwithin 10 seconds. Do slow work asynchronously. Every attempt is shown in the app's delivery log. - Production callback URLs must be public
https://URLs.
Verify the signature
Compute an HMAC-SHA256 of the raw request body with your app's webhook secret, base64-encode it, and compare in constant time:
JavaScript
import crypto from "node:crypto";
export async function POST(req) {
const raw = await req.text();
const expected = crypto.createHmac("sha256", process.env.SELLIFY_WEBHOOK_SECRET).update(raw, "utf8").digest("base64");
const given = req.headers.get("x-sellify-hmac-sha256") ?? "";
const ok = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return new Response("invalid signature", { status: 401 });
const order = JSON.parse(raw);
// ...enqueue work, then acknowledge quickly
return new Response("ok");
}