Shopify's webhook docs are blunt about it: your app might receive the same webhook more than once. Most of the time the cause is on our side. Shopify allows one second to connect and five seconds for the whole request. If your workflow does its Google Sheets write, a Slack message and a CRM call before it answers, you blow past five seconds, Shopify marks the delivery as failed and sends it again. It retries up to 8 times over 4 hours. Each retry runs your whole workflow again.
Here's the setup I use now on every Shopify to n8n build. Four nodes, and it takes about fifteen minutes.
1. Answer Shopify straight away
In the Webhook node, set Respond to Immediately. n8n returns the response code the moment the request arrives and keeps working in the background. That alone removes most duplicates, because Shopify gets its 200 well inside the timeout.
The trade-off: you can't send back an error from a later node any more. That's fine here. Shopify doesn't care what you do with the order, only that you received it.
2. Verify the signature
Anyone who finds your webhook URL can POST a fake order to it. Shopify signs each delivery with an X-Shopify-Hmac-SHA256 header: a base64 HMAC-SHA256 of the raw request body, made with your app's client secret.
The catch is "raw". If you re-serialise the parsed JSON, the bytes change and the HMAC won't match. So:
- In the Webhook node, add the Raw Body option. The request now comes in as binary.
- Add a Crypto node, action Hmac, type SHA256, encoding BASE64, and turn on Binary File so it hashes the binary property from the webhook (check the property name in the Webhook output). The secret goes in the Crypto credential, not the node.
- Add an IF node that compares the Crypto output with
{{ $('Webhook').item.json.headers['x-shopify-hmac-sha256'] }}. False branch goes nowhere.
Note the lowercase header name. Headers arrive lowercased in $json.headers, and X-Shopify-Hmac-SHA256 with capitals will quietly return nothing.
After the check passes, parse the body back into JSON (an Extract From File node set to JSON, or a short Code node) so later nodes can read the order fields.
3. Drop deliveries you've already processed
This is the part most tutorials skip. Shopify gives every delivery an X-Shopify-Webhook-Id header, and a retry of the same delivery carries the same ID. n8n has a node built for this: Remove Duplicates.
- Operation: Remove Items Processed in Previous Executions
- Keep Items Where: Value Is New
- Value to Dedupe On:
{{ $('Webhook').item.json.headers['x-shopify-webhook-id'] }}
The node remembers values across executions, so the second delivery comes out with zero items and nothing downstream runs. History Size defaults to 10,000, which is plenty for a small store. Leave Scope on Node unless another node in the same workflow needs to share that history.
There's a second header, X-Shopify-Event-Id, and it's easy to mix the two up. The event ID is shared by every subscription that fired from the same merchant action, so if you have two subscriptions on the same topic, both deliveries carry it. Dedupe on the webhook ID if you want "process each delivery once". Use the event ID only if you deliberately want two subscriptions to collapse into one.
4. Make the write itself safe
Dedupe catches retries. It won't catch everything. If you clear the node's history, rebuild the workflow, or someone replays a test, you can still get a repeat. So the last step is making the write idempotent: in Google Sheets use Append or Update Row and match on the order ID instead of plain Append. A repeated order overwrites its own row instead of adding a new one.
Same idea in a CRM: search by order ID first, then create or update.
The finished workflow
Webhook (Respond: Immediately, Raw Body on) -> Crypto (Hmac SHA256, base64, binary) -> IF (HMAC matches header) -> Remove Duplicates (x-shopify-webhook-id, Value Is New) -> Extract From File (JSON) -> Google Sheets (Append or Update Row, match on order id) One more thing worth knowing
If your endpoint keeps failing, Shopify gives up on it. Subscriptions created through the Admin API are deleted after 8 consecutive failed deliveries. So if your n8n instance is down for a while, check the subscription still exists once it's back, or orders will stop arriving and nothing will tell you.
That's the whole thing. Answer fast, check the signature, drop repeats by webhook ID, and make the final write safe to run twice.
We build Shopify and n8n automations like this for small stores: https://rizehub.ph/services/n8n-automation