diff --git a/.changeset/curvy-wings-dress.md b/.changeset/curvy-wings-dress.md
new file mode 100644
index 00000000..580f3797
--- /dev/null
+++ b/.changeset/curvy-wings-dress.md
@@ -0,0 +1,5 @@
+---
+"@chat-adapter/twilio": minor
+---
+
+add Twilio SMS, MMS, and voice helpers with webhook, messaging, and formatting primitives
diff --git a/apps/docs/adapters.json b/apps/docs/adapters.json
index 8862a3ae..8e1779ae 100644
--- a/apps/docs/adapters.json
+++ b/apps/docs/adapters.json
@@ -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",
diff --git a/apps/docs/app/[lang]/adapters/(detail)/og-image.tsx b/apps/docs/app/[lang]/adapters/(detail)/og-image.tsx
index ec81c73c..00f9b25d 100644
--- a/apps/docs/app/[lang]/adapters/(detail)/og-image.tsx
+++ b/apps/docs/app/[lang]/adapters/(detail)/og-image.tsx
@@ -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]";
diff --git a/apps/docs/app/[lang]/adapters/components/adapter-card.tsx b/apps/docs/app/[lang]/adapters/components/adapter-card.tsx
index a11ad741..5230456c 100644
--- a/apps/docs/app/[lang]/adapters/components/adapter-card.tsx
+++ b/apps/docs/app/[lang]/adapters/components/adapter-card.tsx
@@ -21,6 +21,7 @@ import {
slack,
teams,
telegram,
+ twilio,
web,
whatsapp,
} from "@/lib/logos";
@@ -42,6 +43,7 @@ const iconMap: Record<
postgres,
memory,
whatsapp,
+ twilio,
messenger,
};
diff --git a/apps/docs/components/geistdocs/adapter-hero.tsx b/apps/docs/components/geistdocs/adapter-hero.tsx
index e8a0d8dd..1bc5fd16 100644
--- a/apps/docs/components/geistdocs/adapter-hero.tsx
+++ b/apps/docs/components/geistdocs/adapter-hero.tsx
@@ -11,6 +11,7 @@ import {
slack,
teams,
telegram,
+ twilio,
web,
whatsapp,
} from "@/lib/logos";
@@ -32,6 +33,7 @@ const ICON_MAP: Record<
postgres,
memory,
whatsapp,
+ twilio,
messenger,
};
diff --git a/apps/docs/content/adapters/official/meta.json b/apps/docs/content/adapters/official/meta.json
index ac0e7d89..246e0da6 100644
--- a/apps/docs/content/adapters/official/meta.json
+++ b/apps/docs/content/adapters/official/meta.json
@@ -11,6 +11,7 @@
"github",
"linear",
"whatsapp",
+ "twilio",
"messenger",
"web",
"---State---",
diff --git a/apps/docs/content/adapters/official/twilio.mdx b/apps/docs/content/adapters/official/twilio.mdx
new file mode 100644
index 00000000..91dfce80
--- /dev/null
+++ b/apps/docs/content/adapters/official/twilio.mdx
@@ -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
+
+
+
+## Quick start
+
+
+The adapter auto-detects `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PHONE_NUMBER`, and `TWILIO_MESSAGING_SERVICE_SID` from the environment.
+
+
+```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 {
+ return bot.webhooks.twilio(request);
+}
+```
+
+Configure your Twilio Messaging webhook URL to:
+
+```text
+https://your-domain.com/api/webhooks/twilio
+```
+
+## Configuration
+
+ string | Promise)",
+ description:
+ "Twilio Account SID. Auto-detected from `TWILIO_ACCOUNT_SID`.",
+ },
+ authToken: {
+ type: "string | (() => string | Promise)",
+ 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)",
+ 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",
+ 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 {
+ 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
+
+
diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx
index 8a997904..6961ca0b 100644
--- a/apps/docs/content/docs/index.mdx
+++ b/apps/docs/content/docs/index.mdx
@@ -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
@@ -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) |
diff --git a/apps/docs/lib/logos.tsx b/apps/docs/lib/logos.tsx
index ce04c687..5393afb6 100644
--- a/apps/docs/lib/logos.tsx
+++ b/apps/docs/lib/logos.tsx
@@ -512,6 +512,17 @@ export const whatsapp = (props: ComponentProps<"svg">) => (
);
+export const twilio = (props: ComponentProps<"svg">) => (
+
+);
+
export const messenger = (props: ComponentProps<"svg">) => (