THE CHANNELS
Every way your app talks to people.
Five channels, one shape. Each resolves its provider from your environment, runs the same hooks, and throws the same normalised errors. Learn one channel and you already know the other four.
PATCH THE CALL INTO ANY LANE · THE CODE NEVER CHANGES
01 The one that started it
Hosted sending with a single token, or 40+ providers of your own behind the same mail(). FormData becomes tidy HTML, delivery events come back normalised, and the dev inbox catches everything you send locally.
- Hosted forms, scheduling, broadcasts, suppressions and webhooks included
- Swap providers with one line of config, keep every call site
- A local dev inbox instead of a nervous test send to yourself
- Postboi
- Resend
- Postmark
- SES
- Mailgun
- 16 more
mail()
import { mail } from "postboi"
await mail({
to: "ada@example.com",
subject: "Your order shipped",
body: "<p>Track it any time.</p>"
})02 For the message that has to land now
SMS
Numbers are normalised to E.164 and never guessed: anything ambiguous throws instead of texting a stranger. Segments are counted before your provider bills them, so the surprise em dash that doubles a bill isn't a surprise.
- UK-native and global providers, with honest prices shown at init
- GSM-7 and UCS-2 segment maths on every send
- Scheduling that rejects rather than silently sending now
- The SMS Works
- Twilio
- Amazon SNS
sms()
import { sms } from "postboi"
// National formats resolve via your default country
await sms({ to: "07788 223344", message: "Your code is 4291" })
// Schedule where the provider supports it
await sms({
to: "+44 7788 223344",
message: "Your table is ready in 15 minutes",
scheduled_at: { hours: 2 }
})03 Templates first, by design
WhatsApp only allows free-form text within 24 hours of the user's last reply. Most transactional messages happen outside that window, so templates are the front door here, not a fallback buried in provider options.
- Twilio or Meta's Cloud API, one call either way
- whatsapp.closed(error) catches the routine failure, so you can hand off
- Named or positional template variables, both supported
- Twilio
- Meta Cloud API
whatsapp()
import { whatsapp } from "postboi"
try {
// Free-form text works within 24h of the user's last reply
await whatsapp({ to, message: "Thanks, on its way!" })
} catch (error) {
// Window closed? Send the approved template instead
if (!whatsapp.closed(error)) throw error
await whatsapp({ to, template: "order_shipped", variables: { name } })
}04 The channel that costs nothing
Push
Web Push has no vendor at all: the browser picks the push service, the subscription is the address, and delivery is free. The encryption is ours, WebCrypto only, verified byte for byte against the spec's published test vector.
- subscribe() in the browser, push() on the server, one import each
- FCM for Android, APNs direct for Apple, Huawei for the phones without Play Services
- push.expired(error) makes the routine token cleanup one line
- Web Push
- Firebase Cloud Messaging
- Apple Push Notification service
- Huawei Push Kit
push()
// In the browser: one call to subscribe, same in every framework
import { subscribe } from "postboi/push"
const subscription = await subscribe({ key: vapid_public_key })
// On the server: one call to notify
import { push } from "postboi"
await push({ to: subscription, title: "Order shipped", message: "On its way" })05 Your app's ops channel
Chat
Slack, Discord, Teams and Telegram, each with a webhook URL or bot token as the only credential. slack() and discord() are separate imports, so posting to several platforms needs no provider choice at all.
- One env var per platform and your app can talk to the team
- Titles render natively on every platform
- Dead legacy Teams URLs are rejected loudly, never silently
- Slack
- Discord
- Microsoft Teams
- Telegram
slack() · discord()
import { slack, discord } from "postboi"
// One env var per platform, and your app can talk to the team
await slack({ title: "Deploy", message: "Finished in 42s" })
// Another platform is another import, not a provider choice
await discord({ message: "New release is live 🎉" })06 The chain
One call.
Cheapest first.
send() fans out to every channel you have an address for, or walks a fallback
chain and stops at the first success. The spread isn't marginal: push is
free and an SMS into Western Europe is about 3p, so preferring a cheaper channel saves the
whole message, not a slice of it.
- 01 PUSH FREE READY
- 02 CHAT FREE READY
- 03 EMAIL 0.04P READY
- 04 WHATSAPP 1.70P READY
- 05 SMS 2.80P READY
READY WHEN YOU ARE
SEND() · FALLBACK CHAIN
import { send } from "postboi"
const result = await send({
to: {
push: subscription,
email: "ada@example.com",
sms: "+44 7788 223344"
},
channels: "cheapest",
subject: "Your code",
message: "Your code is 4291"
})
result.delivered // "push" : sms was never attempted, never billedThe fan-out runs in your process. No hosted orchestrator in the send path, nobody metering the routing, and a WhatsApp message outside its 24-hour window simply hands off to SMS.
07 The safe path
Development
can't cost you money.
In development, texts and WhatsApp messages are logged, never sent, even with a fully configured provider. A stray email is embarrassing. A stray text costs money, reaches a real handset, and cannot be recalled. So the safe path is the one you get by doing nothing.
TERMINAL · NODE_ENV=DEVELOPMENT
postboi: development, texts are logged, not sent.
postboi (mock sms): +447788223344
from: POSTBOI
cost: 1 segment (gsm7)
Your code is 4291Need real delivery locally? Opt out explicitly with POSTBOI_SMS_DEV=send.
Chat posts for real, because posting to your own Slack is usually the point.
08 The ledger
Where the meter
would usually be.
Notification platforms charge for orchestration: per notification, per subscriber, or per contact. Postboi is a library, so the orchestration is yours and the meter never appears.
| Postboi | Knock | Courier | OneSignal | sent.dm | |
|---|---|---|---|---|---|
| Metered on | Nothing. Your providers bill at cost | Notifications | Notifications | Subscribers | Contacts |
| Fan-out runs | In your process | Their servers | Their servers | Their servers | Their servers |
| Hosted or bring your own | Bring your own | Bring your own | Bring your own | Not offered | |
| Open source | Yes | SDKs only | SDKs only | No | No |
CHECKED AUGUST 2026 · THEIR PRICING PAGES HAVE THE FINE PRINT · OURS DOESN'T EXIST
Five channels. One boi.
One command between you and a message that actually arrives.
Start sending, freeNO CARD · NO DNS · 3,000 EMAILS A MONTH, FOREVER