Shopify Webhook Not Firing: Diagnose and Fix

Orders are being created in Shopify but your system never hears about them.
Shopify webhooks fail quietly by design. There is no error in the admin, and after enough failures Shopify removes the subscription entirely — so it stops working permanently without anyone being told.
Symptom
- Some orders sync and others do not, with no obvious pattern.
- The endpoint works when you test it manually with curl.
- Nothing appears in your logs for the missing orders.
- The webhook eventually vanishes from Settings → Notifications → Webhooks.
Cause
1. You responded too slowly
Shopify requires a response within 5 seconds. Processing the order before responding means a slow database write or a third-party API call blows the budget. Shopify sees a timeout and treats it as failure.
2. You returned a non-2xx status
Anything outside 200–299 counts as failure, including a 301 redirect. A webhook pointed at http:// that redirects to https:// fails every time.
3. Retries were exhausted
Shopify retries over roughly 48 hours with backoff. After ~19 failed attempts, the subscription is deleted. Nothing tells you this happened.
4. HMAC verification rejected a valid request
Verifying against the parsed JSON body rather than the raw bytes fails, because re-serialising changes the payload.
Fix
1. Respond immediately, process afterwards
This is the single most important change:
import crypto from 'crypto';
export async function POST(request) {
const raw = await request.text(); // raw body — needed for HMAC
const hmac = request.headers.get('x-shopify-hmac-sha256');
if (!verify(raw, hmac)) {
return new Response('Unauthorized', { status: 401 });
}
// Respond first. Anything slow happens after Shopify has its 200.
queueForProcessing(raw).catch((err) => console.error('queue failed', err));
return new Response('OK', { status: 200 });
}
function verify(raw, hmac) {
if (!hmac) return false;
const digest = crypto
.createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET)
.update(raw, 'utf8')
.digest('base64');
// Constant-time compare — a plain === leaks timing information.
const a = Buffer.from(digest);
const b = Buffer.from(hmac);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Note request.text(), not request.json(). HMAC is computed over the exact bytes Shopify sent.
2. Make processing idempotent
Retries mean you will receive the same webhook more than once. Deduplicate on the delivery ID:
async function queueForProcessing(raw, headers) {
const eventId = headers.get('x-shopify-webhook-id');
// Same event twice is normal during retries — not an error.
const seen = await db.from('webhook_events').select('id').eq('id', eventId).maybeSingle();
if (seen.data) return;
await db.from('webhook_events').insert({ id: eventId, payload: raw });
await handleOrder(JSON.parse(raw));
}
3. Check whether the subscription still exists
query {
webhookSubscriptions(first: 50) {
edges { node {
id
topic
endpoint { ... on WebhookHttpEndpoint { callbackUrl } }
} }
}
}
If your topic is missing, retries were exhausted and it was deleted. Re-create it — and fix the underlying failure first, or it will be deleted again.
4. Never point a webhook at a redirecting URL
# Must be 200 directly, not 301 → 200.
curl -sI -X POST https://yourapp.com/webhooks/orders | head -3
Use the final HTTPS URL, with no trailing-slash redirect.
Verify
- Place a test order and confirm your log records it within 5 seconds.
- Replay the same payload twice and confirm the duplicate is ignored.
- Send a request with a bad HMAC and confirm a 401.
- Confirm the subscription still lists after 48 hours of real traffic.
Shopify also exposes delivery attempts in the Partner Dashboard for apps — check there before assuming the request never arrived.
Prevent
- Always respond before processing. Treat the handler as "validate, enqueue, return 200".
- Alert on webhook silence. If no order webhook arrives in an hour during business hours, something is wrong.
- Reconcile daily. Poll the Orders API for anything your system missed; webhooks are best-effort, not guaranteed.
- Log the
x-shopify-webhook-idso duplicates and gaps are both visible.
Frequently asked questions
How many times does Shopify retry?
Roughly 19 attempts over 48 hours with increasing backoff. After that the subscription is removed.
Can I get the missed orders back?
Not through webhooks — they are not replayable. Query the Orders API with updated_at_min covering the outage.
Is polling a safer alternative?
Polling is a good backstop, not a replacement. Webhooks are faster and cheaper; a daily reconciliation job catches whatever they miss.
Related reading
- Shopify GraphQL throttled errors
- Fixing 404s after a migration
- Shopify development — our service page
Need a hand?
We build reliable Shopify integrations for merchants in the USA, UAE, Saudi Arabia, Qatar and the UK. If orders are going missing between systems, get a free audit or read about our Shopify development work.
The Yaxify Team
Shopify & Web Development Agency
We build custom Shopify stores and high-performance websites for growing businesses across the USA, UK, and UAE. Everything we publish comes out of client work we have actually shipped.
More about Yaxify →