Webhooks are the backbone of event-driven integrations with Shopify. This guide covers the full lifecycle: subscribing to events, securely verifying incoming webhook requests, and responding in a way that prevents duplicate processing and scales with your app.
How Shopify webhooks work
Shopify sends HTTP POST requests to your endpoint when events occur (orders/create, products/update, app/uninstalled, etc.). Each request includes headers you should use for verification and troubleshooting, notably:
- x-shopify-topic — the event topic
- x-shopify-hmac-sha256 — the HMAC signature of the request body (base64)
- x-shopify-shop-domain — the shop that triggered the event
- x-shopify-api-version — API version
Shopify expects a 200 OK (or 2xx) response for successful deliveries. If Shopify receives a non-2xx response, it will retry delivery according to its retry schedule.
Subscribe: creating webhook subscriptions
You can register webhooks via the Admin REST API or GraphQL. You can also create them manually in the Shopify Admin (Apps > Manage private apps or via the App Bridge flow for public apps).
REST API example (server-side)
Use your app’s Admin access token to POST a webhook subscription. Replace SHOP, ACCESS_TOKEN, and your callback URL.
const fetch = require('node-fetch');
const SHOP = 'your-shop.myshopify.com';
const ACCESS_TOKEN = 'shpat_...';
const CALLBACK_URL = 'https://your-app.example.com/webhooks/shopify';
async function createWebhook() {
const url = `https://` + SHOP + `/admin/api/2025-01/webhooks.json`;
const body = {
webhook: {
topic: 'orders/create',
address: CALLBACK_URL,
format: 'json'
}
};
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': ACCESS_TOKEN
},
body: JSON.stringify(body)
});
if (!res.ok) throw new Error(`Failed to create webhook: ${res.status}`);
const data = await res.json();
return data;
}
createWebhook().then(console.log).catch(console.error);
GraphQL example (server-side)
GraphQL subscriptions use the webhookSubscriptionCreate mutation. This example omits full auth plumbing.
const query = `mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $callbackUrl: URL!) { webhookSubscriptionCreate(topic: $topic, webhookSubscription: { callbackUrl: $callbackUrl }) { userErrors { field message } webhookSubscription { id } } }`;
// POST to https://{shop}/admin/api/2025-01/graphql.json with X-Shopify-Access-Token
Verify: authenticate incoming webhooks
Shopify signs webhook payloads using your app’s shared secret. Verification ensures requests actually came from Shopify and prevents replayed/forged requests.
The verification process:
- Compute the HMAC-SHA256 of the raw request body using your app’s shared secret (not the access token).
- Base64-encode the digest.
- Compare the result to the value of the x-shopify-hmac-sha256 header.
- Use a constant-time comparison to avoid timing attacks.
Node (Express) verification example
This example uses the bodyParser.raw middleware to preserve the raw body needed to compute the HMAC.
const express = require('express');
const crypto = require('crypto');
const bodyParser = require('body-parser');
const app = express();
const SHOPIFY_SECRET = process.env.SHOPIFY_SECRET;
// Use raw bodies for HMAC verification
app.post('/webhooks/shopify', bodyParser.raw({ type: 'application/json' }), (req, res) => {
const hmacHeader = req.get('x-shopify-hmac-sha256');
const computed = crypto.createHmac('sha256', SHOPIFY_SECRET).update(req.body).digest('base64');
// constant-time comparison
const valid = crypto.timingSafeEqual(Buffer.from(computed, 'utf8'), Buffer.from(hmacHeader || '', 'utf8'));
if (!valid) {
console.warn('Invalid webhook HMAC');
return res.status(401).send('Unauthorized');
}
// parse JSON after verification
const payload = JSON.parse(req.body.toString('utf8'));
// Enqueue or process asynchronously
enqueueWebhookProcessing(payload);
// Acknowledge quickly
res.status(200).send('OK');
});
function enqueueWebhookProcessing(payload) {
// push to a job queue (Redis, RabbitMQ, etc.) for background processing
console.log('Enqueue:', payload);
}
app.listen(3000);
Notes:
- You must compute the HMAC over the raw bytes of the request body, not the parsed object.
- If you use frameworks or middleware that parse the body before you can access raw data, configure them to expose raw bodies or use a raw parser for webhook routes.
Python (Flask) verification example
import hmac
import hashlib
import base64
from flask import Flask, request, abort
app = Flask(__name__)
SHOPIFY_SECRET = b"your_shopify_shared_secret"
@app.route('/webhooks/shopify', methods=['POST'])
def webhook():
raw_body = request.get_data()
hmac_header = request.headers.get('X-Shopify-Hmac-Sha256')
digest = hmac.new(SHOPIFY_SECRET, raw_body, hashlib.sha256).digest()
computed = base64.b64encode(digest).decode()
# constant-time comparison
if not hmac.compare_digest(computed, hmac_header):
abort(401)
payload = request.get_json()
enqueue_webhook_processing(payload)
return '', 200
def enqueue_webhook_processing(payload):
print('Enqueue:', payload)
if __name__ == '__main__':
app.run(port=3000)
Respond: safe, reliable handling and best practices
- Acknowledge quickly: Return a 200 (or other 2xx) as soon as possible. Shopify expects a quick ack; long-running synchronous processing may cause timeouts and retries.
- Use a durable job queue: Enqueue work for background workers (Redis + Bull/Sidekiq, SQS, etc.).
- Idempotency: Webhooks can be delivered more than once. Make processing idempotent by checking resource versions, storing processed event ids, or using deduplication keys.
- Handle retries and errors: Shopify will retry failed deliveries. Don’t return 2xx for invalid requests; instead return 401 for signature failures, 410 for permanently rejected topics, and 500 for transient failures.
- Logging and observability: Log headers like x-shopify-topic, x-shopify-shop-domain, and delivery attempts for debugging. Track latencies and error rates.
- Validate payload shapes: Don’t trust that the payload exactly matches your expectations — validate required fields before processing.
- Security hygiene: Use HTTPS for webhook endpoints, rotate secrets periodically, and limit endpoint exposure (e.g., only allow known Shopify IPs if practical).
Example: minimal enqueue pattern (pseudo)
// inside verified webhook handler
res.status(200).send('OK');
// then asynchronously
jobQueue.add({ topic: req.get('x-shopify-topic'), payload: JSON.parse(req.body.toString('utf8')), shop: req.get('x-shopify-shop-domain') });
Troubleshooting
- 401 Unauthorized: Usually HMAC mismatch — ensure you use the raw body and the correct secret.
- Duplicate processing: Ensure idempotency strategy in workers.
- Missing fields: Shopify may change payload shapes between API versions — rely on documented fields or check the topic payload before using deeply nested properties.
- Webhook not delivered: Check webhook registration, your app’s access token/permissions, and the endpoint URL (must be reachable via HTTPS).
Summary
- Subscribe using the Admin REST or GraphQL APIs or via the Shopify Admin for manual setups.
- Verify every incoming request using HMAC-SHA256 computed over the raw body with your app secret.
- Acknowledge quickly and process asynchronously; design for retries and idempotency.
Following these rules will make your Shopify webhook integration secure, reliable, and production-ready.
