> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Split transactional and marketing email into separate streams

> Partition your email into transactional and marketing sending streams so bulk volume can never drag receipt deliverability down with it. Create the streams, set a default, bind senders, tune per-stream throttles, and migrate existing senders.

# Split transactional and marketing email into separate streams

A password reset and a Friday promotion have nothing in common except your
sending reputation when they leave through one undifferentiated queue. A
complaint spike on the promotion then lands your resets in the spam folder,
and the resets are exactly the mail you cannot afford to lose.

Email streams fix this by partitioning your org's sending into named
streams. Each stream carries its own reputation class (`transactional`,
`marketing`, or `custom`), its own throttle ceilings, and its own roster of
senders, so one kind of traffic cannot consume or damage another. If you
send both classes from the same org, this split is the single biggest
deliverability move available to a new sender: do it before warm-up ramps,
before volume, before anything else.

**You will:**

1. [Understand what a stream isolates](#1-what-a-stream-isolates)
2. [Create the transactional and marketing streams](#2-create-the-transactional-and-marketing-streams)
3. [Set the default stream](#3-set-the-default-stream)
4. [Bind senders to a stream and verify sends](#4-bind-senders-to-a-stream-and-verify-sends)
5. [Tune per-stream throttles](#5-tune-per-stream-throttles)
6. [Read per-stream delivery and suppression health](#6-read-per-stream-delivery-and-suppression-health)
7. [Migrate existing senders without a big bang](#7-migrate-existing-senders-without-a-big-bang)
8. [Know when streams are enough and when to add a dedicated IP](#8-when-to-add-a-dedicated-ip)

## 1. What a stream isolates

Every stream you create gives you three separations:

* **Reputation surface.** Mailbox providers grade the traffic they see.
  Bulk marketing mail legitimately runs at higher complaint rates than
  transactional mail; on a shared identity, those complaints are counted
  against your receipts too. On separate streams, each class is graded on
  its own behaviour.
* **Throttle ceilings.** Each stream carries its own `maxPerMinute` and
  `dailyQuota`, so the blast class's pacing is set and read independently
  of the transactional class's configuration.
* **Sender roster.** A sender belongs to exactly one stream, so what each
  class sends, and how it performs, is readable per stream instead of
  blended into one org-wide average.

The `kind` field is a closed set: `transactional`, `marketing`, or
`custom`. Use `custom` for a sub-brand or a one-off broadcast class that
deserves its own reputation surface.

## 2. Create the transactional and marketing streams

Create both streams with `POST /email/streams`. Only `name` is required
(1 to 80 characters); `kind` defaults to `transactional` when omitted.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/email/streams \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Transactional",
    "kind": "transactional",
    "description": "Password resets, receipts, account alerts"
  }'
```

The first stream you create becomes the org default automatically. The
response is a **201** carrying the full stream row:

```json theme={null}
{
  "data": {
    "id": "estream_9f2c81ab4e07...",
    "name": "Transactional",
    "kind": "transactional",
    "description": "Password resets, receipts, account alerts",
    "throttle": { "maxPerMinute": null, "dailyQuota": null },
    "isDefault": true,
    "createdAt": "2026-10-14T09:00:00.000Z"
  },
  "meta": { "request_id": "req_..." }
}
```

Now the marketing stream, with its own throttle ceilings set at creation:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/email/streams \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marketing",
    "kind": "marketing",
    "description": "Campaigns, newsletters, promotions",
    "throttle": { "maxPerMinute": 300, "dailyQuota": 50000 }
  }'
```

`throttle.maxPerMinute` and `throttle.dailyQuota` are positive integers, or
`null` for no per-stream cap (the stream then inherits the org-level send
policy). Save both `estream_...` ids; every step below references them.

Two guardrails to know: stream names are unique per org
(case-insensitive), so a duplicate name returns **409** and you should edit
the existing stream instead; and `GET /email/streams` lists every stream
with its current throttle and default flag at any time.

## 3. Set the default stream

The org always has exactly one default stream; the platform maintains that
invariant on every write. The default is where any send resolves when its
sender carries no explicit stream assignment, so make the
**transactional** stream the default: untagged traffic should always take
the safe path.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/email/streams/estream_9f2c81ab4e07.../set-default \
  -H "X-API-Key: dv_live_sk_..."
```

The response returns the promoted stream with `isDefault: true` and the
previous default demoted. Calling set-default on the current default is a
no-op, so it is safe to run in automation.

Resolution is deterministic: a send takes the stream explicitly assigned
to its sender, otherwise the default stream, and an org with no streams
configured sends exactly as it did before this feature. You can also
promote at create or update time with `"isDefault": true`.

## 4. Bind senders to a stream and verify sends

Streams carry no mail by themselves; senders do. Register a sender against
a stream with `streamId` at creation, or re-assign an existing sender with a
`PUT`:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/email/senders \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "yourdomain.com",
    "default_from_email": "receipts@yourdomain.com",
    "default_from_name": "Acme Receipts",
    "streamId": "estream_9f2c81ab4e07..."
  }'
```

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/email/senders/esend_... \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "streamId": "estream_..." }'
```

Assigning a `streamId` that does not exist is rejected with **400** before
anything is persisted, so a typo can never leave a sender pointing at a
ghost stream. Send `"streamId": null` to clear an assignment; the sender
keeps sending and falls back to the default stream.

Templates do not carry a stream; the send's sender does. Render a template
as usual and dispatch it from the `from` address of a sender bound to the
right stream.

A send returns the same **202** envelope regardless of stream, with
`data.succeeded` rows and `meta.request_id`:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/email \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "from": "receipts@yourdomain.com",
    "subject": "Your Acme code",
    "html": "<p>Your one-time code: 123456</p>"
  }'
```

To verify which stream a send travelled on, read the sender back with
`GET /email/senders`: each sender row carries its `streamId`, and the
`from` address on the send resolves to that sender. Correlate the
`meta.request_id` with your webhook events to follow the message through
delivery on that stream's traffic.

## 5. Tune per-stream throttles

Throttles are edited in place with `PUT /email/streams/:streamId`:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/email/streams/estream_... \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "throttle": { "maxPerMinute": 300, "dailyQuota": 50000 } }'
```

The update semantics are deliberate:

* Omitting `throttle` leaves the existing ceilings unchanged.
* Sending an explicit empty object `{ "throttle": {} }` clears both
  ceilings back to `null` (no per-stream cap).
* Each ceiling is independent: set `maxPerMinute` alone and `dailyQuota`
  stays as it was.

Size the marketing stream's ceilings to the blast you actually plan, and
keep the transactional stream uncapped or capped far above your peak
receipt volume. The protection a blast gets from the split is the
partition itself: the campaign's senders live on the marketing stream, so
its pacing and complaint exposure stay with that stream and never touch
the reputation of the senders your receipts use. The ceilings you store
are each stream's own pacing policy, and the org-wide send pool still
applies to every send, so a pool-level 429 is not a stream-throttle
rejection. If the marketing ceilings are too tight for a planned send,
raise them here rather than moving marketing mail onto the
transactional stream.

## 6. Read per-stream delivery and suppression health

Because a sender belongs to exactly one stream, per-stream health is the
roll-up of that stream's senders. Two surfaces cover it:

* **Webhooks.** Subscribe `message.delivered`, `message.failed`,
  `email.opened`, and `email.clicked` as in
  [Send your first transactional email](/guides/first-transactional-email),
  and group events by the sending address. Each address maps to one
  sender, each sender to one stream, so delivery, bounce, and complaint
  rates split cleanly per stream.
* **The dashboard.** Channels → Email shows delivery and engagement for
  your sends, and the Sent Emails log carries the sending address on every
  row, so you can read each stream's traffic the same way.

Watch the complaint and hard-bounce rate per stream after every large
blast; a rising complaint rate on the marketing stream is now a
marketing-stream problem, contained where it belongs.

Suppressions are org-wide on purpose: hard bounces, exhausted soft
bounces, complaints, and unsubscribes are held out of all future sends on
every stream, with the quota slot refunded. A recipient who hard-bounced a
campaign should not receive a retried password reset either; streams
separate reputation exposure, not the suppression list. Manage it under
**Channels → Email → Suppressions**.

## 7. Migrate existing senders without a big bang

If you already send both classes through one undifferentiated setup, move
in steps rather than flipping everything at once:

1. **Audit.** List your senders (`GET /email/senders`) and label each one by
   what it actually sends: transactional, marketing, or mixed.
2. **Create the streams and set the default.** Follow steps 2 and 3, with
   the transactional stream as default so any sender you miss keeps
   resolving to the safe path.
3. **Move clean senders first.** Senders that are purely transactional or
   purely marketing get their `streamId` set immediately with `PUT`.
4. **Split mixed senders by percentage.** For a sender that sends both,
   register a second sender address for one class (for example
   `news@yourdomain.com` for marketing), point a slice of that traffic at
   it, and raise the share over one to two weeks while you watch both
   streams' delivery and complaint deltas.
5. **Compare deltas.** Delivery rate and complaint rate per stream,
   before and after each move. The transactional stream's numbers should
   stay flat or improve as marketing volume leaves it.

Deleting a stream later never breaks its senders: they read as
unassigned and keep sending through the default stream, and the most
recently created remaining stream is promoted if you delete the default.

## 8. When to add a dedicated IP

Streams partition reputation *within* your sending identity; a dedicated
IP gives you an IP reputation that is entirely your own. Streams alone are
enough while your volume sits comfortably in the shared pool and your two
classes behave differently enough that separating them removes the
cross-contamination. Add a dedicated IP when your sustained volume is high
enough to sustain its own warm-up and reputation, or when a class's
traffic pattern (very large periodic blasts) would swing a shared IP's
standing. The warm-up ramp and volume posture for that step are covered in
the [Email channel lifecycle guide](/guides/email-lifecycle-guide). The two
controls compose: dedicated IP for isolation at the IP layer, streams for
isolation between traffic classes on top of it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.