Which webhook topics matter for variants
Shopify emits ~40 webhook topics. For personalization apps tracking variant changes, only 6 matter in practice: products/create, products/update, products/delete, inventory_levels/update, inventory_items/update, and bulk_operations/finish. There is no separate variants/update topic — variant changes come through products/update because a variant edit changes the parent product's version.
Subscribing to webhooks
mutation WebhookSubscribe {
webhookSubscriptionCreate(
topic: PRODUCTS_UPDATE,
webhookSubscription: {
callbackUrl: "https://your-app.com/webhooks/products-update",
format: JSON
}
) {
webhookSubscription { id topic callbackUrl }
userErrors { field message }
}
}
Subscribe once per topic per shop during app installation (OAuth callback). Verify existing subscriptions via webhookSubscriptions query and avoid duplicate subscribes.
HMAC verification (mandatory)
Every webhook request includes an X-Shopify-Hmac-Sha256 header. Verify it before processing — Shopify's webhook URLs are guessable, and unverified endpoints are attack surface.
// Node.js Express example
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const hmac = req.headers['x-shopify-hmac-sha256'];
const body = req.rawBody; // must be the raw buffer, NOT parsed JSON
const expected = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(expected));
}
app.post('/webhooks/products-update', (req, res) => {
if (!verifyWebhook(req, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
res.status(200).send('OK');
});
body-parser middleware parses JSON by default, which mutates the body and breaks HMAC. Use express.raw({type:'application/json'}) for webhook routes or bypass parsing entirely.Retry semantics
Shopify retries failed webhooks for 48 hours with exponential backoff (up to 19 retries). Your endpoint must return a 2xx status within 5 seconds; anything else counts as failure and triggers retry.
- Never process synchronously. Queue the webhook payload (SQS, BullMQ, Kafka, Cloud Tasks) and return 200 immediately. Process the queue in a worker.
- Idempotency. Use
X-Shopify-Webhook-Idas the dedup key — retries carry the same ID. Store processed IDs in Redis with a 48h TTL. - Eventually consistent. Webhooks are not guaranteed in-order. A
products/updatefrom 10:03 can arrive before one from 10:02. Include the payload'supdated_atfield in your dedup logic.
Diffing variant changes
The products/update webhook fires on every product edit, including title, description, media, tags, options, and variant changes. You almost never care about all of them. Diff the incoming payload against your stored snapshot to detect what actually changed:
function diffVariants(oldProduct, newProduct) {
const oldById = Object.fromEntries(oldProduct.variants.map(v => [v.id, v]));
const newById = Object.fromEntries(newProduct.variants.map(v => [v.id, v]));
const changes = { created: [], updated: [], deleted: [] };
for (const id in newById) {
if (!oldById[id]) changes.created.push(newById[id]);
else if (JSON.stringify(oldById[id]) !== JSON.stringify(newById[id])) {
changes.updated.push({ old: oldById[id], new: newById[id] });
}
}
for (const id in oldById) {
if (!newById[id]) changes.deleted.push(oldById[id]);
}
return changes;
}
EventBridge / Kafka fan-out
At scale (100+ shops), route Shopify webhooks into an event bus (AWS EventBridge, Google Pub/Sub, Kafka) so multiple downstream services (POD partner sync, ERP mirror, analytics warehouse, cache invalidation) can consume without coupling to your webhook receiver:
Shopify → Your webhook endpoint → EventBridge topic "shopify.products.update"
↓ ↓
(verify HMAC, Multiple subscribers:
queue for retry) - POD partner sync
- ERP sync
- Cache invalidation
- Search index update
EventBridge webhooks (native Shopify integration)
Shopify supports EventBridge as a native webhook destination (no HTTP endpoint required). Configure via webhookSubscriptionCreate with eventBridgeWebhookSubscription. Shopify pushes directly into your EventBridge partner event source. Simplifies infrastructure and eliminates the HMAC-verification step (EventBridge handles authentication).
Frequently asked questions
Is there a Shopify webhook for variant changes?
Not directly — variant changes come through the products/update webhook, which fires whenever any part of a product (including its variants) is edited. Diff the incoming payload against your stored version to detect specific variant changes.
How do I verify Shopify webhooks are authentic?
Verify the X-Shopify-Hmac-Sha256 header. Compute HMAC-SHA256 of the raw request body using your app's webhook secret, base64-encode it, and compare to the header value using a timing-safe comparison. Reject any request that fails.
How does Shopify retry failed webhooks?
Retries for 48 hours with exponential backoff, up to 19 attempts. Your endpoint must return 2xx within 5 seconds to count as success. Queue payloads and process asynchronously — never handle webhooks synchronously.
Are Shopify webhooks guaranteed in order?
No — webhooks are eventually consistent and may arrive out of order. Include the payload's updated_at field in your dedup + apply logic to handle out-of-order delivery.
How do I dedupe Shopify webhook retries?
Use X-Shopify-Webhook-Id as the dedup key. Retries carry the same ID. Store processed IDs in Redis with a 48-hour TTL and skip already-processed IDs.
Can I send Shopify webhooks directly to AWS EventBridge?
Yes — Shopify has native EventBridge integration. Subscribe via webhookSubscriptionCreate with eventBridgeWebhookSubscription. Eliminates the need for a public HTTP endpoint and offloads HMAC verification.
What webhook topics does a personalization app need?
products/create, products/update, products/delete, inventory_levels/update, orders/create, orders/updated, bulk_operations/finish. Optionally refunds/create for reversal logic and customers/data_request for GDPR compliance.
How do I handle webhook floods when a merchant bulk-updates 1000 variants?
Rate-limit your worker (process max N/sec per shop), deduplicate incoming webhooks by product ID (only process the latest for a given product ID per minute), and use a priority queue that batches many product updates from the same shop into one downstream sync.
Related reading
- Admin API for options — GraphQL reference
- GraphQL for variants — variant mutations
- Headless commerce options — webhook + Storefront combo
- Custom app for options — app architecture
Try it free on Shopify
Print It My Way's permanent Free plan handles webhook fan-out to POD partners, ERP sync, and cache invalidation out of the box — no custom app development needed.
Install Print It My Way