Same shape as mail(): the provider and its credentials come from your
environment, hooks run around every send, and failures throw a normalized PostboiError.
The first question is where you’re sending, because unlike email the right SMS provider
depends on the destination — a UK-native provider is materially cheaper into the UK and no
use anywhere else. Your answer also becomes the default country, which is how national
numbers like 07788 223344 get resolved.
Phone numbers
Anything unambiguous works without configuration:
National formats need a country, either as a default or per send:
Give it an ISO country code ("GB") or a dialling code ("+44") — the dialling code always
works, including for countries the ISO table doesn’t list.
Numbers, and why they’re risky
A bare number reads nicely and is accepted:
But a JavaScript number cannot carry a leading + or a leading 0, so 07788 223344 becomes 7788223344 and nothing downstream can tell a UK number from a US one. We resolve
what we safely can and throw rather than guess otherwise:
A wrong guess texts a stranger, so there isn’t a silent fallback. Pass +-prefixed
strings and none of this applies.
Development sends nothing
In development, texts are captured and logged, never sent — even with a fully configured provider:
This is stricter than email, where the dev inbox only intercepts when it’s actually running. The asymmetry is deliberate: a stray email is embarrassing, a stray text costs money, reaches a real handset, and cannot be recalled.
When you genuinely need real delivery locally:
Sender
Most providers need a sender — either a number you’ve purchased, or an alphanumeric sender ID: up to 11 characters, shown to the recipient in place of a number.
In the UK alphanumeric sender IDs are free and need no registration, which makes SMS setup about as light as email. Two things to know: they are one-way — a recipient cannot reply to one, so use a purchased number for conversations — and they must look like your brand, because generic IDs get filtered.
In the US neither applies: sending needs 10DLC brand and campaign registration first, which takes weeks and is arranged with your provider, not here.
Cost, and message length
SMS is billed per segment, not per message. A GSM-7 message fits 160 characters in one segment, then 153 per segment after that. A single character outside GSM-7 — an emoji, a curly quote, an em dash — switches the whole message to UCS-2, where a segment is 70 characters:
That’s usually the difference between one segment and three, so it’s worth knowing before you paste in a “smart quote”.
Scheduling
Where a provider supports it, scheduled_at takes a Date, an ISO string or a relative duration:
Providers that can’t schedule reject the send rather than delivering immediately — a text meant for Tuesday arriving now is worse than an error, and silent.
Sending many
Pass an array. Each message gets its own result, so one failure never loses the rest:
Providers
Construct one directly instead of using the environment, exactly like an email provider:
RCS — an upgrade, not a channel
RCS is the carrier-native successor to SMS: branded sender, delivery receipts, long messages billed once instead of per segment. Since iOS 18.1 it covers both platforms, and with Twilio it needs no code at all — add an RCS-capable sender to a Messaging Service and put its SID in the environment. Twilio routes each message by device capability with automatic SMS fallback, and your sending code doesn’t change:
Constructing the provider yourself instead? Instances don’t read the environment — pass messaging_service_sid to the new Twilio({ … }) constructor.
Worth knowing before you switch it on:
- Sender registration is console-side — brand verification through your provider, with a one-time onboarding fee and a lead time of days to weeks.
- Pricing is parity-to-higher for short messages (RCS carries carrier fees too), but a message over 160 characters bills once rather than per segment — the crossover where RCS gets cheaper than cheap UK SMS is around 3 segments.
- Which rail delivered arrives on Twilio’s status callbacks, not the send response — the message is queued before the routing decision happens.
Hooks
Hooks run on every channel, so narrow on channel before reading fields that
only one of them has: