{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Send WhatsApp Apology & Reorder Link When Shopify Order is Cancelled",
  "nodes": [
    {
      "id": "19ca882d-78dd-42f7-b57b-d10f46d4ec56",
      "name": "Code",
      "type": "n8n-nodes-base.code",
      "position": [
        -1104,
        544
      ],
      "parameters": {
        "jsCode": "// Get input data (assuming one item only)\nconst order = items[0].json;\n\n// Extract important data\nconst result = {\n  number: order.number,\n  orderNumber: order.order_number,\n  orderUrl: order.order_status_url,\n  confirmationNumber: order.confirmation_number,\n  createdAt: order.created_at,\n  cancelledAt: order.cancelled_at,\n  status: {\n    financial: order.financial_status,\n    fulfillment: order.fulfillment_status,\n  },\n  total: {\n    price: order.total_price,\n    subtotal: order.subtotal_price,\n    tax: order.total_tax,\n    currency: order.currency,\n  },\n  customer: {\n    email: order.customer?.email || null,\n    phone: order.customer?.phone || null,\n    name: `${order.customer?.first_name || ''} ${order.customer?.last_name || ''}`.trim(),\n  },\n  address: {\n    billing: {\n      name: `${order.billing_address?.first_name || ''} ${order.billing_address?.last_name || ''}`.trim(),\n      phone: order.billing_address?.phone || null,\n      address1: order.billing_address?.address1 || null,\n      address2: order.billing_address?.address2 || null,\n      city: order.billing_address?.city || null,\n      zip: order.billing_address?.zip || null,\n      country: order.billing_address?.country || null,\n    },\n    shipping: {\n      name: `${order.shipping_address?.first_name || ''} ${order.shipping_address?.last_name || ''}`.trim(),\n      phone: order.shipping_address?.phone || null,\n      address1: order.shipping_address?.address1 || null,\n      address2: order.shipping_address?.address2 || null,\n      city: order.shipping_address?.city || null,\n      zip: order.shipping_address?.zip || null,\n      country: order.shipping_address?.country || null,\n    }\n  },\n  product: order.line_items.map(item => ({\n    title: item.title,\n    price: item.price,\n    quantity: item.quantity,\n    tax: item.tax_lines?.[0]?.price || 0,\n  })),\n  fulfillment: order.fulfillments?.[0]\n    ? {\n        trackingNumber: order.fulfillments[0].tracking_number,\n        trackingUrl: order.fulfillments[0].tracking_url,\n        status: order.fulfillments[0].status,\n      }\n    : null,\n  refund: order.refunds?.[0]\n    ? {\n        reason: order.refunds[0].note || 'N/A',\n        amount: order.refunds[0].transactions?.[0]?.amount || '0',\n        date: order.refunds[0].created_at,\n      }\n    : null,\n};\n\n// Return simplified data\nreturn [{ json: result }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c02ef42f-6092-4893-baf3-999228b07085",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1328,
        528
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "ed565cef-4455-4f18-a9c1-316d94ef2c65",
      "name": "Shopify Trigger",
      "type": "n8n-nodes-base.shopifyTrigger",
      "position": [
        -1520,
        528
      ],
      "parameters": {
        "topic": "orders/cancelled",
        "authentication": "accessToken"
      },
      "credentials": {
        "shopifyAccessTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4fb74742-5ee8-4a75-884d-311023a8a267",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        -80,
        544
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "3ab8725a-9268-45fd-b91c-0e055fe6fa7c",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.data.exists }}",
              "rightValue": "=\"true\""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "35b0be87-6a5a-46da-bd70-35a73d80a373",
      "name": "Wait",
      "type": "n8n-nodes-base.wait",
      "position": [
        352,
        640
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "ddf47009-eef3-452c-ad02-ea18dfa9d171",
      "name": "Clean WhatsApp Number",
      "type": "n8n-nodes-base.code",
      "position": [
        -688,
        544
      ],
      "parameters": {
        "jsCode": "const items = await $input.all();\n\nconst updatedItems = items.map((item) => {\n  let rawNumber = item?.json?.address?.billing?.phone;\n  rawNumber = rawNumber ? String(rawNumber) : \"\";\n\n  const cleanedNumber = rawNumber.replace(/\\D/g, \"\"); // Remove non-digit characters\n\n  // Update the nested phone field\n  if (item.json.address?.billing) {\n    item.json.address.billing.phone = cleanedNumber;\n  }\n\n  return item;\n});\n\nreturn updatedItems;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "95654791-084f-4847-babe-13685c2c45ee",
      "name": "Save State of Rows in Verified & Sent",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        336,
        416
      ],
      "parameters": {
        "columns": {
          "value": {
            "name": "={{ $('Clean WhatsApp Number').item.json.customer.name }}",
            "email": "={{ $('Clean WhatsApp Number').item.json.customer.email }}",
            "price": "={{ $('Clean WhatsApp Number').item.json.total.currency }} {{ $('Clean WhatsApp Number').item.json.total.price }}",
            "staus": "sent",
            "title": "={{ $('Clean WhatsApp Number').item.json.product[0].title }}",
            "number": "={{ $json.to }}",
            "address1": "={{ $('Clean WhatsApp Number').item.json.address.billing.address1 }}",
            "validity": "verified",
            "re-order link": "={{ $('Code').item.json.orderUrl }}"
          },
          "schema": [
            {
              "id": "name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "number",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "address1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "address1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "price",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "re-order link",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "re-order link",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "validity",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "validity",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "staus",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "staus",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/12zMJod9s3Ov0RZh-7-ZoqTvJr4yMIqOeAQKTBeZlrRk/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "12zMJod9s3Ov0RZh-7-ZoqTvJr4yMIqOeAQKTBeZlrRk",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/12zMJod9s3Ov0RZh-7-ZoqTvJr4yMIqOeAQKTBeZlrRk/edit?usp=drivesdk",
          "cachedResultName": "Automated WhatsApp Apology Flow for Cancelled Shopify Orders with Reorder Link"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "b2612612-56b2-47d3-ac2b-24bbae46b23a",
      "name": "Save State of Rows in Verified & Sent1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        128,
        640
      ],
      "parameters": {
        "columns": {
          "value": {
            "name": "={{ $('Clean WhatsApp Number').item.json.customer.name }}",
            "email": "={{ $('Clean WhatsApp Number').item.json.customer.email }}",
            "price": "={{ $('Clean WhatsApp Number').item.json.total.currency }} {{ $('Clean WhatsApp Number').item.json.total.price }}",
            "staus": "not sent",
            "title": "={{ $('Clean WhatsApp Number').item.json.product[0].title }}",
            "number": "={{ $('Clean WhatsApp Number').item.json.customer.phone }}",
            "address1": "={{ $('Clean WhatsApp Number').item.json.address.billing.address1 }}",
            "validity": "unverified"
          },
          "schema": [
            {
              "id": "name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "number",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "address1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "address1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "price",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "price",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "re-order link",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "re-order link",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "validity",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "validity",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "staus",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "staus",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/12zMJod9s3Ov0RZh-7-ZoqTvJr4yMIqOeAQKTBeZlrRk/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "12zMJod9s3Ov0RZh-7-ZoqTvJr4yMIqOeAQKTBeZlrRk",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/12zMJod9s3Ov0RZh-7-ZoqTvJr4yMIqOeAQKTBeZlrRk/edit?usp=drivesdk",
          "cachedResultName": "Automated WhatsApp Apology Flow for Cancelled Shopify Orders with Reorder Link"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "df643747-05d9-4101-bdcf-a53eb3d20045",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2720,
        96
      ],
      "parameters": {
        "width": 1072,
        "height": 1536,
        "content": "# Send WhatsApp Apology & Reorder Link When Shopify Order is Cancelled\n\n## Overview\nThis **n8n workflow** listens for order cancellations in Shopify, extracts relevant customer and order data, checks if the customer\u2019s phone number is registered on WhatsApp via the **Rapiwa API**, and sends a personalized apology message with a re-order link. It also logs successful and unsuccessful attempts in **Google Sheets** for tracking.\n\n\n## Features\n- Automatically detects cancelled orders via Shopify webhook.\n- Extracts customer data including name, email, phone, order details, and shipping address.\n- Cleans the phone number and converts it to WhatsApp-compatible format.\n- Verifies if the phone number is registered on WhatsApp using Rapiwa API.\n- Sends an apology message with a re-order link if the number is valid.\n- Logs both verified/sent and unverified/not sent entries in Google Sheets.\n- Rate limiting is controlled using `Wait` and `SplitInBatches` nodes.\n\n## Requirements\n- A Shopify store with order cancellation webhook enabled.\n- A Rapiwa account and API token.\n- An n8n instance (cloud or self-hosted).\n- Google Sheet set up to log message statuses.\n- Shopify Access Token (Private App or Admin API).\n- Verified Rapiwa WhatsApp number and message credits.\n\n\n## Google Sheet Required Columns\nYou\u2019ll need two Google Sheets (or two tabs in one spreadsheet):\n**A Google Sheet formatted like this** \u27a4 [sample](https://docs.google.com/spreadsheets/d/12zMJod9s3Ov0RZh-7-ZoqTvJr4yMIqOeAQKTBeZlrRk/edit?usp=sharing)\n\n## Nodes Used in the Workflow\n- **Shopify Trigger** (`orders/cancelled`)\n- **SplitInBatches**\n- **Code** (Simplify order data)\n- **Code** (Clean WhatsApp Number)\n- **HTTP Request** (Verify via Rapiwa)\n- **IF Node** (Check Rapiwa result)\n- **HTTP Request** (Send Message via Rapiwa)\n- **Google Sheets** (Append logs - verified)\n- **Google Sheets** (Append logs - unverified)\n- **Wait** (to control request flow)\n\n\n## Notes & Warnings\n- The `IF` node checks for `data.exists === true` to verify WhatsApp number status \u2014 adjust if the API changes.\n- Make sure **Rapiwa API rate limits** are respected to avoid temporary bans.\n- Number formatting must strip non-digits and use the correct country code (e.g., `88017xxxxxxx`).\n- Double-check message templates to avoid policy violations on WhatsApp Business.\n- Be careful with customer privacy. Ensure compliance with **GDPR** or your local laws.\n\n\n## Useful Links\n- **Dashboard:** [https://app.rapiwa.com](https://app.rapiwa.com/login)\n- **Official Website:** [https://rapiwa.com](https://rapiwa.com/)\n- **Documentation:** [https://docs.rapiwa.com](https://docs.rapiwa.com/)\n\n## Support & Help\n- **WhatsApp**: [Chat on WhatsApp](https://wa.me/8801322827799)\n- **Discord**: [SpaGreen Community](https://discord.gg/SsCChWEP)\n- **Facebook Group**: [SpaGreen Support](https://www.facebook.com/groups/spagreenbd)\n- **Website**: [https://spagreen.net](https://spagreen.net)\n- **Developer Portfolio**: [Codecanyon SpaGreen](https://codecanyon.net/user/spagreen/portfolio)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a9e35272-f2c6-4785-9610-2269ffb3276c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1600,
        96
      ],
      "parameters": {
        "width": 656,
        "height": 752,
        "content": "## Shopify Trigger (Order Cancelled)\n**Purpose:** Starts the workflow whenever an order is cancelled in Shopify  \n**How it works:**  \n- Listens to the Shopify webhook for order cancellation events  \n- Automatically triggers the workflow on each cancellation  \n\n## SplitInBatches\n**Purpose:** Processes cancelled orders in manageable batches  \n\n## Code (Simplify Order Data)\n**Purpose:** Extracts and formats important customer and order details for later use  \n**How it works:**  \n- Parses the Shopify webhook data  \n- Extracts customer name, phone, email, address, order total, order ID, and product details  \n- Converts order total to a fixed decimal string  \n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "20ec2d81-8006-417d-92b5-668993d0586e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        208
      ],
      "parameters": {
        "width": 720,
        "height": 640,
        "content": "## Code (Clean WhatsApp Number)\n**Purpose:** Formats and sanitizes the customer phone number for WhatsApp usage\n**It works:** Removes all non-digit characters (spaces, dashes, parentheses)  \n\n## HTTP Request (Check WhatsApp Number with Rapiwa)\n**Purpose:** Verifies if the cleaned phone number is registered on WhatsApp using Rapiwa API\n### How it works:\n- Sends POST request to Rapiwa\u2019s `/api/verify-whatsapp` endpoint  \n- Uses Bearer token for authentication  \n- **API Endpoint:** `https://app.rapiwa.com/api/verify-whatsapp`\n"
      },
      "typeVersion": 1
    },
    {
      "id": "6f87f286-0aad-46a2-aef3-15a4b8e99fd6",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -192,
        -160
      ],
      "parameters": {
        "width": 912,
        "height": 1008,
        "content": "## IF\n**Purpose:** Routes the workflow depending on WhatsApp number verification result `verified` or `unverified` number\n\n\n## HTTP Request (Send WhatsApp Message via Rapiwa)\n**Purpose:** Sends a personalized WhatsApp apology message to verified customers\n### How it works:\n- Sends a POST request to Rapiwa\u2019s `/api/send-message`  \n- Uses Bearer token for authorization\n**API Endpoint:** `https://app.rapiwa.com/api/send-message`\n\n## Google Sheets (Log Verified & Sent)\n**Purpose:** Records details of customers successfully sent a WhatsApp message\n### It works:\n- Marks status as `\"verified\"` and `\"sent\"` Confirmation of row appended to Google Sheet\n\n\n## Google Sheets (Log Unverified & Not Sent)\n**Purpose:** Records details of customers whose WhatsApp numbers were not verified\n### It works:\n- Appends a row with customer and order details  \n- Marks status as `\"unverified\"` and `\"not sent\"` Confirmation of row appended to Google Sheet"
      },
      "typeVersion": 1
    },
    {
      "id": "ed95aab3-098c-4f38-ae39-0e469b3894aa",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1600,
        896
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 432,
        "content": "## How to Use This Workflow\n### Step 1: Set Up Credentials\nAdd **Shopify**, **Google Sheets OAuth2**, and **Rapiwa Token** credentials in n8n.\n\n### Step 2: Prepare Google Sheet\n- Create a new Google Sheet with the columns listed above.\n- Connect it inside the two Google Sheets nodes in the workflow.\n\n### Step 3: Import Workflow\n- Use **Import JSON** in n8n to load this complete automation.\n- Make sure all credential references match your system.\n\n### Step 4: Customize Message\n- Edit the **Send Message Using Rapiwa** node to personalize your apology message  \n  or localize it by language or product."
      },
      "typeVersion": 1
    },
    {
      "id": "f24902f4-c503-4844-b022-c1005adfe689",
      "name": "Rapiwa",
      "type": "n8n-nodes-rapiwa.rapiwa",
      "position": [
        -480,
        544
      ],
      "parameters": {
        "number": "={{ $json.address.billing.phone }}",
        "operation": "verifyWhatsAppNumber"
      },
      "credentials": {
        "rapiwaApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "fde3fe4b-a404-4847-b8ce-7868eb71875f",
      "name": "Rapiwa (send message)",
      "type": "n8n-nodes-rapiwa.rapiwa",
      "position": [
        128,
        416
      ],
      "parameters": {
        "number": "={{ $('Clean WhatsApp Number').item.json.address.billing.phone }}",
        "messageType": "=Dear {{ $('Clean WhatsApp Number').item.json.customer.name }},\nWe\u2019re really sorry about the issue with your order. \ud83d\ude4f\nYou can re-order using this link:\ud83d\udc49 {{ $('Clean WhatsApp Number').item.json.orderUrl }}\nWe\u2019ve added a small discount for the inconvenience.\nThanks,\nTeam SpaGreen Creative"
      },
      "credentials": {
        "rapiwaApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Rapiwa (send message)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save State of Rows in Verified & Sent1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code": {
      "main": [
        [
          {
            "node": "Clean WhatsApp Number",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rapiwa": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shopify Trigger": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean WhatsApp Number": {
      "main": [
        [
          {
            "node": "Rapiwa",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rapiwa (send message)": {
      "main": [
        [
          {
            "node": "Save State of Rows in Verified & Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save State of Rows in Verified & Sent": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save State of Rows in Verified & Sent1": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}