Channel plugin · listing tai42/channel-twilioInstall
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
TheCHANNEL_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
#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:
(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 awhatsapp: 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
MessageSidkeeps that first receipt, so a retry never moves the window; stateisopen(untilcloses_at, UTC, in whole seconds) orclosed(never opened, or expired);- an SMS pair has no such window and reads
none.
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
MessageSidas the medium message id. A message with several media items is bridged as one record per item, and every record carries the sameMessageSid. resolve_addressreturns the “From” number (the route’s identity, elseCHANNEL_TWILIO_FROM) and the “To” number, both as the inbound webhook passes them, awhatsapp:prefix included. A completed send reports the same pair and the message’ssid.
Replies
On WhatsApp, a message sent as a reply to a message from the last 7 days carriesOriginalRepliedMessageSid, 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
- Client conversations — the bridge model and delivery outcomes.
- Conversation bridge — every setting and the authorization model.

