WhatsApp setup
Twilio configuration, inbound webhook routing, and the 24-hour business-messaging window.
WhatsApp workspaces send and receive messages via your own Twilio account. Every workspace brings its own credentials — there is no platform-wide Twilio fallback.
1. Connect Twilio
Settings → Integrations → Twilio WhatsApp → Connect:
- Account SID (
AC…) — Twilio Console dashboard, top right. - Auth Token — same place.
- From number — must include the
whatsapp:prefix, e.g.whatsapp:+447xxxxxxxxx. From Messaging → Senders → WhatsApp senders.
Hit Save, then Verify. A green "Verified" message means Twilio accepted the SID + Auth Token combination.
2. Configure Twilio's inbound webhook
Inbound replies have to reach Coachbot's /api/whatsapp/inbound endpoint. Where you set this depends on whether your WhatsApp sender is part of a Messaging Service:
If the sender is attached to a Messaging Service
Twilio Console → Messaging → Services → <your service> → Integration:
- Webhook URL for incoming messages:
https://<your-host>/api/whatsapp/inbound - Method: HTTP POST
The Messaging Service's URL takes precedence over the sender-level URL.
If the sender is standalone
Twilio Console → Messaging → Senders → WhatsApp senders → <your number> → Messaging Endpoint Configuration:
- Webhook URL for incoming messages:
https://<your-host>/api/whatsapp/inbound - Method: HTTP POST
3. Check PUBLIC_BASE_URL
Coachbot validates Twilio's webhook signature using PUBLIC_BASE_URL. The value must match the host Twilio is calling exactly — scheme + host, no trailing slash. On Vercel: project → Settings → Environment Variables. After changes, redeploy — env changes don't take effect on existing deployments.
The 24-hour window gotcha
Meta restricts business-initiated WhatsApp messages:
A business can send freeform WhatsApp messages within 24 hours of the user's last inbound message. Outside that window, the business may only send pre-approved Message Templates.
This affects the form-triggered first message. The lead has never messaged you, so the window is closed, so Twilio returns error code 63016 and the lead's phone never rings. The agent's reply silently fails.
Two workarounds
A) Click-to-chat (shipped, no setup needed). Coachbot's public form success screen shows a "Continue on WhatsApp" button that opens wa.me on the lead's phone with a pre-filled Hi, I just signed up — <Name> message. They tap Send, which opens the 24-hour window, and the agent replies freeform from then on. Built into Coachbot's /forms/<slug> already.
B) Message Templates (custom build). Get a template approved by Meta, then Coachbot would send it instead of freeform for the initial reply. Not currently implemented.
We default to (A) because it's reliable and ships today. If you ever want (B), it's a per-workspace template SID stored on Twilio's integration row plus an outbound branch in lib/agent.ts.
How the agent processes a WhatsApp reply
When an inbound message arrives:
- Twilio POSTs
/api/whatsapp/inboundwith the message body and signature. - Coachbot verifies the signature using the workspace's Twilio Auth Token (looked up by the sender phone → most-recently-active lead).
- The message gets inserted into
messagestable. - An Inngest event
agent/turn.requestedfires. - The
agent-turnInngest function:- generate-reply — loads the full conversation history, builds the system prompt, calls OpenAI with tools.
- send-message — sends the reply via the workspace's Twilio credentials.
- persist-message — writes the assistant message + updates lead status.
Per-lead concurrency = 1 so two rapid inbound messages serialize cleanly against a fresh history. Global concurrency cap = 5 to fit the free Inngest plan.
Lead transcript page
/admin/<slug>/leads/<id> shows the full message thread, polling every 5 seconds while visible. Operator-side actions:
- Take over — sets
lead.status = "needs_human". Agent stops auto-replying. Operator can type in the draft input + send manually; messages go via the same Twilio credentials but stamped with the operator's name. - Release — reverts status to
in_conversation. Agent resumes on the next inbound.
Manual replies are WhatsApp-only — the take-over flow doesn't work for voice (calls have already ended by then).