Marketing wants a product update sent to every user. It is quick to call the same sendEmail that sends receipts. A few hundred people hit "report spam". Next morning, magic links arrive late or land in spam.
Postmark built message streams to stop exactly this.
What a stream is
Every Postmark server has streams. Two exist from the start:
| Stream id | Type | Use it for |
|---|---|---|
outbound | Transactional | Mail a user triggered: sign-in, reset, receipt, invite, alert |
broadcast | Broadcasts | Mail you decided to send: newsletter, launch, digest, promo |
Each stream has:
- Its own sending infrastructure. Broadcast traffic goes out separately from transactional traffic, so a bad campaign does not drag your login mail down with it.
- Its own suppression list. An unsubscribe on
broadcastdoes not block a password reset onoutbound. - Its own webhooks and stats.
You pick the stream per message with MessageStream. If you leave it out, Postmark uses outbound.
The test: did the recipient just do something?
- They clicked "email me a link". Transactional.
- They paid. Transactional.
- You shipped a feature. Broadcast.
- It is the first of the month. Broadcast.
- A receipt with "upgrade to annual and save 20%" at the bottom. Broadcast, legally and in the eyes of spam filters. Remove the upsell instead.
Unsubscribes come free on broadcast
On a broadcast stream Postmark:
- appends an unsubscribe link if your body does not have one (or fills its unsubscribe placeholder where you put one),
- adds the RFC 8058 one-click
List-UnsubscribeandList-Unsubscribe-Postheaders that Gmail and Yahoo require of bulk senders, - records the unsubscribe on that stream's suppression list and refuses later sends.
So do not build your own unsubscribe link for broadcast mail. You would end up with two.
Transactional streams have no unsubscribe handling. They do not need it, which is one more reason not to sneak marketing onto them.
Code shape
Keep the stream a per-send choice with a safe default:
await sendEmail({ to, subject, react: ReceiptEmail(props) }); // transactional
await sendEmail({ to, subject, react: LaunchEmail(props), stream: "broadcast" });
Configure stream ids by env (POSTMARK_MESSAGE_STREAM, POSTMARK_BROADCAST_STREAM), not in code. You will add streams later: password-resets on its own, a digests broadcast stream. Env lets each environment point somewhere different without a deploy.
Check the type at startup or in a verify script. GET /message-streams/{id} returns MessageStreamType. A transactional env var pointing at a Broadcasts stream is a bug that sends login links with an unsubscribe footer.
Broadcast volume needs care
- Send broadcasts in batches (
/email/batch, up to 500 per call), from a job, not a request handler. - Only mail people who opted in. A broadcast stream does not make a purchased list OK.
- Watch the complaint rate per stream. Gmail's line is 0.3%. Aim for under 0.1%.
Checklist
- [ ] Every send site either uses the default (transactional) or passes
stream: "broadcast"on purpose. - [ ] No marketing copy in transactional templates.
- [ ] No hand-built unsubscribe link in broadcast templates.
- [ ] Stream ids come from env and are checked for the right type.
- [ ] Webhooks exist on each stream you send from.