{
  "name": "Google review request after checkout (Barbershops & salons, Square)",
  "nodes": [
    {
      "id": "2f7c8c54-65b8-59c1-b25b-3ed87c6c1659",
      "name": "Every hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      }
    },
    {
      "id": "1b4b94f2-d891-5678-b08f-6b7c76454f04",
      "name": "Incoming text",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "review-sms",
        "responseMode": "responseNode",
        "options": {}
      },
      "webhookId": "3edfdfd3-b460-5d41-986e-14399f921232"
    },
    {
      "id": "88f033c2-3bb3-5c7e-a923-36cfe0117321",
      "name": "Config (edit me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        140
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7860b2e8-6485-5c7d-ae26-6cce3796b7ca",
              "name": "business_name",
              "value": "YOUR BUSINESS NAME",
              "type": "string"
            },
            {
              "id": "39416028-d49c-5fc9-ab36-154eb7b95778",
              "name": "booking_link",
              "value": "https://YOUR-BOOKING-LINK",
              "type": "string"
            },
            {
              "id": "59982c79-5786-527e-8473-5b5fb73aa7c6",
              "name": "hours",
              "value": "Mon-Fri 8am-6pm",
              "type": "string"
            },
            {
              "id": "29c3b980-5b72-5b7a-abc5-659117808127",
              "name": "owner_email",
              "value": "you@example.com",
              "type": "string"
            },
            {
              "id": "48a0d87b-022a-504f-94f4-297aa8698b1d",
              "name": "owner_cell",
              "value": "+15555550100",
              "type": "string"
            },
            {
              "id": "4de028cf-7a54-5fff-9881-d52e5b0a56a6",
              "name": "twilio_number",
              "value": "+15555550199",
              "type": "string"
            },
            {
              "id": "e7a0d592-a348-58d2-a4ed-ab189981200c",
              "name": "twilio_auth_token",
              "value": "PASTE_YOUR_TWILIO_AUTH_TOKEN",
              "type": "string"
            },
            {
              "id": "74427199-5ab3-52d5-93bd-fea922df68c9",
              "name": "twilio_sms_webhook_url",
              "value": "https://YOUR-N8N-URL/webhook/review-sms",
              "type": "string"
            },
            {
              "id": "23814e25-f57f-5407-a199-0f19cc630e5b",
              "name": "google_review_link",
              "value": "https://g.page/r/YOUR_REVIEW_LINK/review",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      }
    },
    {
      "id": "631c92ac-2867-5858-8776-9a5ca4938d3b",
      "name": "Scheduled run or text?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        440,
        140
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "4e358447-14ef-5223-8ebb-5308c237daa1",
              "leftValue": "={{ ($json.body || {}).MessageSid }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "empty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "fe4ac414-f517-53c1-b50b-728344df5029",
      "name": "Get recent Square payments",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        0
      ],
      "parameters": {
        "url": "=https://connect.squareup.com/v2/payments?begin_time={{ encodeURIComponent($now.minus({hours: 3}).toISO()) }}&end_time={{ encodeURIComponent($now.minus({hours: 2}).toISO()) }}&limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_SQUARE_CREDENTIAL",
          "name": "Square access token (placeholder)"
        }
      }
    },
    {
      "id": "5c977e1b-98d1-52de-9db8-b056deab542e",
      "name": "Keep finished visits",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Keep finished payments that have a Square customer attached.\nconst out = [];\nfor (const item of $input.all()) {\n  for (const p of (item.json.payments || [])) {\n    if (p.status === 'COMPLETED' && p.customer_id) {\n      out.push({ json: { payment_id: p.id, customer_id: p.customer_id, paid_at: p.created_at } });\n    }\n  }\n}\nreturn out;"
      }
    },
    {
      "id": "1f274e74-b41a-5791-8edd-f4173dc56bc6",
      "name": "Ask each client only once",
      "type": "n8n-nodes-base.removeDuplicates",
      "typeVersion": 2,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "operation": "removeItemsSeenInPreviousExecutions",
        "logic": "removeItemsWithAlreadySeenKeyValues",
        "dedupeValue": "={{ $json.customer_id }}",
        "options": {}
      }
    },
    {
      "id": "20d1c8e3-30ad-5d1d-8251-cc7833e5818d",
      "name": "Get the client from Square",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "url": "=https://connect.squareup.com/v2/customers/{{ $json.customer_id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_SQUARE_CREDENTIAL",
          "name": "Square access token (placeholder)"
        }
      }
    },
    {
      "id": "e56eae16-3709-5b50-ab10-7bd127e6e7f5",
      "name": "Has a mobile number?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "157c089d-8355-5339-a47b-354f101f5ea6",
              "leftValue": "={{ $json.customer.phone_number }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "b3eff70b-13db-5572-98e5-0d043e69a52d",
      "name": "Text the review link",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1760,
        0
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $json.customer.phone_number }}",
        "message": "=Thanks for coming in today, {{ $json.customer.given_name || 'there' }}! If you liked your cut, a quick Google review helps {{ $('Config (edit me)').first().json.business_name }} a lot: {{ $('Config (edit me)').first().json.google_review_link }} Reply STOP to opt out.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "7bfcd45d-2e56-53b4-a1fb-cf64986cc32f",
      "name": "Check Twilio signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        460
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Is this request really from Twilio? Checks the X-Twilio-Signature header.\n// Twilio signs: the exact webhook URL + every POST field, sorted by name, as name+value,\n// using HMAC-SHA1 keyed with your auth token, base64-encoded.\n// n8n Cloud allows require('crypto'). Self-hosted: set NODE_FUNCTION_ALLOW_BUILTIN=crypto\n// (on the task runner too, if you use one).\nconst crypto = require('crypto');\nconst cfg = $('Config (edit me)').first().json;\nconst req = $json;\nconst params = req.body || {};\nconst url = String(cfg.twilio_sms_webhook_url || '').trim();\nconst data = url + Object.keys(params).sort().map(k => k + params[k]).join('');\nconst expected = crypto.createHmac('sha1', String(cfg.twilio_auth_token || '')).update(data, 'utf8').digest('base64');\nconst received = String((req.headers || {})['x-twilio-signature'] || '');\nconst a = Buffer.from(expected);\nconst b = Buffer.from(received);\nconst ok = url !== '' && received !== '' && a.length === b.length && crypto.timingSafeEqual(a, b);\nreturn { json: { ...req, signature_ok: ok } };"
      }
    },
    {
      "id": "1da73bd3-a76d-537a-b74b-e8d28e2c16e3",
      "name": "Signature valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "38e407fa-674b-5ae9-a9b7-86c108527e3d",
              "leftValue": "={{ $json.signature_ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "aae43fcf-d587-5968-b974-9d5a63ede5a7",
      "name": "Reject: not from Twilio (403)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1100,
        640
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "Forbidden",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/plain"
              }
            ]
          },
          "responseCode": 403
        }
      }
    },
    {
      "id": "89f00f20-82da-59e6-9101-e9b23930861c",
      "name": "Acknowledge Twilio (text)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1100,
        460
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "<Response></Response>",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        }
      }
    },
    {
      "id": "40de91bf-9d13-5f38-9bcd-2f3a5a2e0904",
      "name": "Says URGENT?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "d72da633-3ffc-587a-a5c2-d2a4f179505c",
              "leftValue": "={{ $('Incoming text').first().json.body.Body }}",
              "rightValue": "urgent",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "d45660d6-3ade-5ac9-a638-3acc877693d3",
      "name": "Text the owner (URGENT)",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1540,
        280
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Config (edit me)').first().json.owner_cell }}",
        "message": "=URGENT text to {{ $('Config (edit me)').first().json.business_name }} from {{ $('Incoming text').first().json.body.From }}: \"{{ $('Incoming text').first().json.body.Body }}\" Call or text them back now.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "bd7e6966-a66b-524a-bd07-06ea8921dadf",
      "name": "Text the customer (URGENT ack)",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1760,
        280
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Incoming text').first().json.body.From }}",
        "message": "=Got it, we just sent your message straight to the shop's phone. Thanks for the heads-up! - {{ $('Config (edit me)').first().json.business_name }}",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "fe320c74-6a4b-5548-a308-e6fab2db9740",
      "name": "Email the owner (URGENT, optional)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        280
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=URGENT: text from {{ $('Incoming text').first().json.body.From }}",
        "emailType": "text",
        "message": "=They wrote: {{ $('Incoming text').first().json.body.Body }}\n\nYour cell was texted and they were told you'll get back to them right away.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "78cd2dbf-e5bc-507c-933d-9b6a0fa26c9f",
      "name": "Forward the reply to you",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1540,
        640
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=Reply from {{ $('Incoming text').first().json.body.From }} (replied to a review request)",
        "emailType": "text",
        "message": "=They wrote: {{ $('Incoming text').first().json.body.Body }}\n\nReply to them from your phone or the Twilio console.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      }
    },
    {
      "id": "31e2b6b6-86e8-5ae3-9239-ebecd6b6f996",
      "name": "Setup steps",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -200
      ],
      "parameters": {
        "content": "## Setup (about 1 hour)\n1. In the Square Developer Console, create an app and copy its **production access token** (for your own account only).\n2. In n8n, create a **Header Auth** credential: Name `Authorization`, Value `Bearer YOUR_SQUARE_ACCESS_TOKEN`. Pick it on both Square nodes.\n3. Fill in **Config (edit me)**, including your Google review link (Google Business Profile > Ask for reviews), Twilio **auth token** and the incoming-text webhook URL.\n4. Connect **Twilio** and **Gmail** credentials.\n5. In Twilio > Phone Numbers > your number > Messaging: 'A message comes in' > Webhook, HTTP POST, `https://YOUR-N8N-URL/webhook/review-sms`. Paste the exact same URL into **twilio_sms_webhook_url** in Config. A number has one 'message comes in' webhook, so if you run several of these workflows on one number, point it at only one of them.\n6. Activate. Every hour it looks at payments from 2-3 hours ago, so each client is texted about 2 hours after checkout, once.\n\nSquare only: payments must have a customer attached. Multiple locations? Add `&location_id=...` to the payments URL (default is your main location).\n\n**Security:** the Check Twilio signature node rejects (403) anything not signed with your Twilio auth token. Self-hosted n8n: set `NODE_FUNCTION_ALLOW_BUILTIN=crypto`. Don't share this workflow once your token is in it.",
        "height": 700,
        "width": 540
      }
    }
  ],
  "connections": {
    "Every hour": {
      "main": [
        [
          {
            "node": "Config (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Incoming text": {
      "main": [
        [
          {
            "node": "Config (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config (edit me)": {
      "main": [
        [
          {
            "node": "Scheduled run or text?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scheduled run or text?": {
      "main": [
        [
          {
            "node": "Get recent Square payments",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check Twilio signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get recent Square payments": {
      "main": [
        [
          {
            "node": "Keep finished visits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep finished visits": {
      "main": [
        [
          {
            "node": "Ask each client only once",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask each client only once": {
      "main": [
        [
          {
            "node": "Get the client from Square",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get the client from Square": {
      "main": [
        [
          {
            "node": "Has a mobile number?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has a mobile number?": {
      "main": [
        [
          {
            "node": "Text the review link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Twilio signature": {
      "main": [
        [
          {
            "node": "Signature valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature valid?": {
      "main": [
        [
          {
            "node": "Acknowledge Twilio (text)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject: not from Twilio (403)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acknowledge Twilio (text)": {
      "main": [
        [
          {
            "node": "Says URGENT?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Says URGENT?": {
      "main": [
        [
          {
            "node": "Text the owner (URGENT)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Forward the reply to you",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the owner (URGENT)": {
      "main": [
        [
          {
            "node": "Text the customer (URGENT ack)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the customer (URGENT ack)": {
      "main": [
        [
          {
            "node": "Email the owner (URGENT, optional)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false,
    "generatedBy": "Speed-to-Lead Playbooks"
  },
  "tags": []
}