MainSDK
Search⌘K
Integrations

Twilio

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

2 interfaces2 driversWritten guide
Updated 2026-10-28

Twilio carries two jobs in a MainSDK app. As an SMS driver for notify, it delivers text messages when SMS is the channel a person is reached on. Through Twilio Verify, it backs 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

Pick Twilio in main.yaml for any of these. Your calls stay the same, and you can swap later.

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:

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.

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:

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.

Use it

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

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:

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 and Phone, email and identity checks.

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.
  • This driver covers SMS only. The WhatsApp channel drivers in the spec are WhatsApp Cloud API, Bird and MSG91.

Swap to or from it

main.yaml
notify:
  sms: msg91   # was twilio
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.

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. 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

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet