Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curvy-wings-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chat-adapter/twilio": minor
---

add Twilio SMS, MMS, and voice helpers with webhook, messaging, and formatting primitives
10 changes: 10 additions & 0 deletions apps/docs/adapters.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@
"beta": true,
"readme": "https://github.com/vercel/chat/tree/main/packages/adapter-whatsapp"
},
{
"name": "Twilio",
"slug": "twilio",
"type": "platform",
"icon": "twilio",
"description": "Build SMS and MMS bots with Twilio Messaging webhooks and the Messages API.",
"packageName": "@chat-adapter/twilio",
"beta": true,
"readme": "https://github.com/vercel/chat/tree/main/packages/adapter-twilio"
},
{
"name": "Messenger",
"slug": "messenger",
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/app/[lang]/adapters/(detail)/og-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ const ADAPTER_LOGOS: Record<
width: LOGO_SIZE,
height: LOGO_SIZE,
},
twilio: {
component: logos.twilio,
width: LOGO_SIZE,
height: LOGO_SIZE,
},
};

const FONTS_DIR = "app/[lang]/og/[...slug]";
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/app/[lang]/adapters/components/adapter-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
slack,
teams,
telegram,
twilio,
web,
whatsapp,
} from "@/lib/logos";
Expand All @@ -42,6 +43,7 @@ const iconMap: Record<
postgres,
memory,
whatsapp,
twilio,
messenger,
};

Expand Down
2 changes: 2 additions & 0 deletions apps/docs/components/geistdocs/adapter-hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
slack,
teams,
telegram,
twilio,
web,
whatsapp,
} from "@/lib/logos";
Expand All @@ -32,6 +33,7 @@ const ICON_MAP: Record<
postgres,
memory,
whatsapp,
twilio,
messenger,
};

Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/adapters/official/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"github",
"linear",
"whatsapp",
"twilio",
"messenger",
"web",
"---State---",
Expand Down
240 changes: 240 additions & 0 deletions apps/docs/content/adapters/official/twilio.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
---
title: Twilio
description: Twilio SMS and MMS adapter for Chat SDK.
packageName: "@chat-adapter/twilio"
slug: twilio
type: platform
logo: twilio
tagline: Build SMS and MMS 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: Plain text fallback
buttons: no
linkButtons: no
selectMenus: no
tables:
status: partial
label: ASCII
fields: yes
imagesInCards: no
modals: no
slashCommands: no
mentions: no
addReactions: no
removeReactions: no
typingIndicator: 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`.",
},
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.",
},
}}
/>

## 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",
});
```

## 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 plain text fallback. Buttons and select menus are not interactive over SMS.
- `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.

## Feature support

<FeatureSupport />
2 changes: 2 additions & 0 deletions apps/docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Each adapter factory auto-detects credentials from environment variables (`SLACK
| GitHub | `@chat-adapter/github` | Yes | Yes | No | No | Buffered | No |
| Linear | `@chat-adapter/linear` | Yes | Yes | No | No | Agent sessions / Post+Edit | No |
| WhatsApp | `@chat-adapter/whatsapp` | N/A | Yes | Partial | No | Buffered | Yes |
| Twilio | `@chat-adapter/twilio` | N/A | No | Fallback | No | Buffered | Yes |
| Messenger | `@chat-adapter/messenger` | Yes | Receive-only | Partial | No | Buffered | Yes |

## AI coding agent support
Expand Down Expand Up @@ -87,6 +88,7 @@ The SDK is distributed as a set of packages you install based on your needs:
| `@chat-adapter/github` | GitHub Issues adapter |
| `@chat-adapter/linear` | Linear Issues adapter |
| `@chat-adapter/whatsapp` | WhatsApp Business adapter |
| `@chat-adapter/twilio` | Twilio SMS and MMS adapter |
| `@chat-adapter/messenger` | Facebook Messenger adapter |
| `@chat-adapter/state-redis` | Redis state adapter (production) |
| `@chat-adapter/state-ioredis` | ioredis state adapter (alternative) |
Expand Down
11 changes: 11 additions & 0 deletions apps/docs/lib/logos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,17 @@ export const whatsapp = (props: ComponentProps<"svg">) => (
</svg>
);

export const twilio = (props: ComponentProps<"svg">) => (
<svg
fill="#F22F46"
viewBox="0 0 30 30"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M15 0C6.7 0 0 6.7 0 15s6.7 15 15 15 15-6.7 15-15S23.3 0 15 0zm0 26C8.9 26 4 21.1 4 15S8.9 4 15 4s11 4.9 11 11-4.9 11-11 11zm6.8-14.7c0 1.7-1.4 3.1-3.1 3.1s-3.1-1.4-3.1-3.1 1.4-3.1 3.1-3.1 3.1 1.4 3.1 3.1zm0 7.4c0 1.7-1.4 3.1-3.1 3.1s-3.1-1.4-3.1-3.1c0-1.7 1.4-3.1 3.1-3.1s3.1 1.4 3.1 3.1zm-7.4 0c0 1.7-1.4 3.1-3.1 3.1s-3.1-1.4-3.1-3.1c0-1.7 1.4-3.1 3.1-3.1s3.1 1.4 3.1 3.1zm0-7.4c0 1.7-1.4 3.1-3.1 3.1S8.2 13 8.2 11.3s1.4-3.1 3.1-3.1 3.1 1.4 3.1 3.1z" />
</svg>
);

export const messenger = (props: ComponentProps<"svg">) => (
<svg
fill="none"
Expand Down
Loading