---
title: "Twilio"
description: "Send and receive SMS and MMS through Twilio Programmable Messaging with the SMS SDK twilio() adapter: setup, senders, webhooks, errors, and limits."
---

The `twilio()` adapter sends SMS and MMS through Twilio Programmable Messaging and verifies Twilio's status and inbound webhooks. To compare providers, see [Choose a provider](/providers/overview).

## Support status

**Supported.** Requests, responses, error codes, statuses, and webhook signing follow Twilio's official documentation, and the adapter passes the shared contract tests. Limits to know:

- Scheduling (`sendAt`) works only from a Messaging Service sender.
- Content Templates (`ContentSid`) are not supported.
- Webhook verification needs the Auth Token of the account that owns the number. An API key cannot verify webhooks.
- Twilio signs no timestamp, so webhooks have no replay window. Deduplicate on `dedupeKey`.

## Setup

```package-install
npm install @opencoredev/sms-sdk
```

| Variable | Required | Used for |
| --- | --- | --- |
| `TWILIO_ACCOUNT_SID` | Yes | Account SID, `AC` plus 32 hex characters. |
| `TWILIO_AUTH_TOKEN` | Yes, unless you use an API key | API auth, and webhook verification. |
| `TWILIO_API_KEY_SID`, `TWILIO_API_KEY_SECRET` | Instead of the Auth Token | API auth with a revocable key. Twilio recommends this for production. |
| `TWILIO_FROM` or `TWILIO_MESSAGING_SERVICE_SID` | One of them | Default sender. The service SID is `MG` plus 32 hex characters. |

The adapter reads no environment variables itself; the examples and the [doctor CLI](/reference/doctor-cli) use these names. Keep the secrets on the server.

```ts
import { createSmsClient } from "@opencoredev/sms-sdk";
import { twilio } from "@opencoredev/sms-sdk/twilio";

const sms = createSmsClient({
  adapters: [
    twilio({
      accountSid: process.env.TWILIO_ACCOUNT_SID ?? "",
      authToken: process.env.TWILIO_AUTH_TOKEN ?? "",
      // Or, instead of authToken: apiKeySid and apiKeySecret.
      from: "+15550100001",
    }),
  ],
});

const result = await sms.send({ to: "+14155550123", body: "Your order has shipped." });
console.log(result.providerId); // "SM..." ("MM..." for MMS)
```

Pass either `authToken` or `apiKeySid` with `apiKeySecret`, not both. `twilio()` throws `ConfigurationError` at startup when the Account SID is malformed or a credential is empty. Two more options exist for tests and proxies: `baseUrl` (default `https://api.twilio.com`) and `fetch`. To test without a Twilio account, see [Test without a provider](/testing/local-testing).

The adapter calls `POST /2010-04-01/Accounts/{AccountSid}/Messages.json` with a form body. A 2xx without a valid `SM` or `MM` SID is an unknown outcome.

## Senders

| Sender | Write | Sent as |
| --- | --- | --- |
| Long code or toll-free number | `"+15550100001"` | `From` |
| Short code | `{ shortCode: "12345" }` | `From` |
| Alphanumeric sender ID | `{ senderId: "Acme" }` | `From` |
| Messaging Service | `{ messagingService: "MG…" }` | `MessagingServiceSid` |

Which senders work depends on the destination country, and a sender Twilio refuses is a `sender` rejection. In the US, long codes need A2P 10DLC registration, toll-free numbers need verification before they reach the US or Canada, and alphanumeric sender IDs are not supported. Twilio's [SMS guidelines](https://www.twilio.com/en-us/guidelines/sms) list the rules for each country. SMS SDK does not check them before sending; see [Registration and consent](/providers/registration-and-consent).

## Features

| Feature | Twilio |
| --- | --- |
| MMS | Up to 10 `mediaUrls` |
| Scheduling (`sendAt`) | Only with a `{ messagingService }` sender, inside Twilio's scheduling window |
| Validity period | 1 to 36000 seconds |
| Per-message `webhookUrl` | Yes, sent as `StatusCallback` |
| Body length | Up to 1600 characters, checked before sending |

## Provider options

Pass other Twilio Message parameters in `providerOptions.twilio`:

```ts
import { sms } from "./sms";

await sms.send({
  to: "+14155550123",
  body: "Track your order: https://example.com/orders/123",
  providerOptions: { twilio: { smartEncoded: true, messageIntent: "delivery" } },
});
```

| Option | Twilio parameter | Type |
| --- | --- | --- |
| `applicationSid` | `ApplicationSid` | `string`, `AP` plus 32 hex characters |
| `provideFeedback` | `ProvideFeedback` | `boolean` |
| `attempt` | `Attempt` | integer, 1 or more |
| `contentRetention` | `ContentRetention` | `"retain" \| "discard"` |
| `addressRetention` | `AddressRetention` | `"retain" \| "obfuscate"` |
| `smartEncoded` | `SmartEncoded` | `boolean` |
| `shortenUrls` | `ShortenUrls` | `boolean`. Needs a `{ messagingService }` sender, or the send throws `UnsupportedFieldError`. |
| `sendAsMms` | `SendAsMms` | `boolean` |
| `messageIntent` | `MessageIntent` | `"otp" \| "notifications" \| "marketing" \| "fraud" \| "security" \| "customercare" \| "delivery" \| "education" \| "polling" \| "announcements" \| "events"` |
| `riskCheck` | `RiskCheck` | `"enable" \| "disable"` |
| `extra` | any other parameter | `Record<string, string \| number \| boolean>`, sent as is |

Unknown keys and wrong types throw before any request, and the options go out only when the `twilio` adapter sends. `extra` cannot set the fields SMS SDK owns: `To`, `From`, `MessagingServiceSid`, `Body`, `MediaUrl`, `StatusCallback`, `ValidityPeriod`, `SendAt`, `ScheduleType`, `ContentSid`, and `ContentVariables`. `MaxPrice`, `ForceDelivery`, and `TrafficType` have no typed option; send them through `extra`. See [`providerOptions`](/reference/send#provideroptions) for how options interact with fallback and idempotency.

## Webhooks

```ts
import { parseSmsWebhook, WebhookSignatureError } from "@opencoredev/sms-sdk/webhooks";
import { db } from "./db";

export async function POST(request: Request): Promise<Response> {
  try {
    const event = await parseSmsWebhook({
      provider: "twilio",
      request,
      publicUrl: "https://example.com/webhooks/sms/twilio",
      credentials: { authToken: process.env.TWILIO_AUTH_TOKEN ?? "" },
    });
    if (event.type === "message.delivered" || event.type === "message.undelivered") {
      await db.messages.updateStatus(event.providerId, event.type, event.errorCode);
    } else if (event.type === "recipient.opted_out") {
      await db.suppressions.add(event.from);
    }
    // Empty TwiML: no automatic reply.
    return new Response("<Response></Response>", { headers: { "content-type": "text/xml" } });
  } catch (error) {
    if (error instanceof WebhookSignatureError) return new Response("Invalid signature", { status: 401 });
    throw error;
  }
}
```

- **Credential:** the Auth Token of the account that owns the number. A subaccount's number is signed with the subaccount's token. API keys cannot verify.
- **Point Twilio at it:** for status, pass `webhookUrl` per message or set a status callback on the Messaging Service. For inbound, set "A message comes in" on the number, or the incoming message webhook on the Messaging Service, with HTTP POST.
- **`publicUrl`:** pass the exact URL configured in Twilio, or verification fails behind a proxy. SMS SDK also tries it with and without `:443` or `:80`. See [Webhook security](/receiving/webhook-security).
- **Signing:** `X-Twilio-Signature` is an HMAC-SHA1 over the URL and the sorted form parameters. For JSON bodies, Twilio signs only the URL and adds a `bodySHA256` query parameter, which SMS SDK checks against the raw body.
- **Replay window:** none, because Twilio signs no timestamp. Deduplicate on `dedupeKey`.

| Twilio status | Event |
| --- | --- |
| `queued`, `accepted`, `scheduled`, `sending` | `message.queued` |
| `sent` | `message.sent` |
| `delivered` | `message.delivered` |
| `undelivered`, `failed` | `message.undelivered`, or `message.filtered` with error 30007 |
| `read`, `canceled`, others | `unrecognized` |
| Inbound message | `message.received` |
| Inbound with `OptOutType` `STOP`, `START`, `HELP` | `recipient.opted_out`, `recipient.opted_in`, `recipient.help` (`source: "provider"`) |

`OptOutType` appears when Advanced Opt-Out is on for the Messaging Service. `dedupeKey` is `twilio:{MessageSid}:{status}` for status updates and `twilio:{MessageSid}:received` for inbound messages.

## Limitations

- Scheduling needs a Messaging Service sender. To schedule from a plain number, use Telnyx.
- Content Templates are not supported, because a template replaces `body`, which SMS SDK needs for segment estimates, policy checks, and idempotency. WhatsApp and Conversations are out of scope.
- Twilio enforces its own scheduling window and MMS country coverage, and rejects sends outside them.

## Error codes

| Twilio response | Category | Falls back |
| --- | --- | --- |
| 401, or error 20003 | `auth` | Yes |
| 403 with an unlisted code | `auth` | Yes |
| Error 20429 | `rate_limited` | Yes, after retries |
| 21212, 21606, 21612, 21659, 21660, 21703 | `sender` | Yes |
| 21408 (geographic permission), 21608 (trial account) | `account` | Yes |
| 21211, 21614 | `recipient` | No |
| 21610 (recipient unsubscribed) | `compliance` | Never |
| Any other 4xx | `request` | No |
| 429 without error 20429 | unknown | Never |
| 5xx, network error, timeout, 2xx without a valid SID | unknown | Never |

Twilio documents that a 429 with error 20429 was not processed. A 429 without that code may come from a proxy, so SMS SDK treats it as unknown. `error.provider.code` holds Twilio's error code and `error.provider.requestId` the `Twilio-Request-Id` header. For adapter authors, the subpath also exports `buildTwilioForm`, `interpretTwilioResponse`, `twilioRejectionCategory`, and `twilioDelivery`.

## Official documentation

Checked on 2026-10-08. The full evidence list is in [PROVIDERS.md](https://github.com/opencoredev/sms-sdk/blob/main/packages/sms-sdk/PROVIDERS.md).

- [Message resource](https://www.twilio.com/docs/messaging/api/message-resource)
- [Error dictionary](https://www.twilio.com/docs/api/errors)
- [Webhook request validation](https://www.twilio.com/docs/usage/security#validating-requests)
- [Incoming message webhook parameters](https://www.twilio.com/docs/messaging/guides/webhook-request)
- [Tracking outbound message status](https://www.twilio.com/docs/messaging/guides/track-outbound-message-status)
- [Messaging Services](https://www.twilio.com/docs/messaging/services)

## Next steps

- [Migrate from the Twilio SDK](/migration/migrate-from-twilio)
- [Receive webhooks](/receiving/webhooks)
- [Switch providers](/migration/switch-providers)
