Missed Call Workflows
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.
What "missed" means in practice
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:
| State | SignalWire/LaML call status | Common cause |
|---|---|---|
| No answer | no-answer | All ring targets timed out |
| Busy | busy | Destination was on another call |
| Failed | failed | Carrier-level failure (e.g., bad number) |
| Voicemail left | completed (call was answered by VM) | Caller hit voicemail |
| Voicemail no-message | completed (short duration, no recording) | Caller hung up at VM beep |
| IVR abandon | completed (short duration, no transfer) | Caller hung up in menu |
Each state may warrant a different text-back message.
The basic flow
Inbound call → SWML or LaML script → call ends
↓
Webhook fires on call.completed event
↓
Check: did caller reach a human? did they leave a message?
↓
If "missed" by any definition: trigger SMS within 60s
↓
SMS sent from same DID the caller dialed
↓
Reply tracked back to CRM thread (GHL conversation, HubSpot ticket, etc.)
SignalWire implementation — status callback
Configure the phone number's voice URL to point at your SWML, and the status callback URL to fire on call termination:
@app.route("/call-status", methods=["POST"])
def call_status():
call_status = request.form["CallStatus"]
call_sid = request.form["CallSid"]
caller = request.form["From"]
called = request.form["To"]
duration = int(request.form.get("CallDuration", 0))
answered_by = request.form.get("AnsweredBy") # only set if AMD ran
if is_missed_call(call_status, duration, answered_by):
message = compose_text_back(call_status, caller)
send_sms(from_=called, to=caller, body=message)
log_to_crm(caller, call_status, "Text-back sent")
return "OK"
def is_missed_call(status, duration, answered_by):
if status in ("no-answer", "busy", "failed"):
return True
if status == "completed" and duration < 15:
return True # IVR abandon or quick hangup
if answered_by in ("machine_start", "machine_end_beep", "machine_end_silence"):
return True # VM picked up
return False
Text-back message templates
Personalize by call state. Generic "we missed you" copy converts worse than state-specific copy.
No answer
"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."
Busy
"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"
Voicemail no message
"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"
IVR abandon
"Hey — we noticed you called and hung up. Anything we can help with? Text us back and we'll skip the menu. — Acme"
After hours
"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"
Keep messages under 160 characters when possible — single SMS segment, fastest delivery, lowest cost.
Speed matters — under 60 seconds
Conversion drops sharply with response delay:
| Response time | Recovery rate | |---|---| | < 60 seconds | 40-70% | | 1-5 minutes | 20-40% | | 5-30 minutes | 10-20% | | 30+ minutes | < 5% |
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.
CRM thread continuity
The SMS reply must land in the same CRM thread that tracked the call. Three common patterns:
GoHighLevel (GHL)
def send_via_ghl(from_, to, body, location_id):
# GHL conversations API auto-threads SMS + calls by phone number
requests.post(
f"https://services.leadconnectorhq.com/conversations/messages",
headers={"Authorization": f"Bearer {GHL_TOKEN}", "Version": "2021-04-15"},
json={
"type": "SMS",
"contactId": resolve_contact(to, location_id),
"message": body,
"fromNumber": from_,
},
)
GHL automatically threads the SMS into the same conversation as the inbound call.
HubSpot / Salesforce
Send SMS via SignalWire, then push a Note or Activity to the CRM:
def send_and_log(from_, to, body, contact_id):
sw_response = send_sms(from_, to, body)
hubspot.engagements.create({
"type": "SMS",
"associations": {"contactIds": [contact_id]},
"metadata": {"body": body, "direction": "outbound"},
})
Custom CRM / Supabase
INSERT INTO call_events (call_sid, type, direction, body, created_at)
VALUES ($1, 'sms_textback', 'outbound', $2, now());
Then a dashboard view filters by phone number for unified conversation view.
Avoiding double-texts
If the same caller misses multiple calls in a short window (often happens with frustrated callers re-dialing), don't text them five times.
def should_send_textback(caller, called):
recent = db.query("""
SELECT 1 FROM textback_log
WHERE caller = $1 AND called = $2
AND created_at > now() - interval '15 minutes'
LIMIT 1
""", caller, called)
return not recent
Cool-down window of 15 minutes is a common default.
Opt-out handling
Even though the caller initiated contact (which arguably implies consent for follow-up), STOP must still work:
@app.route("/sms-inbound", methods=["POST"])
def sms_inbound():
from_ = request.form["From"]
body = request.form["Body"].strip().upper()
if body in ("STOP", "STOPALL", "UNSUBSCRIBE", "QUIT", "CANCEL", "END"):
db.opt_out(from_)
send_sms(to=from_, body="You've been unsubscribed. Reply START to resume.")
return Response("", mimetype="text/xml")
# Normal inbound handling
Conversion tracking
Tag each text-back so you can measure recovery rate:
CREATE TABLE textback_events (
id UUID PRIMARY KEY,
call_sid TEXT,
caller TEXT,
sent_at TIMESTAMPTZ,
replied_at TIMESTAMPTZ,
converted_at TIMESTAMPTZ,
revenue NUMERIC
);
A "conversion" can be:
- Reply within 24 hours → soft conversion
- Booked appointment → mid conversion
- Job won → hard conversion
Track all three to compute the recovery funnel.
Compliance — TCPA and CTIA
Inbound 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).
Transactional, allowed:
"We missed your call about plumbing — what's the address?"
Promotional, not allowed without prior opt-in:
"We missed your call! Get 20% off any service today with code AUTUMN20!"
Mixing the two ("Sorry we missed you — also we have a sale!") drifts into marketing territory. CTIA enforces this in the gray zone.
Pattern: AI receptionist + text-back hybrid
Combine the AI receptionist (handles the call when possible) with text-back (recovers when AI didn't reach a human or wasn't enough).
Flow:
- Caller dials, AI receptionist answers.
- AI qualifies, attempts transfer to live agent.
- If transfer fails (no agent available), AI offers to text the caller.
- Caller accepts → AI calls a SWAIG function that sends an SMS confirming the callback request.
- Webhook on call end double-checks: was an agent reached? If not, text-back fires automatically.
Pattern: missed call → voicemail-to-text
For callers who do leave a voicemail, transcribe it and text the transcript back to confirm receipt:
@app.route("/voicemail-recorded", methods=["POST"])
def voicemail_recorded():
recording_url = request.form["RecordingUrl"]
caller = request.form["From"]
called = request.form["To"]
transcript = transcribe(recording_url) # AssemblyAI, Deepgram, or SW transcription
summary = summarize(transcript)
send_sms(
from_=called,
to=caller,
body=f"Got your voicemail: '{summary[:120]}'. Reply here to add details or call back at any time.",
)
This shows the caller their message was received and gives them a low-friction way to add information.
Common pitfalls
- Texting from a different DID than they called — confusing, recipients ignore. Always send from the dialed number.
- No cool-down — caller re-dials 3 times in 2 minutes and gets 3 identical texts. Embarrassing.
- Treating answered-by-AMD-machine as a live answer — caller hit the office voicemail, didn't reach a human. That's a miss.
- Slow webhook handler — synchronous DB or CRM calls block the SMS send. Fire the SMS first, log/CRM in background.
- Missing STOP keyword respect — caller previously opted out, but the text-back fires anyway because the opt-out wasn't checked. CTIA complaint magnet.
Related patterns
- Voicemail drop — pre-recorded VM delivery (related but distinct: voicemail drop is outbound, missed-call is inbound recovery)
- Callback scheduling — let the caller pick a time
- Call routing strategies — fewer misses if routing is better
- SMS best practices — opt-out, consent, segment math
References
- TCPA 47 USC §227 — telemarketing consent rules
- CTIA Messaging Principles v1.10 — transactional vs marketing classification
- SignalWire docs — Call Status Callback parameters