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.
The one that started it
Hosted sending with a single token, or 20+ 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
import { mail } from "postboi"
await mail({
to: "ada@example.com",
subject: "Your order shipped",
body: "<p>Track it any time.</p>"
})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
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 }
})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
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 } })
}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
// 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" })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
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 🎉" })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 3p, so preferring a cheaper channel saves the whole message,
not a slice of it.
Tap a channel to make it fail. Watch the chain route around it.
Delivered on push. 4 channels were never attempted, never billed.
Indicative UK prices. Your provider sets the real ones.
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.
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.
Need 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.
postboi: development, texts are logged, not sent.
postboi (mock sms): +447788223344
from: POSTBOI
cost: 1 segment (gsm7)
Your code is 4291Where 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, because there's nothing to price.
Five channels. One boi.
One command between you and a message that actually arrives.
Start sending, free