Catching Shopify orders your webhook workflow never received

This is a really sensible safety net, and I like that it only runs once a day instead of adding constant polling. I would lean towards doing both rather than choosing one — a quick check that the webhook subscription still exists catches the silent removal case, while the reconciliation catches everything else. One small tweak I have found helpful in similar setups is to give the daily check a bit of overlap, for example looking back slightly more than just yesterday, so late or timezone-shifted orders do not slip between runs. Logging the incoming order as the very first step, before any heavier processing, also helps because then the sheet is a reliable record even when a later step fails.

@nachiketmistry your question turned into a template, so here’s the follow-up. I built the subscription check as its own small workflow:

  • Every 3 hours it runs the webhookSubscriptions query and checks that each topic you expect (e.g. ORDERS_CREATE) still has a subscription pointing at your n8n host.
  • It also sends a plain GET to each n8n webhook URL. That never runs the workflow, but n8n’s 404 message is different for “registered for POST” (workflow listening) and “not registered” (workflow inactive or deleted). That catches the case where the subscription still exists but every delivery fails, before Shopify removes it.
  • Duplicate subscriptions (same topic and URL twice, so the workflow runs twice per order) get flagged too.
  • It stores the last result and only emails when something breaks or recovers, so it doesn’t send an “all fine” mail every 3 hours.

Tested by deleting a subscription and by pointing one at an inactive workflow. Free download if it’s useful: Shopify Webhook Health Check: free n8n template

Together with the daily reconciliation that covers both cases: the health check finds a dead webhook within hours, the reconciliation finds the orders that slipped through meanwhile.

@Akanazubaba thanks, those are exactly the kind of edge cases that only show up in production. I checked all three against the free version and shipped 1.1 this morning:

Timezone / window. This one was already covered: the check builds the window from $now.startOf('day') in the workflow’s timezone and sends it to Shopify as ISO with the offset (created_at:>='2026-10-25T00:00:00+05:30'), so Shopify filters on the shop’s local day, not UTC midnight. The guide says to set the workflow timezone to the store’s. Your overlap idea is still a good safety net for anyone who can’t trust that setting, and the order-ID match makes it harmless.

Throttling. Before, a THROTTLED answer made the run fail loudly through the error output (so no silent truncation), but it didn’t recover. Now the collect step reads extensions.cost.throttleStatus: on THROTTLED, or when the bucket is nearly empty, a Wait node pauses for (needed − available) / restoreRate seconds and the same cursor is asked again, up to 5 retries before it gives up and alerts.

Test orders. They used to be reported with a “test order” note. Now there’s skipTestOrders (on by default), so it matches what most order workflows do.

@Shafique_Mumtaz took your “backfilled, not done” suggestion: appended rows now get backfilled in a status column (statusColumn), next to the date they were added, so they stay distinguishable from rows the order workflow wrote.

@SilentJMA your open/closed flag in Shopify would be a tag or metafield set as the last step of the order workflow, as Shafique said. The trade-off is that the app then needs write_orders; comparing against a log keeps it read-only.

1 Like

Nice, thanks for actually shipping that bit — the backfilled label next to the date is exactly what I meant, otherwise those rows just look like normal orders later on. The throttle wait makes sense too, as long as it still alerts after those retries instead of quietly giving up.

Thanks, the throttle and test-order points both made it into 1.1.0: it reads throttleStatus.currentlyAvailable and waits/retries before the next page, and skipTestOrders mirrors the webhook side. On the window: it uses the workflow’s time zone for “yesterday”, but the overlap + ID dedupe idea is cleaner than trusting the boundary. Adding that next.

One important point from my side: idempotency should be enforced at the persistence layer, not only through an IF node in n8n.

In my FastAPI + PostgreSQL demo, I tested repeated requests with the same event ID: 100 concurrent requests resulted in 1 processed event and 99 duplicates. I also tested a changed payload with the same event ID, which returned HTTP 409 Conflict.

For a Shopify workflow, I’d use the Shopify order ID as a stable idempotency key and enforce uniqueness in the database before triggering downstream side effects. I’d also record duplicate attempts in metrics, so deduplication prevents repeated processing without hiding a possible webhook subscription issue.

Reconciliation should remain separate from replay: a missing audit row is evidence of a gap, not proof that the order was never processed.

@Akanazubaba the overlap is in now (1.2.0): a new overlapHours setting (default 2) pulls that many hours before and after the day, orders are deduped by ID, and the ones already reported are remembered in workflow static data, so an order in the overlap is reported once even when nothing gets appended to the sheet. Same change went into the morning check of the Pro alert, which now compares against the whole sheet rather than only yesterday’s rows.

@Shital_Katre agreed, the reconciliation leans on the order ID for the same reason: Shopify’s list is the record, the n8n side only decides what’s new.

Nice, 1.2.0 sounds like the right call — the 2-hour overlap plus dedupe by order ID beats trusting the day boundary, especially around timezone edges. Agree with Shital too: order ID as the stable key, and keeping duplicate attempts in metrics instead of dropping them silently, so a spike still tells you the webhook side might be sick. One small thing worth keeping: the morning check comparing against the whole sheet is the safer source of truth — yesterday-only comparisons are exactly how a missed order hides.