# Missed-call text-back (reference build)

When a customer calls a trades business and nobody answers, this texts the caller "Sorry we missed your call. What do you need help with?" and forwards their reply to the owner's cell. It runs on Twilio (phone number, voice, SMS) plus three small Vercel serverless functions. It has no dependencies and is about 150 lines of code.

> **Status: unit-tested and simulated locally. Not yet run against a live Twilio account.** See [What's tested](#whats-tested).

## How it works

```
Customer calls the Twilio number
  └─ /api/voice        → rings the owner's cell for RING_SECONDS (default 20). The caller ID shown is the customer's number.
       └─ /api/dial-status
            ├─ answered        → hang up, do nothing
            └─ no-answer / busy / failed / canceled
                  ├─ caller hears: "Sorry, <business> can't take your call right now. We'll text you in a moment."
                  └─ sends the text-back SMS to the caller (skipped for anonymous or non-US numbers)
Customer replies by SMS
  └─ /api/sms
       ├─ STOP / HELP / START → no reply from us (Twilio's built-in opt-out handling replies and blocks)
       └─ anything else       → forwarded to the owner: "<business> lead from +1303…: <message>"
```

Every request is checked against Twilio's `X-Twilio-Signature`, and forged requests get a 403.

## Setup (about 1 hour of work, plus days to weeks waiting on 10DLC approval)

1. **Twilio account.** Upgrade from trial, because trial accounts can only text verified numbers. Buy a **local** number in the business's area code.
2. **A2P 10DLC registration.** This is required before any SMS will reliably deliver to US phones. Start it on day one, because approval is the slow part.
   - For a business with an EIN: register a **Low-Volume Standard** brand and a campaign. For a sole proprietor without an EIN, use **Sole Proprietor**.
   - Suggested use case: "Customer Care" or "Low-Volume Mixed." Sample message: the text-back message below.
   - Opt-in description: *"Customers who call our business line and are not answered receive one text asking how we can help. Replies are answered by our staff. Reply STOP to opt out."*
   - **You need a public privacy policy and terms page** on the business's website that cover SMS. Campaigns without them get rejected. The privacy policy must say mobile numbers aren't shared with third parties for marketing.
   - Add the number to the campaign's Messaging Service.
3. **Deploy the functions.** Create a separate Vercel project with **Root Directory = `builds/missed-call-textback`**. Add these environment variables from `.env.example`: `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `OWNER_PHONE`, and `BUSINESS_NAME`. `RING_SECONDS` and `TEXTBACK_MESSAGE` are optional.
   - Turn off Vercel Deployment Protection for this project, because Twilio can't log in.
4. **Point the number at it.** In the Twilio Console, under Phone Numbers → your number:
   - Voice, "A call comes in": Webhook, `https://<your-app>.vercel.app/api/voice`, HTTP POST
   - Messaging, "A message comes in": `https://<your-app>.vercel.app/api/sms`, HTTP POST. If the number is in a Messaging Service, set it on the service instead.
5. **Route the business's calls to the Twilio number.** Either publish it as the business number (Google Business Profile, truck wraps), or set **conditional call forwarding on no-answer** from the owner's existing carrier line to the Twilio number. With conditional forwarding, set `MODE=forwarded` so `/api/voice` texts right away instead of ringing the owner again. Carriers usually pass the original caller's number on forwarded calls, but check this with the owner's carrier (*not verified*).
6. **Test live:** call from a phone you own and don't answer. Confirm you get the text, reply, and confirm the owner gets the forward. Then text STOP and confirm nothing more arrives.

Run the tests locally with `npm test` (requires Node 18+).

## Real monthly cost (Twilio, US)

Prices were checked on Twilio's pricing pages on 2026-10-14. Carrier pass-through fees are extra and change often.

| Item | Price | Source |
|---|---|---|
| Local number | **$1.15 / mo** | [twilio.com/voice/pricing/us](https://www.twilio.com/en-us/voice/pricing/us) |
| Inbound call | $0.0085 / min | same |
| Outbound leg ringing or connecting to owner's cell | $0.0140 / min | same |
| SMS, outbound or inbound (long code) | **$0.0083 / segment** | [twilio.com/sms/pricing/us](https://www.twilio.com/en-us/sms/pricing/us) |
| Failed message | $0.001 each | same |
| Carrier fees per SMS segment | extra, *not verified* (typically a fraction of a cent) | same page, "carrier fees" section |
| 10DLC brand registration | $4.50 one-time for Low-Volume Standard or Standard. The marketing page says $4 and the docs say $4.50. | [Twilio brand registration docs](https://www.twilio.com/docs/trust-hub/registrations/a2p-10dlc-brand) |
| Standard Brand vetting (only if Standard, not Low-Volume) | $41.50 one-time | same |
| Campaign vetting | **$15 one-time** | [twilio.com/phone-numbers/a2p-10dlc](https://www.twilio.com/en-us/phone-numbers/a2p-10dlc) |
| Campaign monthly | **$1.50/mo** for Low-Volume Mixed, $2/mo for Sole Prop, $10/mo for Standard use cases | same |

**Example:** a shop with 300 inbound calls a month, of which 40 are missed and 25 reply.
- Number: $1.15
- 10DLC campaign: $1.50
- SMS: 40 text-backs plus 25 inbound replies plus 25 forwards = 90 segments × $0.0083 ≈ $0.75
- Voice: 300 calls × about 2.5 minutes, counting both legs at about $0.0225/min ≈ $17
- **Total: about $20/month plus carrier fees, and about $20 one-time for 10DLC.**
- Voice dominates the cost because every call routes through Twilio. With conditional forwarding (only missed calls hit Twilio), voice drops to about $1, for a total of **about $5/month**.
- These are estimates from list prices, not a real invoice.

### Alternatives

| Provider | Number | SMS (long code) | Voice | Source |
|---|---|---|---|---|
| Telnyx | from $1.00/mo | $0.004 per part each way, plus carrier fee | not checked | [telnyx.com/pricing/messaging](https://telnyx.com/pricing/messaging), [numbers](https://telnyx.com/pricing/numbers) |
| SignalWire | $0.50/mo local | *not verified* | $0.0066/min in, $0.008/min out | [signalwire.com/pricing](https://signalwire.com/pricing/messaging) |

Both still require 10DLC registration, which is a US carrier rule and not specific to Twilio. SignalWire supports TwiML-compatible ("LaML") webhooks, so the code should port with small changes (*not tested*). Telnyx uses a different webhook format (TeXML for voice, JSON for messaging), so you'd need to rewrite the adapters. The core logic in `lib.js` stays the same.

**Before you build, check the owner's existing tools.** Many phone apps and field-service tools (OpenPhone/Quo, some VoIP providers, Jobber and Housecall Pro add-ons) advertise missed-call text-back built in (*not verified per plan*). If they already pay for one, turning it on beats running this.

## Gotchas

- **10DLC is the real gate.** Unregistered traffic from a 10DLC number gets blocked or filtered, plus extra carrier fees.
  - Approval takes days for sole proprietors and "up to several weeks" for standard campaigns, per Twilio. Rejections are common for missing privacy or terms pages, vague opt-in descriptions, or a sample message that doesn't match the real one.
  - If a campaign is rejected, edit and resubmit it instead of creating a new one, which costs a second vetting fee.
- **Carrier filtering.** Even when registered, links (especially URL shorteners), all caps, and "FREE!!!" language get filtered. Keep the text-back plain. Error 30007 means the message was filtered, and 30034 means the number isn't registered.
- **Opt-out (STOP).** Twilio's Advanced Opt-Out is on by default for Messaging Services. It replies to STOP, blocks further sends (error 21610), and handles HELP and START.
  - This code deliberately doesn't reply to those words, so customers don't get two replies.
  - If you turn off Twilio's opt-out handling, you must keep your own opt-out list. This build doesn't keep one.
- **TCPA.** A single text responding to a call the customer just placed is generally treated as an expected, informational reply. Marketing follow-ups are not, and need prior express written consent.
  - Don't reuse these numbers for promos or reactivation without consent.
  - Quiet hours, state "mini-TCPA" laws (e.g. Florida, Oklahoma), and FCC rules still apply.
  - *This is not legal advice. Have the client's counsel review the opt-in wording.*
- **Repeat callers get repeat texts.** There's no memory, so someone who calls three times gets three texts. The fix is a small store (e.g. Vercel KV or Upstash) keyed by number with a 24-hour cooldown. That isn't built here, to stay dependency-free.
- **Ring time vs. voicemail.** If the owner's cell sends the call to its own voicemail before `RING_SECONDS`, Twilio sees the call as **answered** by voicemail and no text goes out. Set `RING_SECONDS` lower than the cell's voicemail pickup (often around 25 seconds), or turn off cell voicemail.
- **Landlines can't receive texts.** The text-back to a landline caller fails quietly ($0.001 failed-message fee). You could add Twilio Lookup to skip them, but it costs extra per lookup and isn't built here.

## What it can't do

- It doesn't hold a conversation, qualify the lead, or book jobs. It forwards one-way, and the owner calls or texts back from their own phone. That second step is where an LLM layer would go.
- It doesn't record voicemail or transcribe calls.
- It doesn't write to Jobber, Housecall Pro, or ServiceTitan.
- It has no dashboard or logs beyond Vercel's function logs.
- It doesn't de-duplicate repeat callers or keep its own opt-out list (see Gotchas).
- It doesn't do after-hours rules (e.g. "we'll reply at 7am").

## What's tested

| Tested (locally, `npm test`, 23 passing) | Not tested |
|---|---|
| Twilio signature validation, checked against **Twilio's own documented example** | Any live call or SMS through Twilio |
| Missed-call detection for no-answer, busy, failed, canceled, and completed | 10DLC registration or approval |
| Anonymous and non-US callers skipped | Real carrier delivery or filtering |
| STOP, HELP, and START words not forwarded; "can't stop it" still forwarded | Vercel deployment of these functions |
| XML escaping (business names with `&`, injection attempts) | Behavior with real cell voicemail timing |
| SMS send request format (URL, basic auth, body) with a mocked `fetch` | SignalWire or Telnyx ports |
| `MODE=forwarded`: immediate text, no dial |
| **Full flow simulation:** real handlers, signed requests, call → no answer → text-back → reply forwarded to owner; forged request rejected | Twilio's Advanced Opt-Out behavior (relied on, not exercised) |
