# Postmark message streams, transactional vs broadcast, and why the split protects your login emails

> Postmark sends transactional and broadcast mail through separate streams with separate reputations and suppression lists. Route every send on purpose, and never put marketing on the transactional stream.

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 `broadcast` does not block a password reset on `outbound`.
- **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-Unsubscribe` and `List-Unsubscribe-Post` headers 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:

```ts
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.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
