---
title: "Twilio"
description: "Use Twilio for MainSDK SMS notifications and phone checks with Twilio Verify - account setup, main.yaml, secrets, testing and swapping."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/twilio"
updated: "2026-10-28"
---

# Twilio

> Use Twilio for MainSDK SMS notifications and phone checks with Twilio Verify - account setup, main.yaml, secrets, testing and swapping.

Canonical: https://mainsdk.dev/docs/integrations/twilio · Updated: 2026-10-28

Twilio carries two jobs in a MainSDK app. As an SMS driver for [`notify`](https://mainsdk.dev/docs/interfaces/notify), it delivers text messages when SMS is the channel a person is reached on. Through Twilio Verify, it backs [`verify`](https://mainsdk.dev/docs/interfaces/verify), which confirms that a phone number belongs to the person who gave it. Pick Twilio when you send SMS in many countries from one account; for India-first SMS, MSG91 is the other spec driver.

## Interfaces it serves

### Phone, email and identity checks (`verify`)

- Status: Building for 0.7 · ships as a beta API
- Driver: `twilio-verify` (Twilio Verify, default)
- Docs: https://mainsdk.dev/docs/interfaces/verify.md

```yaml
verify:
  driver: twilio-verify
```

Swap to another provider: `bird`, `local`, `digilocker`, `onfido`, `persona`.

### Notifications (`notify`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `twilio` (Twilio)
- Docs: https://mainsdk.dev/docs/interfaces/notify.md

```yaml
notify:
  driver: twilio
```

Swap to another provider: `whatsapp-cloud`, `bird`, `msg91`, `local`, `fcm`, `apns`, `expo`, `resend`, `ses`, `onesignal`, `slack`, `pagerduty`, `opsgenie`, `webhook`.

## Before you start

You need a Twilio account. Twilio gives every account an Account SID and an auth token, and lets you create API keys for apps.

1. **Get credentials.** In the Twilio Console, note your Account SID. Create an API key for this app rather than sharing the account's auth token, so you can revoke it on its own.
2. **For SMS:** buy a phone number or set up a messaging service that can send SMS to the countries you serve. Some countries need a registered sender or registered templates before messages are delivered; in India, SMS must use DLT-registered templates.
3. **For phone checks:** create a Verify service. Twilio Verify generates, sends and checks the codes, and gives the service its own ID.
4. **Use separate credentials per environment.** Twilio test credentials let you exercise the API without sending real messages.

Store every credential in your secrets manager, never in code or a committed env file.

## Configure

SMS for `notify`:

```yaml title="main.yaml"
notify:
  channels: [whatsapp, sms, email]
  sms: twilio
  twilio:
    account: ACxxxxxxxxxxxxxxxx            # the Account SID
    user: SKxxxxxxxxxxxxxxxx               # an API key SID; leave out to use the auth token
    key: secret:TWILIO_API_SECRET          # the API key's secret, or the auth token without `user`
    from: "+14155550100"                   # a Twilio number; or messaging-service: MGxxxxxxxx
  templates:
    order.shipped:
      sms: { text: "Pinecart: order {{orderId}} shipped" }
```

- `account` is required. Set `from` or `messaging-service`; with neither the app fails to start with `MAIN-NOTIFY-0001`.
- `key` defaults to `secret:TWILIO_AUTH_TOKEN`. A literal key fails validation with `MAIN-CONFIG-0004`.
- Twilio sends the template's `sms.text` with its `{{name}}` values filled from `data`. A notification with no `sms` template is skipped on SMS with the reason `no-template`.

> **Warning:** The Twilio request shape is written from Twilio's documentation and is unverified against the live API. Verify with a sandbox key before production: use Twilio's test credentials and a Twilio test number first.

### What the driver sends

Each message is one form-encoded `POST https://api.twilio.com/2010-04-01/Accounts/<account>/Messages.json` with HTTP basic authentication (`user` and `key`, or `account` and the auth token). The form holds `To`, `Body`, and `From` or `MessagingServiceSid`. The id Twilio returns (`sid`) becomes the delivery's `providerId`. The call goes through MainSDK's network layer, is never retried by it, does not follow redirects, and reads at most 64 KiB of the answer. A 429, a 5xx or no answer is a retryable `MAIN-NOTIFY-0007` and `notify` tries the next channel; the error holds neither the number nor Twilio's reply. MainSDK does not read Twilio's delivery status callbacks.

Phone checks:

```yaml title="main.yaml"
verify:
  driver: twilio-verify
  key: secret:VERIFY_KEY
  drivers:
    twilio-verify:
      account: secret:TWILIO_ACCOUNT_SID
      token: secret:TWILIO_AUTH_TOKEN
      service: VAxxxxxxxxxxxxxxxx    # the Verify service ID
```

MainSDK makes the six-digit code, its expiry and its tries itself and gives Twilio the code as a custom code, so your Verify service needs custom codes enabled (ask Twilio support). Each send is one form-encoded `POST https://verify.twilio.com/v2/Services/<service>/Verifications` with `To`, `Channel` (`sms`, `whatsapp` or `call`) and `CustomCode`. A 429, a 5xx or no answer is a retryable `MAIN-VERIFY-0003`; a 4xx is not retryable and leaves the answer the same as for any other number.

> **Note:** The Verify request follows Twilio's public docs and is tested against recorded replies, not a live service. Send one real code from a Twilio test service before launch.

## Use it

Notify a person; Twilio delivers when SMS is the channel chosen. The person needs a recorded SMS opt-in.

```ts title="app/orders/ship.ts"
import { notify } from "mainsdk";

await notify(user, {
  template: "order.shipped",
  data: { orderId: order.id },
  category: "orders",
  idempotencyKey: `order.shipped:${order.id}`,
});
```

Confirm a phone number a signed-in user gave you:

```ts title="app/account/phone/actions.ts"
"use server";
import { auth, db } from "mainsdk";
import { verify } from "mainsdk/verify";

export async function sendCode(req: Request, phone: string) {
  const session = await auth.require(req);
  return (await verify.phone({ number: phone, for: session.user, req })).id;
}

export async function confirmPhone(req: Request, id: string, code: string) {
  const session = await auth.require(req);
  const check = await verify.phone.check({ id, code });
  if (check.ok) {
    await db.users.for(session.user).update(session.user, { phoneVerified: true });
  }
  return check.ok;
}
```

`verify.phone()` sends the code and answers `{ sent: true, id, expiresAt }`; `verify.phone.check()` answers `{ ok, number }`.

## Local development and testing

- **No Twilio account:** `mainsdk dev` runs the local terminal channel for `notify`, so messages show in your terminal.
- **Twilio test credentials:** use them to try the real API without sending messages or being charged for them.
- **Tests:** `createTestApp()` from `@mainsdk/testing` runs `notify` on a local channel when you set `notify.sms: local`; read it with `channels(app).sms`. It enforces opt-ins, consent and quiet hours. `channels(app).sms?.failNext(1)` simulates a provider outage, and `fakeMessagingServer("twilio")` records the request the real driver makes. For `verify`, set `verify.driver: local` and read the codes from `app.verifyKit().outbox`; `runVerifyConformance` checks the `twilio-verify` driver against recorded replies.

## What MainSDK adds on top

- Every SMS recipient needs a recorded opt-in; without one, SMS is skipped with `MAIN-NOTIFY-0003` and the next channel is tried.
- Opt-outs and quiet hours are respected on every send, in the person's time zone.
- A retry with the same idempotency key never sends twice.
- India SMS uses DLT-registered templates. Twilio takes only `sms.text`, so set `sms.dlt-template-id` for a `+91` number or the SMS is skipped with `MAIN-NOTIFY-0005`.

See [Notifications](https://mainsdk.dev/docs/interfaces/notify) and [Phone, email and identity checks](https://mainsdk.dev/docs/interfaces/verify).

## Limits and notes

- The `notify:twilio` SMS driver is built (spec `email-notify`). `verify:twilio-verify` is built (spec `verify`) and declares its own host, `verify.twilio.com`.
- The driver declares its own host, `api.twilio.com`, so `network.allow` does not change.
- Set `notify.limits.sms-per-day` to bound cost; more is skipped with `MAIN-NOTIFY-0010`.
- Twilio's delivery rules differ by destination country; check the countries you serve, and register senders or templates where required.
- Phone numbers are personal data. Label the fields that store them `personal` in [`db`](https://mainsdk.dev/docs/interfaces/db).
- This driver covers SMS only. The WhatsApp channel drivers in the spec are WhatsApp Cloud API, Bird and MSG91.

## Swap to or from it

```yaml title="main.yaml"
notify:
  sms: msg91   # was twilio
```

```yaml title="main.yaml"
verify:
  driver: bird   # was twilio-verify
```

Sending SMS is stateless, so the swap needs no migration. Phone checks are one-off calls; verified flags you keep live in your own `db`. Recorded opt-ins are not held by Twilio; MainSDK keeps them in your app's `kv` store. See [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver).

## FAQ

**Can I use Twilio for SMS outside India and MSG91 inside it?** Yes. Set `sms: twilio` in `main.yaml` and `sms: msg91` in the `main.in.yaml` market overlay.

**Is Twilio Verify the same as WhatsApp sign-in?** No. WhatsApp one-time codes for sign-in belong to [`auth`](https://mainsdk.dev/docs/interfaces/auth). `verify` confirms a number a signed-in user gives you.

**Why was SMS skipped for a user?** They have no recorded SMS opt-in (`MAIN-NOTIFY-0003`), opted out of the category, or are in quiet hours. The delivery records the reason.

## Next

- [Notifications](https://mainsdk.dev/docs/interfaces/notify) and [Phone, email and identity checks](https://mainsdk.dev/docs/interfaces/verify)
- [MSG91](https://mainsdk.dev/docs/integrations/msg91), [Bird](https://mainsdk.dev/docs/integrations/bird) and [WhatsApp](https://mainsdk.dev/docs/integrations/whatsapp)
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)
