{
  "name": "Missed-call text-back (Insurance Agents)",
  "nodes": [
    {
      "id": "017d35d4-dbb6-54be-8dbf-e127ff36fb1d",
      "name": "Call ended (from Twilio)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "missed-call",
        "responseMode": "responseNode",
        "options": {}
      },
      "webhookId": "4bac7d47-6570-5cd0-a3ac-68e3b229cec7"
    },
    {
      "id": "3545ff76-0754-5be1-b960-c7fad6efe72f",
      "name": "Incoming text",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "missed-call-sms",
        "responseMode": "responseNode",
        "options": {}
      },
      "webhookId": "b208c150-dd49-5a5d-b4ce-444aee080690"
    },
    {
      "id": "cb89cc3a-2c7c-5a27-93d6-fab2eefc6fe9",
      "name": "Config (edit me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        140
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "33030796-7a57-528a-85a1-c11562b85ce4",
              "name": "business_name",
              "value": "YOUR BUSINESS NAME",
              "type": "string"
            },
            {
              "id": "1f132e43-d826-5d13-962f-1a89d709c088",
              "name": "booking_link",
              "value": "https://YOUR-BOOKING-LINK",
              "type": "string"
            },
            {
              "id": "343cbc1c-368d-55c5-a93a-e72f39965a9c",
              "name": "hours",
              "value": "Mon-Fri 8am-6pm",
              "type": "string"
            },
            {
              "id": "9381070e-4023-5d67-a63e-6936d3a94a49",
              "name": "owner_email",
              "value": "you@example.com",
              "type": "string"
            },
            {
              "id": "919b707c-bed7-5de1-8f27-097ca27d2ed8",
              "name": "owner_cell",
              "value": "+15555550100",
              "type": "string"
            },
            {
              "id": "6c6da7ee-46e8-5516-a6dd-499cf36f4c24",
              "name": "twilio_number",
              "value": "+15555550199",
              "type": "string"
            },
            {
              "id": "d3b9765b-97b5-594a-affa-026a676f6f77",
              "name": "twilio_auth_token",
              "value": "PASTE_YOUR_TWILIO_AUTH_TOKEN",
              "type": "string"
            },
            {
              "id": "677030dc-8cf3-5d08-a0f4-9aa4a4ad0cb6",
              "name": "twilio_sms_webhook_url",
              "value": "https://YOUR-N8N-URL/webhook/missed-call-sms",
              "type": "string"
            },
            {
              "id": "47d73b96-4332-5e17-b4c3-b6875757b7a7",
              "name": "twilio_voice_webhook_url",
              "value": "https://YOUR-N8N-URL/webhook/missed-call",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      }
    },
    {
      "id": "6f210fea-6be0-5bc9-98cc-1e59c3b6d11e",
      "name": "Check Twilio signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        140
      ],
      "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(params.MessageSid !== undefined ? cfg.twilio_sms_webhook_url : cfg.twilio_voice_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": "fa6d958e-c5c7-5d41-9f93-8ec749e387e4",
      "name": "Signature valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        660,
        140
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "0695b3a5-d573-5cce-822b-68c94bfadf48",
              "leftValue": "={{ $json.signature_ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "fb9f7942-1604-5240-a0ce-fc7202c08051",
      "name": "Reject: not from Twilio (403)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        880,
        320
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "Forbidden",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/plain"
              }
            ]
          },
          "responseCode": 403
        }
      }
    },
    {
      "id": "3e0c223a-319c-5ca9-9c88-a85a3e7da805",
      "name": "Call or text?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        140
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "6a074c40-5950-535d-8c98-3471f6a96da6",
              "leftValue": "={{ $json.body.DialCallStatus }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "2be879a7-9c89-5d79-8460-5f453fb6066c",
      "name": "Tell Twilio what to say",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "={{ $json.body.DialCallStatus === 'completed' ? '<Response><Hangup/></Response>' : '<Response><Say>Sorry we missed you. We are texting you right now.</Say><Hangup/></Response>' }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/xml"
              }
            ]
          }
        }
      }
    },
    {
      "id": "f0adaa6f-292b-5fd9-9c13-94d6317483d4",
      "name": "Was the call missed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "b30032c4-75b8-5c5c-a987-950a97646999",
              "leftValue": "={{ $('Call ended (from Twilio)').first().json.body.DialCallStatus }}",
              "rightValue": "completed",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "1ef98fb0-71b1-5ad1-ac8e-90b839c88d68",
      "name": "Text the caller",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Call ended (from Twilio)').first().json.body.From }}",
        "message": "=Hi, this is {{ $('Config (edit me)').first().json.business_name }}. Sorry we missed your call, we're probably on the phone with a client. Reply with your name, what you'd like a quote on (auto, home, business, life) and a good time to call, and a licensed agent will get back to you. Please don't text Social Security, license or policy numbers. Book a time: {{ $('Config (edit me)').first().json.booking_link }} Need to talk to an agent today? Reply URGENT.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "a47588d5-9233-579a-8cda-b23b5969bcbd",
      "name": "Email the owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1760,
        0
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=Missed call from {{ $('Call ended (from Twilio)').first().json.body.From }}",
        "emailType": "text",
        "message": "=We missed a call from {{ $('Call ended (from Twilio)').first().json.body.From }} and texted them automatically. Call them back when you can.",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      }
    },
    {
      "id": "33a6a425-2d91-53ae-9603-f8338c125c6f",
      "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": "13f5f0c6-a907-5852-980f-dd1efe539e5b",
      "name": "Says URGENT?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "f1ecab38-6e4e-5f43-a874-66262536a304",
              "leftValue": "={{ $('Incoming text').first().json.body.Body }}",
              "rightValue": "urgent",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "2f4e1acc-e51d-558d-a573-ab45833b66c2",
      "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": "fbdd6401-645a-5a0e-873d-39c481f82b7d",
      "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've texted the agent's cell and someone will call you shortly. Coverage can't be started or changed by text. If anyone is hurt or in danger, call 911. - {{ $('Config (edit me)').first().json.business_name }}",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "4c0f484b-c3f7-5ebf-8179-055bef6e454d",
      "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": "ac09df86-7bbc-5ee4-8e60-da41ea5fb16d",
      "name": "AI writes reply",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        1540,
        660
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "id"
        },
        "messages": {
          "values": [
            {
              "content": "You are the friendly front-desk assistant at a small independent insurance agency. You are not a licensed agent. Write one short, warm SMS reply (under 300 characters) to a person asking about insurance. Thank them, say a licensed agent will call them back, and offer the booking link. You may ask what type of coverage they'd like a quote on and a good time to call. Never quote, estimate or compare premiums or rates. Never explain, interpret, compare or recommend coverage, policies or carriers. Never say coverage is bound, active, changed or cancelled, and never promise a price or savings. Ask them not to text Social Security, driver's license, policy or bank numbers. If they mention an accident, injury, fire or other emergency, tell them to call 911 if anyone is hurt or in danger and to call the office. Sign off with the agency name.",
              "role": "system"
            },
            {
              "content": "=Business: {{ $('Config (edit me)').first().json.business_name }}\nHours: {{ $('Config (edit me)').first().json.hours }}\nBooking link: {{ $('Config (edit me)').first().json.booking_link }}\nCustomer's text: {{ $('Incoming text').first().json.body.Body }}"
            }
          ]
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "REPLACE_OPENAI_CREDENTIAL",
          "name": "OpenAI account (placeholder)"
        }
      }
    },
    {
      "id": "b45ee125-6563-50ba-823e-c40970ce64cf",
      "name": "Text the AI reply",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1760,
        660
      ],
      "parameters": {
        "from": "={{ $('Config (edit me)').first().json.twilio_number }}",
        "to": "={{ $('Incoming text').first().json.body.From }}",
        "message": "={{ $json.message.content }}",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "id": "REPLACE_TWILIO_CREDENTIAL",
          "name": "Twilio account (placeholder)"
        }
      }
    },
    {
      "id": "0c19142d-ce49-5d25-a194-df844b2c2ad3",
      "name": "Email owner about the text",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1980,
        660
      ],
      "parameters": {
        "sendTo": "={{ $('Config (edit me)').first().json.owner_email }}",
        "subject": "=New text from {{ $('Incoming text').first().json.body.From }}",
        "emailType": "text",
        "message": "=They wrote: {{ $('Incoming text').first().json.body.Body }}\n\nWe auto-replied: {{ $('AI writes reply').first().json.message.content }}",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_GMAIL_CREDENTIAL",
          "name": "Gmail account (placeholder)"
        }
      }
    },
    {
      "id": "d45a1802-19ad-56b2-b7e9-c260c3da2bb7",
      "name": "Setup steps",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -160
      ],
      "parameters": {
        "content": "## Setup (about 1-2 hours)\n1. Open **Config (edit me)** and fill in your business info, Twilio number, Twilio **auth token** (Twilio Console > Account info) and both webhook URLs.\n2. Click each node with a red warning and connect your own **Twilio**, **OpenAI**, **Gmail** credentials.\n3. In Twilio, on your number: Voice > 'A call comes in' > TwiML Bin with:\n`<Response><Dial timeout=\"20\" action=\"https://YOUR-N8N-URL/webhook/missed-call\">+1YOURCELL</Dial></Response>`\nPaste that exact action URL into **twilio_voice_webhook_url**.\n4. In Twilio > Phone Numbers > your number > Messaging: 'A message comes in' > Webhook, HTTP POST, `https://YOUR-N8N-URL/webhook/missed-call-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.\n5. Register your number for A2P 10DLC in Twilio before texting customers.\n6. Activate the workflow and run the tests on the setup page.\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": 620,
        "width": 520
      }
    }
  ],
  "connections": {
    "Call ended (from Twilio)": {
      "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": "Check Twilio signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Twilio signature": {
      "main": [
        [
          {
            "node": "Signature valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signature valid?": {
      "main": [
        [
          {
            "node": "Call or text?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject: not from Twilio (403)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call or text?": {
      "main": [
        [
          {
            "node": "Tell Twilio what to say",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Acknowledge Twilio (text)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tell Twilio what to say": {
      "main": [
        [
          {
            "node": "Was the call missed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Was the call missed?": {
      "main": [
        [
          {
            "node": "Text the caller",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the caller": {
      "main": [
        [
          {
            "node": "Email the owner",
            "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": "AI writes reply",
            "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
          }
        ]
      ]
    },
    "AI writes reply": {
      "main": [
        [
          {
            "node": "Text the AI reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the AI reply": {
      "main": [
        [
          {
            "node": "Email owner about the text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false,
    "generatedBy": "Speed-to-Lead Playbooks"
  },
  "tags": []
}