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 WhatsAppConnect:

  • 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:

  1. Twilio POSTs /api/whatsapp/inbound with the message body and signature.
  2. Coachbot verifies the signature using the workspace's Twilio Auth Token (looked up by the sender phone → most-recently-active lead).
  3. The message gets inserted into messages table.
  4. An Inngest event agent/turn.requested fires.
  5. The agent-turn Inngest 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).