Skip to main content
Channel plugin · listing tai42/channel-twilio

Install

Permissions

Provides

twilio

Channel — Deliver ask questions to a phone over Twilio SMS/WhatsApp and bridge the reply back. Bridge a WhatsApp number carried by Twilio to an agent.

Enable it

manifest.yml

Configuration

The CHANNEL_TWILIO_ env group. CHANNEL_TWILIO_REDIS_URL (and the inherited connection-tuning fields) point the plugin-owned correlation store.

Point the webhooks at the deployment

Twilio calls two webhooks (configured in the Twilio console or REST — the plugin never mutates your Twilio account):
  • Inbound message (HTTP POST) → {public base URL}/api/channels/twilio/inbound
  • Delivery status callback (HTTP POST) → {public base URL}/api/channels/twilio/status
Configure the inbound webhook URL with #rp=5xx (and #rc= of at least 1) so a message refused while the server reloads is delivered again: Twilio retries only on the failures its retry policy names, and a reload answers a retriable 503. The status callback surfaces a late delivery outcome. For a bridge message Twilio accepts the send, then posts a failed/undelivered status, which flips the answer record to failed (this is how the WhatsApp 24-hour service window is surfaced on the async path); a delivered status confirms it. For a notify_user send (which runs no bridge record) the status is resolved to that send and posted as a delivery_receipt event onto its monitoring trace — failed as an ERROR. A status neither owns is acknowledged and ignored.

Inbound media

A message that carries media (NumMedia > 0) bridges as a turn per media item. Each item’s bytes are fetched from Twilio (MediaUrl{N}, HTTP Basic auth, following the vendor’s redirect to its CDN) and ingested into the platform’s served media store: the turn gets a typed attachment whose url is /api/interactions/media/<id>, plus the parity media_* entry-params (media_kind, media_id, media_mime_type, media_sha256, media_size, and media_filename for a document) carrying the served id. The MediaContentType{N} major type resolves the kind (image/audio/video, application/text → document, otherwise file). The Body caption rides the first item’s text; an item with no caption gets a [kind] placeholder (a document reads [document: <name>] from the platform-sanitised filename), so a media message with a blank body is never dropped as empty. Entry params are bounded at the door (at most 16 keys, each value at most 512 characters, the whole set at most 2048 bytes). Because Twilio redelivers a failed webhook, this door drops rather than refuses: an over-cap value is dropped on its own, an over-bound set is dropped whole, and the participant’s message still bridges. Every drop is logged at WARNING (naming the door and the bound, never a value) and counted on tai_entry_params_dropped_total{door="twilio"}. When an item cannot be received, the participant is notified once and the message is acknowledged, never silently dropped: a gone media, an unfetchable byte source, or no configured store reads as could not receive; a file over the platform cap as too large; a disallowed content type as unsupported. A caption on a rejected first item is still bridged as its own text turn. A transient fault (a 5xx or timeout from Twilio or its CDN, a torn read) instead returns a non-2xx webhook so Twilio redelivers; the MessageSid dedupe keeps the redelivery from double-processing. A message with no body and no media stays the legitimate acknowledged drop.

Create a bridge route

our_identity is the number the client texts — the Twilio WhatsApp sender, whatsapp:-prefixed:
In Studio, pick twilio as the route’s Channel; the Our identity field then shows this hint. A (channel, identity) pair may be claimed by only one route; more senders are more rows.

Reply window

WhatsApp delivers a freeform reply only inside a 24-hour window opened by the client’s last message. For a whatsapp: pair the channel tracks that window and reports it on every bridged turn as turn.reply_window ({state, closes_at}), so a route’s start_expr, reply_expr or tool can see it before replying:
  • each new inbound message on the pair opens the window for 24 hours from the moment the webhook first receives it; a redelivered MessageSid keeps that first receipt, so a retry never moves the window;
  • state is open (until closes_at, UTC, in whole seconds) or closed (never opened, or expired);
  • an SMS pair has no such window and reads none.
The window lives in the channel’s Redis (CHANNEL_TWILIO_REDIS_URL), one key per pair that expires when the window closes, plus one first-receipt key per WhatsApp MessageSid that expires after 24 hours; there is no setting. The state is read once per turn, so a turn that runs past closes_at may still see open; Twilio’s 63016 rejection of an out-of-window send stays the final check.

Message ids and addresses

The conversation bridge records every message this channel sends to a participant. These are the ids and addresses it records:
  • An inbound message passes its MessageSid as the medium message id. A message with several media items is bridged as one record per item, and every record carries the same MessageSid.
  • resolve_address returns the “From” number (the route’s identity, else CHANNEL_TWILIO_FROM) and the “To” number, both as the inbound webhook passes them, a whatsapp: prefix included. A completed send reports the same pair and the message’s sid.

Replies

On WhatsApp, a message sent as a reply to a message from the last 7 days carries OriginalRepliedMessageSid, the MessageSid of the message it quotes. The channel hands that sid to the platform beside the message, on every media item of the message too, and the platform resolves it against the conversation’s own records into a typed reply_to ({message_id, status, text}): a tool target reads it in its payload, an agent target through start_expr. When the reply answers a pending question, the asker receives the same reply_to beside the unchanged answer. An SMS has no reply field, so it quotes nothing, and the channel sends no tappable buttons, so no tap arrives.

Working-on-it signal

SMS and WhatsApp-over-Twilio carry no typing indicator, so this channel emits no working-on-it signal while a turn runs. The answer is delivered when it is ready.

See also