mirror of
https://github.com/vercel/chat.git
synced 2026-09-14 18:32:29 +08:00
75cadbf9aa
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>
316 lines
11 KiB
Plaintext
316 lines
11 KiB
Plaintext
---
|
||
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 />
|