Twilio
Send SMS via Twilio Messages API
The Twilio transport is provided by @betternotify/twilio. It calls the Twilio Messages API via fetch with HTTP Basic Auth — no Twilio SDK required (Node 22+ / Bun built-in).
It is a thin delivery adapter. Better-Notify handles rendering and validation upstream; twilioSmsTransport() posts the rendered SMS to Twilio's Messages endpoint.
Install
npm install @betternotify/twilioGetting credentials
- Sign up at twilio.com
- Copy your Account SID and Auth Token from the Twilio Console
- Buy a phone number or create a Messaging Service
Usage
import { twilioSmsTransport } from '@betternotify/twilio';
const transport = twilioSmsTransport({
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
fromNumber: process.env.TWILIO_FROM_NUMBER!,
});Pass it to createClient via transportsByChannel:
import { createNotify, createClient } from '@betternotify/core';
import { smsChannel } from '@betternotify/sms';
import { twilioSmsTransport } from '@betternotify/twilio';
import { z } from 'zod';
const sms = smsChannel();
const rpc = createNotify({ channels: { sms } });
const catalog = rpc.catalog({
otpCode: rpc
.sms()
.input(z.object({ code: z.string().length(6) }))
.body(({ input }) => `Your code is ${input.code}. Expires in 10 minutes.`),
});
const notify = createClient({
catalog,
transportsByChannel: {
sms: twilioSmsTransport({
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
fromNumber: process.env.TWILIO_FROM_NUMBER!,
}),
},
});
await notify.otpCode.send({
to: '+15555555555',
input: { code: '482910' },
});Standalone usage
The transport can be used directly without the full pipeline:
import { twilioSmsTransport } from '@betternotify/twilio';
const transport = twilioSmsTransport({
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
fromNumber: '+15551234567',
});
const result = await transport.send(
{ body: 'Your order has shipped!', to: '+15559876543' },
{ route: 'manual', messageId: 'msg-1', attempt: 1 },
);
if (!result.ok) {
throw result.error;
}
console.log(result.data.messageId); // Twilio Message SIDOptions
Prop
Type
Messaging Service
For number pooling, local number selection, or compliance use cases, pass a Messaging Service SID instead of a phone number:
const transport = twilioSmsTransport({
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID!,
});When both fromNumber and messagingServiceSid are provided, messagingServiceSid takes priority (matching Twilio's own behavior).
Twilio uses the same Messages API for WhatsApp. Prefix the fromNumber and the to address with whatsapp::
const transport = twilioSmsTransport({
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
fromNumber: 'whatsapp:+15551234567',
});
await notify.orderShipped.send({
to: 'whatsapp:+15559876543',
input: { orderId: 'ORD-9281' },
});No code changes are needed — the transport sends whatever To and From values it receives, and Twilio routes to WhatsApp when it sees the prefix. You will need a WhatsApp-enabled sender configured in your Twilio account.
Verifying credentials
Call verify() at startup to confirm the Account SID and Auth Token are valid:
const transport = twilioSmsTransport({ ... });
const { ok, details } = await transport.verify!();
if (!ok) {
throw new Error('Invalid Twilio credentials');
}
console.log(details); // { friendlyName, status }Error handling
Twilio API errors are normalized to NotifyRpcError with mapped error codes:
| Twilio error code | Code | Meaning |
|---|---|---|
| 20003, 20005, 20006, 20008 | CONFIG | Authentication or account issue |
| 21211, 21612, 21610, 21614, 21217, 21219 | VALIDATION | Invalid number, unsubscribed, or bad input |
| 14107, 20429, 63018 (or HTTP 429) | RATE_LIMITED | Rate limit exceeded |
| HTTP 401, 403 | CONFIG | Credentials rejected |
| Others | PROVIDER | Server or delivery issue |
import { NotifyRpcError } from '@betternotify/core';
const result = await notify.otpCode.send({ to: '+15555555555', input: { code: '123456' } });
if (!result.ok) {
const err = result.error as NotifyRpcError;
if (err.code === 'VALIDATION') {
console.error('Invalid phone number:', err.message);
}
}