The zero-config way to send. No provider account, no DNS records, no card — run one
command, authorise in the browser, and mail() works.
Pick Postboi when prompted. The CLI opens your browser to authorise the device,
then writes a single env var — POSTBOI_TOKEN, your API key (keep it secret). Everything
else is config, not environment: it offers to set defaults (to, reply_to, cc, bcc — and from, once you have custom domains to choose between) and writes them to a
committed postboi.config.ts — the same file where you can add hooks later. The CLI knows your domains and their verification status, so it
won’t accept a default from at a domain that isn’t on your account (listing the ones
that are), and warns when the domain is still pending verification.
That’s the whole setup:
from is optional — when omitted entirely, the API uses your account’s sending address,
which the token identifies. Env vars still override config for per-environment tweaks
(POSTBOI_FROM beats default.from), but nothing needs to live in the environment
except the token.
The Postboi provider also includes managed invisible captcha for your forms — one script tag from the dashboard, no Cloudflare account, no keys. See Spam protection.
Your sending address
Free-tier mail goes out from you@send.postboi.email, derived from your signup email.
It’s a real, deliverable address on our reputation-managed sending domain — but it isn’t
an inbox, so set reply_to if you want responses:
You can rename the address (once a day) from the dashboard.
Sending from your own domain
Verify a domain in the dashboard — add a domain, publish the three DKIM CNAME records it
shows you, and hit Check. Once verified, any address at that domain is a valid from:
Type-safe from
postboi init (and bunx postboi sync) generate types from your account’s sending
address and domains, narrowing from so TypeScript rejects addresses you can’t send
from — before the API does it at runtime:
Display-name form works too ("Joe Bloggs <hello@example.com>"), and pending domains are
included deliberately — you can write the code while DNS propagates; deliverability is
enforced at send time either way (from_not_allowed).
The generated types live inside the installed package (node_modules/postboi), so
there’s no file in your project — nothing to commit, gitignore, or see in diffs. Three
consequences of that:
- A reinstall resets them.
initadds a"prepare": "postboi sync"script that restores them after every install (chained onto your existing prepare script, if any). - They’re always optional. Without them (fresh clone, CI without a token, teammate
who hasn’t run init),
fromfalls back to plainstring— builds and deploys never fail because the types are missing.syncitself is a quiet no-op without aPOSTBOI_TOKENand always exits 0, so it’s safe anywhere. - They’re a snapshot. Re-run
bunx postboi syncafter adding or removing a domain (your editor may want a TS-server restart to pick the change up).
This only applies to the Postboi provider (we can’t know another provider’s identities). If you
mix Postboi with a bring-your-own provider in one project, remove postboi sync from your
prepare script — the narrowing applies to from everywhere.
Limits
The free tier stops at its caps; paid tiers keep sending and meter the overage. Every
plan has a burst rate limit. When a limit is hit, mail() throws a PostboiError with a
machine-readable code:
Delivery status
Every send appears in the message log with its delivery status — bounces and complaints are tracked automatically. High bounce or complaint rates pause sending to protect deliverability for everyone; the dashboard shows when that happens.
Notes
scheduled_atschedules a send up to 30 days ahead. Scheduled messages appear in the dashboard’s Messages → Scheduled tab, where they can be rescheduled or canceled until they send. Scheduling counts against the free tier’s daily cap on the day it’s accepted; the monthly quota is charged when the message actually sends.scheduled_ataccepts an ISO 8601 datetime string. Include an explicit timezone offset orZ(e.g.2026-07-10T14:30:00Zor2026-07-10T09:30:00-05:00) — a bare local time without an offset is interpreted as UTC. It must be in the future and at most 30 days ahead.- In runtimes without ambient env vars (e.g. Cloudflare Workers), pass the token
explicitly:
new Postboi({ token }). - The token can be revoked and reissued any time from the dashboard’s API keys panel.