{"slug":"missed-call-workflows","title":"Missed Call Workflows — Text-Back Automation and Recovery","tags":["missed-call","text-back","sms-followup","conversion","automation"],"agent_summary":"When a caller hangs up before reaching a human, automatically text them within 60 seconds with a contextual message that recovers the conversation. Reduces lost leads 40-70% in service industries. Implemented via call event webhooks (no-answer, busy, voicemail) + SMS API, with GHL or CRM integration for thread continuity.","trigger_phrases":["missed call text back","MCTB","missed call followup","recover missed call","no answer SMS","call abandoned recovery"],"runnable":true,"markdown":"\n# Missed Call Workflows\n\nWhen a caller hangs up before reaching a human — no answer, busy, voicemail, abandoned in IVR — automatically respond with an SMS. This pattern is the single highest-ROI play in service-industry telephony. Industries that adopt it consistently recover 40-70% of otherwise-lost leads.\n\n## What \"missed\" means in practice\n\nA \"missed\" call is any inbound call where the desired outcome (reach a live human, complete a transaction) did not happen. Six common terminal states:\n\n| State | SignalWire/LaML call status | Common cause |\n|---|---|---|\n| No answer | `no-answer` | All ring targets timed out |\n| Busy | `busy` | Destination was on another call |\n| Failed | `failed` | Carrier-level failure (e.g., bad number) |\n| Voicemail left | `completed` (call was answered by VM) | Caller hit voicemail |\n| Voicemail no-message | `completed` (short duration, no recording) | Caller hung up at VM beep |\n| IVR abandon | `completed` (short duration, no transfer) | Caller hung up in menu |\n\nEach state may warrant a different text-back message.\n\n## The basic flow\n\n```\nInbound call → SWML or LaML script → call ends\n                                       ↓\n                    Webhook fires on call.completed event\n                                       ↓\n                    Check: did caller reach a human? did they leave a message?\n                                       ↓\n                    If \"missed\" by any definition: trigger SMS within 60s\n                                       ↓\n                    SMS sent from same DID the caller dialed\n                                       ↓\n                    Reply tracked back to CRM thread (GHL conversation, HubSpot ticket, etc.)\n```\n\n## SignalWire implementation — status callback\n\nConfigure the phone number's voice URL to point at your SWML, and the **status callback URL** to fire on call termination:\n\n```python\n@app.route(\"/call-status\", methods=[\"POST\"])\ndef call_status():\n    call_status = request.form[\"CallStatus\"]\n    call_sid = request.form[\"CallSid\"]\n    caller = request.form[\"From\"]\n    called = request.form[\"To\"]\n    duration = int(request.form.get(\"CallDuration\", 0))\n    answered_by = request.form.get(\"AnsweredBy\")  # only set if AMD ran\n    \n    if is_missed_call(call_status, duration, answered_by):\n        message = compose_text_back(call_status, caller)\n        send_sms(from_=called, to=caller, body=message)\n        log_to_crm(caller, call_status, \"Text-back sent\")\n    \n    return \"OK\"\n\ndef is_missed_call(status, duration, answered_by):\n    if status in (\"no-answer\", \"busy\", \"failed\"):\n        return True\n    if status == \"completed\" and duration < 15:\n        return True  # IVR abandon or quick hangup\n    if answered_by in (\"machine_start\", \"machine_end_beep\", \"machine_end_silence\"):\n        return True  # VM picked up\n    return False\n```\n\n## Text-back message templates\n\nPersonalize by call state. Generic \"we missed you\" copy converts worse than state-specific copy.\n\n### No answer\n\n> \"Hi! This is Sarah at Acme Plumbing — we just missed your call. Reply with your address and the issue, and we'll get a tech out today. Or call us back at +1 555-1234.\"\n\n### Busy\n\n> \"Hey, sorry — we were on another call when you reached out. Reply here and we'll get back to you in 5 minutes. — Acme Plumbing\"\n\n### Voicemail no message\n\n> \"Got your call but didn't catch a message. Text me here with what you need and I'll handle it right away. — Sarah at Acme\"\n\n### IVR abandon\n\n> \"Hey — we noticed you called and hung up. Anything we can help with? Text us back and we'll skip the menu. — Acme\"\n\n### After hours\n\n> \"We're closed until 8 AM, but we got your call! Text us with the issue and we'll respond first thing. Emergency? Call +1 555-9999. — Acme\"\n\nKeep messages under 160 characters when possible — single SMS segment, fastest delivery, lowest cost.\n\n## Speed matters — under 60 seconds\n\nConversion drops sharply with response delay:\n\n| Response time | Recovery rate |\n|---|---|\n| < 60 seconds | 40-70% |\n| 1-5 minutes | 20-40% |\n| 5-30 minutes | 10-20% |\n| 30+ minutes | < 5% |\n\nThe window collapses fast because callers move on to the next vendor. Build the webhook handler to fire SMS synchronously inside the status callback request — don't queue for later.\n\n## CRM thread continuity\n\nThe SMS reply must land in the same CRM thread that tracked the call. Three common patterns:\n\n### GoHighLevel (GHL)\n\n```python\ndef send_via_ghl(from_, to, body, location_id):\n    # GHL conversations API auto-threads SMS + calls by phone number\n    requests.post(\n        f\"https://services.leadconnectorhq.com/conversations/messages\",\n        headers={\"Authorization\": f\"Bearer {GHL_TOKEN}\", \"Version\": \"2021-04-15\"},\n        json={\n            \"type\": \"SMS\",\n            \"contactId\": resolve_contact(to, location_id),\n            \"message\": body,\n            \"fromNumber\": from_,\n        },\n    )\n```\n\nGHL automatically threads the SMS into the same conversation as the inbound call.\n\n### HubSpot / Salesforce\n\nSend SMS via SignalWire, then push a Note or Activity to the CRM:\n\n```python\ndef send_and_log(from_, to, body, contact_id):\n    sw_response = send_sms(from_, to, body)\n    hubspot.engagements.create({\n        \"type\": \"SMS\",\n        \"associations\": {\"contactIds\": [contact_id]},\n        \"metadata\": {\"body\": body, \"direction\": \"outbound\"},\n    })\n```\n\n### Custom CRM / Supabase\n\n```sql\nINSERT INTO call_events (call_sid, type, direction, body, created_at)\nVALUES ($1, 'sms_textback', 'outbound', $2, now());\n```\n\nThen a dashboard view filters by phone number for unified conversation view.\n\n## Avoiding double-texts\n\nIf the same caller misses multiple calls in a short window (often happens with frustrated callers re-dialing), don't text them five times.\n\n```python\ndef should_send_textback(caller, called):\n    recent = db.query(\"\"\"\n        SELECT 1 FROM textback_log\n        WHERE caller = $1 AND called = $2\n        AND created_at > now() - interval '15 minutes'\n        LIMIT 1\n    \"\"\", caller, called)\n    return not recent\n```\n\nCool-down window of 15 minutes is a common default.\n\n## Opt-out handling\n\nEven though the caller initiated contact (which arguably implies consent for follow-up), STOP must still work:\n\n```python\n@app.route(\"/sms-inbound\", methods=[\"POST\"])\ndef sms_inbound():\n    from_ = request.form[\"From\"]\n    body = request.form[\"Body\"].strip().upper()\n    \n    if body in (\"STOP\", \"STOPALL\", \"UNSUBSCRIBE\", \"QUIT\", \"CANCEL\", \"END\"):\n        db.opt_out(from_)\n        send_sms(to=from_, body=\"You've been unsubscribed. Reply START to resume.\")\n        return Response(\"\", mimetype=\"text/xml\")\n    \n    # Normal inbound handling\n```\n\n## Conversion tracking\n\nTag each text-back so you can measure recovery rate:\n\n```sql\nCREATE TABLE textback_events (\n    id UUID PRIMARY KEY,\n    call_sid TEXT,\n    caller TEXT,\n    sent_at TIMESTAMPTZ,\n    replied_at TIMESTAMPTZ,\n    converted_at TIMESTAMPTZ,\n    revenue NUMERIC\n);\n```\n\nA \"conversion\" can be:\n\n- Reply within 24 hours → soft conversion\n- Booked appointment → mid conversion\n- Job won → hard conversion\n\nTrack all three to compute the recovery funnel.\n\n## Compliance — TCPA and CTIA\n\nInbound caller initiating contact does not by itself create marketing consent. The text-back must be **transactional** (responding to their inquiry) not **promotional** (selling a different service).\n\nTransactional, allowed:\n\n> \"We missed your call about plumbing — what's the address?\"\n\nPromotional, not allowed without prior opt-in:\n\n> \"We missed your call! Get 20% off any service today with code AUTUMN20!\"\n\nMixing the two (\"Sorry we missed you — also we have a sale!\") drifts into marketing territory. CTIA enforces this in the gray zone.\n\n## Pattern: AI receptionist + text-back hybrid\n\nCombine the AI receptionist (handles the call when possible) with text-back (recovers when AI didn't reach a human or wasn't enough).\n\nFlow:\n\n1. Caller dials, AI receptionist answers.\n2. AI qualifies, attempts transfer to live agent.\n3. If transfer fails (no agent available), AI offers to text the caller.\n4. Caller accepts → AI calls a SWAIG function that sends an SMS confirming the callback request.\n5. Webhook on call end double-checks: was an agent reached? If not, text-back fires automatically.\n\n## Pattern: missed call → voicemail-to-text\n\nFor callers who do leave a voicemail, transcribe it and text the transcript back to confirm receipt:\n\n```python\n@app.route(\"/voicemail-recorded\", methods=[\"POST\"])\ndef voicemail_recorded():\n    recording_url = request.form[\"RecordingUrl\"]\n    caller = request.form[\"From\"]\n    called = request.form[\"To\"]\n    \n    transcript = transcribe(recording_url)  # AssemblyAI, Deepgram, or SW transcription\n    summary = summarize(transcript)\n    \n    send_sms(\n        from_=called,\n        to=caller,\n        body=f\"Got your voicemail: '{summary[:120]}'. Reply here to add details or call back at any time.\",\n    )\n```\n\nThis shows the caller their message was received and gives them a low-friction way to add information.\n\n## Common pitfalls\n\n- **Texting from a different DID than they called** — confusing, recipients ignore. Always send from the dialed number.\n- **No cool-down** — caller re-dials 3 times in 2 minutes and gets 3 identical texts. Embarrassing.\n- **Treating answered-by-AMD-machine as a live answer** — caller hit the office voicemail, didn't reach a human. That's a miss.\n- **Slow webhook handler** — synchronous DB or CRM calls block the SMS send. Fire the SMS first, log/CRM in background.\n- **Missing STOP keyword respect** — caller previously opted out, but the text-back fires anyway because the opt-out wasn't checked. CTIA complaint magnet.\n\n## Related patterns\n\n- [Voicemail drop](/topic/voicemail-drop) — pre-recorded VM delivery (related but distinct: voicemail drop is outbound, missed-call is inbound recovery)\n- [Callback scheduling](/topic/callback-scheduling) — let the caller pick a time\n- [Call routing strategies](/topic/call-routing-strategies) — fewer misses if routing is better\n- [SMS best practices](/topic/sms-best-practices) — opt-out, consent, segment math\n\n## References\n\n- TCPA 47 USC §227 — telemarketing consent rules\n- CTIA Messaging Principles v1.10 — transactional vs marketing classification\n- SignalWire docs — Call Status Callback parameters\n","html":"<h1>Missed Call Workflows</h1>\n<p>When a caller hangs up before reaching a human — no answer, busy, voicemail, abandoned in IVR — automatically respond with an SMS. This pattern is the single highest-ROI play in service-industry telephony. Industries that adopt it consistently recover 40-70% of otherwise-lost leads.</p>\n<h2>What \"missed\" means in practice</h2>\n<p>A \"missed\" call is any inbound call where the desired outcome (reach a live human, complete a transaction) did not happen. Six common terminal states:</p>\n<p>| State | SignalWire/LaML call status | Common cause |\n|---|---|---|\n| No answer | <code>no-answer</code> | All ring targets timed out |\n| Busy | <code>busy</code> | Destination was on another call |\n| Failed | <code>failed</code> | Carrier-level failure (e.g., bad number) |\n| Voicemail left | <code>completed</code> (call was answered by VM) | Caller hit voicemail |\n| Voicemail no-message | <code>completed</code> (short duration, no recording) | Caller hung up at VM beep |\n| IVR abandon | <code>completed</code> (short duration, no transfer) | Caller hung up in menu |</p>\n<p>Each state may warrant a different text-back message.</p>\n<h2>The basic flow</h2>\n<pre><code>Inbound call → SWML or LaML script → call ends\n                                       ↓\n                    Webhook fires on call.completed event\n                                       ↓\n                    Check: did caller reach a human? did they leave a message?\n                                       ↓\n                    If \"missed\" by any definition: trigger SMS within 60s\n                                       ↓\n                    SMS sent from same DID the caller dialed\n                                       ↓\n                    Reply tracked back to CRM thread (GHL conversation, HubSpot ticket, etc.)\n</code></pre>\n<h2>SignalWire implementation — status callback</h2>\n<p>Configure the phone number's voice URL to point at your SWML, and the <strong>status callback URL</strong> to fire on call termination:</p>\n<pre><code class=\"language-python\">@app.route(\"/call-status\", methods=[\"POST\"])\ndef call_status():\n    call_status = request.form[\"CallStatus\"]\n    call_sid = request.form[\"CallSid\"]\n    caller = request.form[\"From\"]\n    called = request.form[\"To\"]\n    duration = int(request.form.get(\"CallDuration\", 0))\n    answered_by = request.form.get(\"AnsweredBy\")  # only set if AMD ran\n    \n    if is_missed_call(call_status, duration, answered_by):\n        message = compose_text_back(call_status, caller)\n        send_sms(from_=called, to=caller, body=message)\n        log_to_crm(caller, call_status, \"Text-back sent\")\n    \n    return \"OK\"\n\ndef is_missed_call(status, duration, answered_by):\n    if status in (\"no-answer\", \"busy\", \"failed\"):\n        return True\n    if status == \"completed\" and duration &#x3C; 15:\n        return True  # IVR abandon or quick hangup\n    if answered_by in (\"machine_start\", \"machine_end_beep\", \"machine_end_silence\"):\n        return True  # VM picked up\n    return False\n</code></pre>\n<h2>Text-back message templates</h2>\n<p>Personalize by call state. Generic \"we missed you\" copy converts worse than state-specific copy.</p>\n<h3>No answer</h3>\n<blockquote>\n<p>\"Hi! This is Sarah at Acme Plumbing — we just missed your call. Reply with your address and the issue, and we'll get a tech out today. Or call us back at +1 555-1234.\"</p>\n</blockquote>\n<h3>Busy</h3>\n<blockquote>\n<p>\"Hey, sorry — we were on another call when you reached out. Reply here and we'll get back to you in 5 minutes. — Acme Plumbing\"</p>\n</blockquote>\n<h3>Voicemail no message</h3>\n<blockquote>\n<p>\"Got your call but didn't catch a message. Text me here with what you need and I'll handle it right away. — Sarah at Acme\"</p>\n</blockquote>\n<h3>IVR abandon</h3>\n<blockquote>\n<p>\"Hey — we noticed you called and hung up. Anything we can help with? Text us back and we'll skip the menu. — Acme\"</p>\n</blockquote>\n<h3>After hours</h3>\n<blockquote>\n<p>\"We're closed until 8 AM, but we got your call! Text us with the issue and we'll respond first thing. Emergency? Call +1 555-9999. — Acme\"</p>\n</blockquote>\n<p>Keep messages under 160 characters when possible — single SMS segment, fastest delivery, lowest cost.</p>\n<h2>Speed matters — under 60 seconds</h2>\n<p>Conversion drops sharply with response delay:</p>\n<p>| Response time | Recovery rate |\n|---|---|\n| &#x3C; 60 seconds | 40-70% |\n| 1-5 minutes | 20-40% |\n| 5-30 minutes | 10-20% |\n| 30+ minutes | &#x3C; 5% |</p>\n<p>The window collapses fast because callers move on to the next vendor. Build the webhook handler to fire SMS synchronously inside the status callback request — don't queue for later.</p>\n<h2>CRM thread continuity</h2>\n<p>The SMS reply must land in the same CRM thread that tracked the call. Three common patterns:</p>\n<h3>GoHighLevel (GHL)</h3>\n<pre><code class=\"language-python\">def send_via_ghl(from_, to, body, location_id):\n    # GHL conversations API auto-threads SMS + calls by phone number\n    requests.post(\n        f\"https://services.leadconnectorhq.com/conversations/messages\",\n        headers={\"Authorization\": f\"Bearer {GHL_TOKEN}\", \"Version\": \"2021-04-15\"},\n        json={\n            \"type\": \"SMS\",\n            \"contactId\": resolve_contact(to, location_id),\n            \"message\": body,\n            \"fromNumber\": from_,\n        },\n    )\n</code></pre>\n<p>GHL automatically threads the SMS into the same conversation as the inbound call.</p>\n<h3>HubSpot / Salesforce</h3>\n<p>Send SMS via SignalWire, then push a Note or Activity to the CRM:</p>\n<pre><code class=\"language-python\">def send_and_log(from_, to, body, contact_id):\n    sw_response = send_sms(from_, to, body)\n    hubspot.engagements.create({\n        \"type\": \"SMS\",\n        \"associations\": {\"contactIds\": [contact_id]},\n        \"metadata\": {\"body\": body, \"direction\": \"outbound\"},\n    })\n</code></pre>\n<h3>Custom CRM / Supabase</h3>\n<pre><code class=\"language-sql\">INSERT INTO call_events (call_sid, type, direction, body, created_at)\nVALUES ($1, 'sms_textback', 'outbound', $2, now());\n</code></pre>\n<p>Then a dashboard view filters by phone number for unified conversation view.</p>\n<h2>Avoiding double-texts</h2>\n<p>If the same caller misses multiple calls in a short window (often happens with frustrated callers re-dialing), don't text them five times.</p>\n<pre><code class=\"language-python\">def should_send_textback(caller, called):\n    recent = db.query(\"\"\"\n        SELECT 1 FROM textback_log\n        WHERE caller = $1 AND called = $2\n        AND created_at > now() - interval '15 minutes'\n        LIMIT 1\n    \"\"\", caller, called)\n    return not recent\n</code></pre>\n<p>Cool-down window of 15 minutes is a common default.</p>\n<h2>Opt-out handling</h2>\n<p>Even though the caller initiated contact (which arguably implies consent for follow-up), STOP must still work:</p>\n<pre><code class=\"language-python\">@app.route(\"/sms-inbound\", methods=[\"POST\"])\ndef sms_inbound():\n    from_ = request.form[\"From\"]\n    body = request.form[\"Body\"].strip().upper()\n    \n    if body in (\"STOP\", \"STOPALL\", \"UNSUBSCRIBE\", \"QUIT\", \"CANCEL\", \"END\"):\n        db.opt_out(from_)\n        send_sms(to=from_, body=\"You've been unsubscribed. Reply START to resume.\")\n        return Response(\"\", mimetype=\"text/xml\")\n    \n    # Normal inbound handling\n</code></pre>\n<h2>Conversion tracking</h2>\n<p>Tag each text-back so you can measure recovery rate:</p>\n<pre><code class=\"language-sql\">CREATE TABLE textback_events (\n    id UUID PRIMARY KEY,\n    call_sid TEXT,\n    caller TEXT,\n    sent_at TIMESTAMPTZ,\n    replied_at TIMESTAMPTZ,\n    converted_at TIMESTAMPTZ,\n    revenue NUMERIC\n);\n</code></pre>\n<p>A \"conversion\" can be:</p>\n<ul>\n<li>Reply within 24 hours → soft conversion</li>\n<li>Booked appointment → mid conversion</li>\n<li>Job won → hard conversion</li>\n</ul>\n<p>Track all three to compute the recovery funnel.</p>\n<h2>Compliance — TCPA and CTIA</h2>\n<p>Inbound caller initiating contact does not by itself create marketing consent. The text-back must be <strong>transactional</strong> (responding to their inquiry) not <strong>promotional</strong> (selling a different service).</p>\n<p>Transactional, allowed:</p>\n<blockquote>\n<p>\"We missed your call about plumbing — what's the address?\"</p>\n</blockquote>\n<p>Promotional, not allowed without prior opt-in:</p>\n<blockquote>\n<p>\"We missed your call! Get 20% off any service today with code AUTUMN20!\"</p>\n</blockquote>\n<p>Mixing the two (\"Sorry we missed you — also we have a sale!\") drifts into marketing territory. CTIA enforces this in the gray zone.</p>\n<h2>Pattern: AI receptionist + text-back hybrid</h2>\n<p>Combine the AI receptionist (handles the call when possible) with text-back (recovers when AI didn't reach a human or wasn't enough).</p>\n<p>Flow:</p>\n<ol>\n<li>Caller dials, AI receptionist answers.</li>\n<li>AI qualifies, attempts transfer to live agent.</li>\n<li>If transfer fails (no agent available), AI offers to text the caller.</li>\n<li>Caller accepts → AI calls a SWAIG function that sends an SMS confirming the callback request.</li>\n<li>Webhook on call end double-checks: was an agent reached? If not, text-back fires automatically.</li>\n</ol>\n<h2>Pattern: missed call → voicemail-to-text</h2>\n<p>For callers who do leave a voicemail, transcribe it and text the transcript back to confirm receipt:</p>\n<pre><code class=\"language-python\">@app.route(\"/voicemail-recorded\", methods=[\"POST\"])\ndef voicemail_recorded():\n    recording_url = request.form[\"RecordingUrl\"]\n    caller = request.form[\"From\"]\n    called = request.form[\"To\"]\n    \n    transcript = transcribe(recording_url)  # AssemblyAI, Deepgram, or SW transcription\n    summary = summarize(transcript)\n    \n    send_sms(\n        from_=called,\n        to=caller,\n        body=f\"Got your voicemail: '{summary[:120]}'. Reply here to add details or call back at any time.\",\n    )\n</code></pre>\n<p>This shows the caller their message was received and gives them a low-friction way to add information.</p>\n<h2>Common pitfalls</h2>\n<ul>\n<li><strong>Texting from a different DID than they called</strong> — confusing, recipients ignore. Always send from the dialed number.</li>\n<li><strong>No cool-down</strong> — caller re-dials 3 times in 2 minutes and gets 3 identical texts. Embarrassing.</li>\n<li><strong>Treating answered-by-AMD-machine as a live answer</strong> — caller hit the office voicemail, didn't reach a human. That's a miss.</li>\n<li><strong>Slow webhook handler</strong> — synchronous DB or CRM calls block the SMS send. Fire the SMS first, log/CRM in background.</li>\n<li><strong>Missing STOP keyword respect</strong> — caller previously opted out, but the text-back fires anyway because the opt-out wasn't checked. CTIA complaint magnet.</li>\n</ul>\n<h2>Related patterns</h2>\n<ul>\n<li><a href=\"/topic/voicemail-drop\">Voicemail drop</a> — pre-recorded VM delivery (related but distinct: voicemail drop is outbound, missed-call is inbound recovery)</li>\n<li><a href=\"/topic/callback-scheduling\">Callback scheduling</a> — let the caller pick a time</li>\n<li><a href=\"/topic/call-routing-strategies\">Call routing strategies</a> — fewer misses if routing is better</li>\n<li><a href=\"/topic/sms-best-practices\">SMS best practices</a> — opt-out, consent, segment math</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>TCPA 47 USC §227 — telemarketing consent rules</li>\n<li>CTIA Messaging Principles v1.10 — transactional vs marketing classification</li>\n<li>SignalWire docs — Call Status Callback parameters</li>\n</ul>\n"}