Twilio
Use Twilio for MainSDK SMS notifications and phone checks with Twilio Verify - account setup, main.yaml, secrets, testing and swapping.
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.
verifyBuilding for 0.7Driver twilio-verify Twilio VerifyDefault
verify:
driver: twilio-verifySwap to another provider
notifyBuilding for 0.3Driver twilio Twilio
notify:
driver: twilioBefore 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.
- 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.
- 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.
- For phone checks: create a Verify service. Twilio Verify generates, sends and checks the codes, and gives the service its own ID.
- 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:
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" }accountis required. Setfromormessaging-service; with neither the app fails to start withMAIN-NOTIFY-0001.keydefaults tosecret:TWILIO_AUTH_TOKEN. A literal key fails validation withMAIN-CONFIG-0004.- Twilio sends the template's
sms.textwith its{{name}}values filled fromdata. A notification with nosmstemplate is skipped on SMS with the reasonno-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:
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 IDMainSDK 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.
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:
"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 devruns the local terminal channel fornotify, 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/testingrunsnotifyon a local channel when you setnotify.sms: local; read it withchannels(app).sms. It enforces opt-ins, consent and quiet hours.channels(app).sms?.failNext(1)simulates a provider outage, andfakeMessagingServer("twilio")records the request the real driver makes. Forverify, setverify.driver: localand read the codes fromapp.verifyKit().outbox;runVerifyConformancechecks thetwilio-verifydriver against recorded replies.
What MainSDK adds on top
- Every SMS recipient needs a recorded opt-in; without one, SMS is skipped with
MAIN-NOTIFY-0003and 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 setsms.dlt-template-idfor a+91number or the SMS is skipped withMAIN-NOTIFY-0005.
See Notifications and Phone, email and identity checks.
Limits and notes
- The
notify:twilioSMS driver is built (specemail-notify).verify:twilio-verifyis built (specverify) and declares its own host,verify.twilio.com. - The driver declares its own host,
api.twilio.com, sonetwork.allowdoes not change. - Set
notify.limits.sms-per-dayto bound cost; more is skipped withMAIN-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
personalindb. - This driver covers SMS only. The WhatsApp channel drivers in the spec are WhatsApp Cloud API, Bird and MSG91.
Swap to or from it
notify:
sms: msg91 # was twilioverify:
driver: bird # was twilio-verifySending 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?