Files
OSS Polar Bear 75cadbf9aa feat(twilio): add RCS support for interactive inbound and rich outbound (#590)
Extend the Twilio adapter with RCS webhook parsing, Content API
integration, and card-to-template mapping with SMS fallback.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 19:57:44 +10:00

316 lines
11 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Twilio
description: Twilio SMS, MMS, and RCS adapter for Chat SDK.
packageName: "@chat-adapter/twilio"
slug: twilio
type: platform
logo: twilio
tagline: Build SMS, MMS, and RCS bots with Twilio Messaging webhooks and the Messages API.
beta: true
features:
postMessage: yes
editMessage: no
deleteMessage: yes
fileUploads:
status: partial
label: Public media URLs
streaming:
status: partial
label: Buffered
scheduledMessages: no
cardFormat:
status: partial
label: RCS rich cards + SMS text fallback
buttons:
status: partial
label: RCS quick-replies
linkButtons:
status: partial
label: RCS call-to-action
selectMenus: no
tables:
status: partial
label: ASCII
fields: yes
imagesInCards:
status: partial
label: RCS only
modals: no
slashCommands: no
mentions: no
addReactions: no
removeReactions: no
typingIndicator: no
messageUpdatedEvents: no
messageDeletedEvents: no
directMessages: yes
ephemeralMessages: no
customApiEndpoint: yes
fetchMessages:
status: partial
label: Messages API
fetchSingleMessage: yes
fetchThreadInfo: yes
fetchChannelMessages: no
listThreads: no
fetchChannelInfo: no
postChannelMessage: no
---
## Install
<PackageInstall package="@chat-adapter/twilio" />
## Quick start
<Callout type="info">
The adapter auto-detects `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PHONE_NUMBER`, and `TWILIO_MESSAGING_SERVICE_SID` from the environment.
</Callout>
```typescript title="lib/bot.ts" lineNumbers
import { createTwilioAdapter } from "@chat-adapter/twilio";
import { Chat } from "chat";
const bot = new Chat({
userName: "mybot",
adapters: {
twilio: createTwilioAdapter(),
},
});
bot.onDirectMessage(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});
```
```typescript title="app/api/webhooks/twilio/route.ts" lineNumbers
import { bot } from "@/lib/bot";
export async function POST(request: Request): Promise<Response> {
return bot.webhooks.twilio(request);
}
```
Configure your Twilio Messaging webhook URL to:
```text
https://your-domain.com/api/webhooks/twilio
```
## Configuration
<TypeTable
type={{
accountSid: {
type: "string | (() => string | Promise<string>)",
description:
"Twilio Account SID. Auto-detected from `TWILIO_ACCOUNT_SID`.",
},
authToken: {
type: "string | (() => string | Promise<string>)",
description:
"Twilio Auth Token for API calls and webhook verification. Auto-detected from `TWILIO_AUTH_TOKEN`.",
},
phoneNumber: {
type: "string",
description:
"Default sender phone number for `openDM`. Auto-detected from `TWILIO_PHONE_NUMBER`.",
},
messagingServiceSid: {
type: "string",
description:
"Default Messaging Service SID for `openDM`. Auto-detected from `TWILIO_MESSAGING_SERVICE_SID`.",
},
rcsSenderId: {
type: "string",
description:
"Direct RCS sender address (e.g. `rcs:MyBrand`) for `openDM` when targeting RCS. Auto-detected from `TWILIO_RCS_SENDER_ID`.",
},
contentApiUrl: {
type: "string",
description:
"Override the Twilio Content API base URL. Defaults to `apiUrl` when that is set, otherwise `https://content.twilio.com`.",
},
webhookUrl: {
type: "string | ((request: Request) => string | Promise<string>)",
description:
"Public webhook URL to use for Twilio signature validation when the runtime request URL differs from the URL configured in Twilio.",
},
webhookVerifier: {
type: "(request: Request, body: string) => boolean | string | Promise<boolean | string>",
description:
"Custom verifier for runtimes that terminate or transform Twilio requests before they reach the adapter.",
},
statusCallbackUrl: {
type: "string",
description: "Optional status callback URL for outbound messages.",
},
apiUrl: {
type: "string",
description:
"Override the Twilio API base URL and trusted origin for media downloads.",
},
}}
/>
## Authentication
1. Create or open a Twilio account.
2. Copy the **Account SID** to `TWILIO_ACCOUNT_SID`.
3. Copy the **Auth Token** to `TWILIO_AUTH_TOKEN`.
4. Copy a sender phone number to `TWILIO_PHONE_NUMBER`, or copy a Messaging Service SID to `TWILIO_MESSAGING_SERVICE_SID`.
## Webhooks
Twilio sends Messaging webhooks as form-encoded requests and signs them with the `X-Twilio-Signature` header. The adapter validates the exact public URL plus the submitted form parameters before dispatching an inbound message.
If your framework rewrites the request URL before it reaches the adapter, pass `webhookUrl` with the public URL configured in Twilio:
```typescript title="lib/bot.ts" lineNumbers
createTwilioAdapter({
webhookUrl: "https://your-domain.com/api/webhooks/twilio",
});
```
## RCS setup
<Callout type="info">
RCS uses the same webhook URL and adapter as SMS/MMS — no separate endpoint is needed.
</Callout>
To enable RCS rich messaging:
1. **Register an RCS Sender** in the Twilio Console under Messaging → RCS Senders. Carrier approval typically takes 4–6 weeks.
2. **Add the RCS Sender and an SMS phone number** to a Messaging Service so Twilio can auto-fallback to SMS when RCS is unavailable.
3. **Set `TWILIO_MESSAGING_SERVICE_SID`** to the Messaging Service SID (starts with `MG`).
4. Point the Messaging Service webhook to the same URL as your SMS webhook — the adapter handles both channels.
When an RCS-capable sender is detected (`MG…` Messaging Service or `rcs:` address), the adapter automatically:
- Sends cards as Twilio Content API templates (rich cards with buttons) over RCS
- Includes an SMS text fallback in every template so non-RCS recipients get a usable message
- Routes inbound taps of buttons rendered by Chat SDK to `onAction` handlers
Inbound RCS messages are keyed to the Messaging Service (or configured RCS sender), so replies go back out over RCS. Plain SMS threads keep their phone-number thread ids even when the number belongs to a Messaging Service, and `openDM` prefers `phoneNumber` over `messagingServiceSid` and `rcsSenderId`, so enabling RCS does not change the thread ids of existing conversations.
Button taps from templates that Chat SDK did not send (for example Studio flows or pre-created WhatsApp templates) are not turned into actions: when they carry a message body they arrive as regular messages, matching the adapter's behavior before RCS support.
### Handling button taps
```typescript title="lib/bot.ts" lineNumbers
bot.onAction(async (action) => {
if (action.actionId === "approve") {
await action.thread.post(`Approved: ${action.value}`);
}
});
```
### Sending rich cards
```typescript title="send-card.ts" lineNumbers
import { Actions, Button, Card, CardText } from "chat";
await thread.post({
card: Card({
title: "Deploy v1.2.3",
children: [
CardText("Ready to deploy to production?"),
Actions([
Button({ id: "approve", label: "Approve", value: "v1.2.3" }),
Button({ id: "reject", label: "Reject" }),
]),
],
}),
});
```
Over RCS, this renders as a rich card with tappable buttons. Over SMS, it falls back to plain text.
## Media
Inbound MMS media is exposed as message attachments. Twilio media URLs are not treated as public files, so each attachment includes `fetchData()` and `fetchMetadata` for authenticated downloads and queue rehydration.
Outbound media supports attachments that already have a public `url`. Chat SDK cannot upload arbitrary binary files to Twilio for you because the Messages API expects each `MediaUrl` to be reachable by Twilio.
```typescript title="send-photo.ts" lineNumbers
await thread.post({
markdown: "photo attached",
attachments: [
{
type: "image",
url: "https://example.com/photo.jpg",
},
],
});
```
## Advanced
### Messaging services
When a thread sender starts with `MG`, outbound messages use `MessagingServiceSid` instead of `From`:
```typescript title="send-with-service.ts" lineNumbers
const threadId = twilio.encodeThreadId({
sender: "MGXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
recipient: "+15555550100",
});
await bot.adapters.twilio.postMessage(threadId, "hello");
```
### Low-level helpers
The package includes runtime-light subpaths for apps that only need Twilio primitives:
```typescript title="twilio-primitives.ts" lineNumbers
import { sendTwilioMessage } from "@chat-adapter/twilio/api";
import { truncateTwilioText } from "@chat-adapter/twilio/format";
import { gatherSpeechTwilioResponse } from "@chat-adapter/twilio/voice";
import { readTwilioWebhook } from "@chat-adapter/twilio/webhook";
```
These subpaths do not import the full Chat SDK adapter or the `twilio` npm package.
### Voice helpers
Twilio voice calls are exposed as low-level primitives, not routed through the SMS/MMS adapter. Use them when your app owns the voice route and wants reusable TwiML or call-update helpers:
```typescript title="app/api/webhooks/twilio/voice/route.ts" lineNumbers
import {
gatherSpeechTwilioResponse,
parseTwilioVoiceCall,
} from "@chat-adapter/twilio/voice";
import { verifyTwilioRequest } from "@chat-adapter/twilio/webhook";
export async function POST(request: Request): Promise<Response> {
const verified = await verifyTwilioRequest(request);
const call = parseTwilioVoiceCall(verified.params);
if (!call) {
return new Response("Invalid voice webhook", { status: 400 });
}
return gatherSpeechTwilioResponse({
actionUrl: "https://your-domain.com/api/webhooks/twilio/voice/result",
prompt: "How can I help?",
});
}
```
Custom voice routes should verify the Twilio signature and apply your own caller allow-list before returning TwiML.
For live calls, `updateTwilioCall()` in `@chat-adapter/twilio/api` can post replacement TwiML or redirect the call to another URL.
### Notes
- Twilio does not support message edits, reactions, modals, or typing indicators for SMS.
- Cards render as rich RCS content when the sender is a Messaging Service (`MG…`) or RCS address; otherwise they fall back to plain text.
- RCS read receipts (`EventType=READ`) are parsed and logged but not surfaced to Chat SDK handlers (no delivery API exists today).
- `fetchMessages` uses the Messages API and is best for phone-number based threads. Messaging Service history can be less precise because inbound webhooks identify the receiving phone number, not only the Messaging Service SID.
- Content templates are created on demand and reused by a stable name derived from the card's content, so identical cards share one template across restarts. Cards that embed changing values (timestamps, order ids, user names) produce a new template per unique body, and Twilio keeps Content resources until you delete them. Keep card bodies stable, or pre-create templates, for high-volume use.
## Feature support
<FeatureSupport />