David Lewin David Lewin
SEO, Traffic & Automation

Abandoned-cart recovery in n8n: Shopify webhook, wait, check order, email or SMS

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

Trigger an n8n workflow on Shopify's checkouts/create webhook, pause it for two hours with a Wait node, then ask Shopify's Admin GraphQL API whether an order exists with that checkout's token. If there's no order, an IF node sends a recovery email with the checkout's abandoned_checkout_url, or an SMS through Twilio when the buyer gave a phone number and opted in to SMS marketing. The whole workflow is eight nodes and one n8n execution per checkout. The import-ready JSON is below.

Most shoppers who start a checkout don't finish it. Shopify can send its own abandoned-checkout email, but it doesn't let you change the timing logic much, add an SMS, post to Slack or skip low-value carts. This guide builds the flow yourself in n8n: Shopify tells n8n when a checkout starts, n8n waits two hours, checks whether the shopper paid, and only if they didn't, sends a recovery email or a text message. You can paste the finished workflow straight into n8n from section 4.

How this guide was made

We imported the workflow JSON below into n8n 2.43.8 and ran it against a mock of Shopify's Admin GraphQL API and a local mail server, with five test checkouts: unpaid with an email, already paid, no email but SMS opt-in, no contact details, and a Shopify API error. Each one took the expected path, and the recovery email arrived with the cart lines and link filled in. The Twilio step was checked for routing only. We haven't run it against a live Shopify store, so place a test order before you switch it on. Node settings and webhook fields come from n8n's node source and Shopify's API reference, checked in October 2026.

1. How the flow works

Abandoned-cart recovery in n8n
Abandoned-cart recovery in n8nShopify Trigger → Wait 2 hours → Find order for this checkout → Order found: stop, they bought / No order: still abandoned → Has email: send recovery email / No email, SMS opt-in: send SMS via TwilioShopify Trigger: checkouts/create webhook: token, email, phone, cart, recovery linkShopify Triggercheckouts/create webhook: token, email, phone, cart, recoverylinkWait 2 hours: The execution sleeps in n8n's database, so restarts don't lose itWait 2 hoursThe execution sleeps in n8n's database, so restarts don't loseitFind order for this checkout: Admin GraphQL: orders with checkout_token equal to the checkout's tokenFind order for this checkoutAdmin GraphQL: orders with checkout_token equal to thecheckout's tokenOrder found: stop, they boughtOrder found: stop, theyboughtNo order: still abandonedNo order: still abandonedHas email: send recovery emailHas email: send recoveryemailNo email, SMS opt-in: send SMS via TwilioNo email, SMS opt-in: sendSMS via Twilio
Eight nodes, one n8n execution per checkout started. Hover or tab through the boxes for details.

The key step is the order check. Shopify's checkouts/create webhook fires for every checkout, including the ones that turn into orders a minute later. Every order created from a checkout carries that checkout's token, and the Admin API's orders query can filter by checkout_token. So after the wait, one query tells you whether the shopper finished.

2. Before you start

  • n8n. n8n Cloud, or a recent self-hosted n8n reachable over HTTPS from the internet, because Shopify has to deliver the webhook to it. Our self-hosted n8n guide covers the server side.
  • A Shopify app with Admin API access and the read_orders scope (n8n's docs note that read_orders also needs read_products). In n8n, create a "Shopify Access Token API" credential with your shop subdomain, the Admin API access token and the app's API secret key. The Shopify Trigger node uses the secret to verify webhooks.
  • If your store has no custom app yet: since January 1, 2026, Shopify no longer lets you create new legacy custom apps from the store admin, and apps created in Shopify's Dev Dashboard don't show a permanent access token. Use n8n's Shopify OAuth2 credential instead, and set it on both the Shopify Trigger and the HTTP Request node.
  • Email: an SMTP account, from your email provider or a transactional service. In n8n, create an SMTP credential.
  • SMS (optional): a Twilio account with a sending number and a Twilio credential in n8n.

3. The nodes, one by one

Shopify Trigger: checkouts/create

Set Trigger On to "Checkout Created". When you activate the workflow, n8n registers the webhook in your store. The payload includes the fields the rest of the flow uses:

{
  "token": "123123123",
  "email": "[email protected]",
  "buyer_accepts_marketing": false,
  "buyer_accepts_sms_marketing": false,
  "sms_marketing_phone": null,
  "abandoned_checkout_url": "https://checkout.local/548380009/checkouts/123123123/recover?key=example-secret-token",
  "total_price": "421.88",
  "presentment_currency": "USD",
  "line_items": [{"title": "IPod Nano - 8GB", "quantity": 1, "price": "199.00"}]
}

That's an excerpt of Shopify's sample payload. abandoned_checkout_url takes the shopper back to their checkout with the cart filled in.

Wait: 2 hours

Resume "After Time Interval", amount 2, unit hours. Waits longer than about a minute are saved to n8n's database and resumed later, so an n8n restart doesn't lose them. Two hours is a common first reminder. Some stores test one hour, others the next morning. Change the number, not the structure.

HTTP Request: find an order for this checkout

A POST to your store's Admin GraphQL endpoint, https://YOUR-STORE.myshopify.com/admin/api/2026-07/graphql.json, using the Shopify credential (Authentication: Predefined Credential Type, Shopify Access Token API). The body sends this query, with the checkout token from the trigger:

query ($q: String!) {
  orders(first: 1, query: $q) {
    nodes { id name displayFinancialStatus }
  }
}
# variables: { "q": "checkout_token:<token from the trigger>" }

Shopify retires API versions about a year after release. If the request starts failing with a version error, change 2026-07 in the URL to a current version.

IF: still unpaid?

Checks {{ $json.data.orders.nodes.length }} equals 0. No order means the checkout is still abandoned, so the flow goes on. Any order, including one waiting on a bank transfer, ends the flow, because that shopper doesn't need a reminder. If Shopify returns an error instead of order data, the condition doesn't match and the flow stops without sending anything, which we confirmed in testing. A failed check never turns into an unwanted email, but it also fails quietly, so look at your executions now and then, or set an error workflow in the workflow settings. If the HTTP request itself fails, for example with an expired token, the execution fails and shows up in n8n's error list.

IF: has email? Then send the email

Email is the default channel. The Send Email node uses your SMTP credential and builds the message from the trigger data: the first name if Shopify has it, the cart lines, the total and a "Finish your order" link to abandoned_checkout_url. The workflow turns off n8n's default "sent automatically with n8n" footer.

IF: opted in to SMS? Then send the SMS

Only for shoppers with no email, and only when buyer_accepts_sms_marketing is true and sms_marketing_phone is set. Recovery texts are marketing messages in most countries, and sending them without consent can break laws such as the US TCPA. The Twilio node sends a short message with the recovery link and opt-out wording.

4. The complete workflow JSON

Copy the JSON, open an empty workflow in n8n and paste with Ctrl+V (Cmd+V on a Mac). The nodes appear on the canvas. Then:

  1. Select your Shopify credential on the Shopify Trigger and Find order for this checkout nodes.
  2. Replace YOUR-STORE in the HTTP Request URL with your shop subdomain.
  3. Select your SMTP credential and set the From address in Send recovery email.
  4. For SMS, select your Twilio credential and set your Twilio number in Send recovery SMS. If you don't use SMS, delete the last two nodes.
  5. Activate the workflow.
{
  "name": "Abandoned-cart recovery (Shopify)",
  "nodes": [
    {
      "parameters": {
        "authentication": "accessToken",
        "topic": "checkouts/create"
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a01",
      "name": "Shopify Trigger",
      "type": "n8n-nodes-base.shopifyTrigger",
      "typeVersion": 1,
      "position": [0, 300],
      "webhookId": "7d3c2a1b-0e9f-4a8b-8c7d-6e5f4a3b2c01"
    },
    {
      "parameters": {
        "amount": 2,
        "unit": "hours"
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a02",
      "name": "Wait 2 hours",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [220, 300],
      "webhookId": "7d3c2a1b-0e9f-4a8b-8c7d-6e5f4a3b2c02"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR-STORE.myshopify.com/admin/api/2026-07/graphql.json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "shopifyAccessTokenApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ query: 'query ($q: String!) { orders(first: 1, query: $q) { nodes { id name displayFinancialStatus } } }', variables: { q: 'checkout_token:' + $('Shopify Trigger').item.json.token } }) }}",
        "options": {}
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a03",
      "name": "Find order for this checkout",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [440, 300]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1a2b3c4-0001-4000-8000-000000000001",
              "leftValue": "={{ $json.data.orders.nodes.length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a04",
      "name": "Still unpaid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [660, 300]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1a2b3c4-0002-4000-8000-000000000002",
              "leftValue": "={{ $('Shopify Trigger').item.json.email || '' }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a05",
      "name": "Has email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [880, 200]
    },
    {
      "parameters": {
        "fromEmail": "Your Store <[email protected]>",
        "toEmail": "={{ $('Shopify Trigger').item.json.email }}",
        "subject": "You left something in your cart",
        "emailFormat": "html",
        "html": "=<p>Hi{{ $('Shopify Trigger').item.json.customer?.first_name ? ' ' + $('Shopify Trigger').item.json.customer.first_name : '' }},</p>\n<p>You left these in your cart:</p>\n<ul>{{ $('Shopify Trigger').item.json.line_items.map(i => '<li>' + i.quantity + ' × ' + i.title + '</li>').join('') }}</ul>\n<p>Total: {{ $('Shopify Trigger').item.json.total_price }} {{ $('Shopify Trigger').item.json.presentment_currency }}</p>\n<p><a href=\"{{ $('Shopify Trigger').item.json.abandoned_checkout_url }}\">Finish your order</a></p>",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a06",
      "name": "Send recovery email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [1100, 100]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1a2b3c4-0003-4000-8000-000000000003",
              "leftValue": "={{ $('Shopify Trigger').item.json.buyer_accepts_sms_marketing === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            },
            {
              "id": "c1a2b3c4-0004-4000-8000-000000000004",
              "leftValue": "={{ $('Shopify Trigger').item.json.sms_marketing_phone || '' }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a07",
      "name": "Opted in to SMS?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1100, 320]
    },
    {
      "parameters": {
        "from": "+15550000000",
        "to": "={{ $('Shopify Trigger').item.json.sms_marketing_phone }}",
        "message": "=Your cart is waiting: {{ $('Shopify Trigger').item.json.abandoned_checkout_url }} Reply STOP to opt out."
      },
      "id": "3b1f0c52-6f5e-4c55-9a0e-1c2d3e4f5a08",
      "name": "Send recovery SMS",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [1320, 300]
    }
  ],
  "connections": {
    "Shopify Trigger": { "main": [[{ "node": "Wait 2 hours", "type": "main", "index": 0 }]] },
    "Wait 2 hours": { "main": [[{ "node": "Find order for this checkout", "type": "main", "index": 0 }]] },
    "Find order for this checkout": { "main": [[{ "node": "Still unpaid?", "type": "main", "index": 0 }]] },
    "Still unpaid?": { "main": [[{ "node": "Has email?", "type": "main", "index": 0 }], []] },
    "Has email?": { "main": [[{ "node": "Send recovery email", "type": "main", "index": 0 }], [{ "node": "Opted in to SMS?", "type": "main", "index": 0 }]] },
    "Opted in to SMS?": { "main": [[{ "node": "Send recovery SMS", "type": "main", "index": 0 }], []] }
  },
  "settings": { "executionOrder": "v1" },
  "pinData": {}
}

5. Test it before customers see it

  1. Change the Wait node to 2 minutes and activate the workflow.
  2. In your store, start a checkout with your own email, then close the tab. After two minutes the execution should continue, find no order and send you the email. Check that the link restores the cart.
  3. Start a second checkout and complete it with a test payment or a 100% discount code. That execution should stop at Still unpaid?.
  4. Look at both runs in n8n's Executions list, then set the Wait back to 2 hours.

6. What it costs to run

Each checkout started is one n8n execution, whether or not a message goes out. The waiting time doesn't add anything.

Where it runsCheckouts per month it coversPrice
n8n Cloud StarterUp to 2,500, minus your other workflows€20 a month billed annually, €24 monthly
n8n Cloud ProUp to 10,000€50 a month billed annually, €60 monthly
Self-hosted n8nNo execution limitYour server and time, about $12 to $15 a month for a VPS

n8n prices from n8n's pricing page, October 2026. Email via SMTP costs whatever your mail provider charges, and Twilio bills per SMS. The same flow on Make uses a credit for each module that runs, about 4 to 6 credits per checkout. Our n8n vs Make cost guide works through the numbers.

7. Ideas for extending it

  • A second reminder. After the email node, add another Wait (24 hours), the same order check and a second email, for example with a discount code.
  • Skip small carts. Add a condition on total_price to the first IF so you only chase carts above a set value.
  • Tell your team. Send carts over a set value to Slack so someone can follow up personally.
  • Avoid double reminders. A shopper who starts two checkouts gets two emails. To prevent that, check the email address against recent orders as well, or keep a short list of recently emailed addresses in a data table.

8. Tools

Top pick

ToolRoleWhy it fits
n8n CloudHosted workflow automationRuns this workflow without a server to maintain: HTTPS webhooks, saved waiting executions and updates are handled for you. Billing is per execution, so a checkout is one execution however many steps the flow has. Starter covers 2,500 executions a month.

Alternatives: free tools and tools we have no affiliation with

We don't earn anything from these links. They're here because for some readers they're the better choice.

9. Pitfalls

  • The workflow isn't active. n8n only registers the Shopify webhook when the workflow is activated. Testing in the editor with the workflow switched off won't receive real checkouts.
  • n8n isn't reachable. A self-hosted n8n on localhost, or without a public HTTPS URL, never receives webhooks. Set WEBHOOK_URL to your public address.
  • Missing email at checkout start. The payload carries whatever the shopper had entered when the checkout was created. Checkouts with no email and no SMS opt-in end without a message. That's intended.
  • Ignoring consent rules. In the EU and the UK, recovery emails can count as marketing. If that applies to you, add buyer_accepts_marketing is true as a condition on the email branch.
  • An old API version. Update the version in the GraphQL URL at least once a year.
Bottom line

Shopify's checkouts/create webhook, a two-hour Wait node and one GraphQL query on checkout_token are all you need to tell abandoned carts from completed ones. Paste the JSON above, connect your Shopify, SMTP and Twilio credentials, test with your own checkout, and you have a recovery flow you can extend however you like, at one n8n execution per checkout.

Tools mentioned in this guide · affiliate links

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.