> ## 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.

# Isolate and throttle transactional and marketing email streams

> Provision transactional, marketing, and aggregate streams, cap each with its own throttle, pin the default stream, read per-stream bounce telemetry, and migrate off a single stream.

# Isolate and throttle transactional and marketing email streams

[Split transactional and marketing email into separate
streams](/guides/email-streams-transactional-vs-marketing) covers the reasoning
behind the split. This guide is the operator walkthrough: provision the streams,
cap each one's throttle, pin the default, read per-stream health, and migrate an
org that currently sends everything through a single stream.

**You will:**

1. [Classify your mail into three mailbox kinds](#1-three-mailbox-kinds)
2. [Provision three streams with separate ceilings](#2-provision-three-streams)
3. [Map templates to streams](#3-map-templates-to-streams)
4. [Pin sender isolation](#4-pin-sender-isolation)
5. [Read bounce and complaint telemetry per stream](#5-read-per-stream-telemetry)
6. [Migrate from a single stream](#6-migration-playbook)

## 1. Three mailbox kinds

Almost every org's outbound mail falls into three kinds:

* **Transactional.** The recipient triggered it: password resets, one-time codes,
  order receipts, account alerts. Complaint rates should sit near zero, and these
  are the messages you cannot afford to lose.
* **Marketing.** You initiated it: campaigns, newsletters, promotions. A healthy
  marketing program runs at complaint rates several times higher than
  transactional mail. That is normal, and it must never be averaged into your
  resets.
* **Searchable / aggregate.** The recipient opted into a class, not a message:
  activity digests, weekly summaries, saved-search alerts. Mailbox providers grade
  this traffic closer to bulk than to transactional, even when it feels
  operational.

Orbit pins each kind to its own stream because a stream separates three things at
once: the reputation surface mailbox providers grade, the throttle ceilings
(`maxPerMinute` and `dailyQuota`), and the sender roster. One stream per kind
means a compromised marketing blast cannot consume the transactional throttle or
drag the transactional reputation down with it.

Two streams are enough if you genuinely send only two kinds. The third exists for
orgs whose aggregate volume is large enough to deserve its own reputation
surface.

## 2. Provision three streams

Create each stream with `POST /email/streams`. Only `name` is required; `kind`
defaults to `transactional` when omitted. The first stream you create becomes the
org default.

```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": "Resets, receipts, account alerts"
  }'
```

Cap the marketing stream at creation so a campaign bug cannot empty your org's
sending headroom in one retry loop:

```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 }
  }'
```

And the aggregate class on its own `custom` stream:

```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": "Digests",
    "kind": "custom",
    "description": "Activity digests and saved-search alerts",
    "throttle": { "maxPerMinute": 200, "dailyQuota": 30000 }
  }'
```

Leave the transactional stream uncapped (`throttle` omitted, both ceilings
`null`) or capped far above peak receipt volume. `GET /email/streams` lists every
stream with its current ceilings and default flag at any time. Stream names are
unique per org, so a duplicate name returns **409**: edit the existing stream
instead of creating a second one.

## 3. Map templates to streams

In Orbit a template carries no stream field. The stream rides on the **sender**:
each sender row holds a `streamId`, and a send resolves to the stream of the
sender whose `from` address it uses (or the org default when the sender is
unassigned). Mapping a template to a stream therefore means sending it from a
sender bound to that stream.

A workable mapping for six common templates:

| Template | Mailbox kind | Stream | Sends from |
| - | - | - | - |
| Welcome email | Transactional | Transactional | `welcome@yourdomain.com` |
| Password reset | Transactional | Transactional | `security@yourdomain.com` |
| Order receipt | Transactional | Transactional | `receipts@yourdomain.com` |
| Weekly newsletter | Marketing | Marketing | `news@yourdomain.com` |
| Win-back offer | Marketing | Marketing | `offers@yourdomain.com` |
| Activity digest | Aggregate | Digests | `digest@yourdomain.com` |

Bind a sender to its stream at creation with `streamId`, or re-assign an
existing sender with `PUT`:

```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_4b7e11cd90a2..." }'
```

A `streamId` that does not resolve is rejected with **400** before anything is
persisted, and `"streamId": null` clears the assignment, falling the sender back
to the default stream. To confirm which stream a send travelled on, read the
sender back with `GET /email/senders` and match the `from` address.

## 4. Pin sender isolation

The default stream is where any send lands when its sender carries no explicit
assignment, so pin the **transactional** stream: anything you forget to classify
takes the safe path, never the blast 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_..."
```

Verify the pin with `GET /email/streams`: exactly one row shows
`"isDefault": true`, and it is the transactional one.

```json theme={null}
{
  "data": [
    { "id": "estream_9f2c81ab4e07...", "name": "Transactional", "kind": "transactional", "isDefault": true },
    { "id": "estream_4b7e11cd90a2...", "name": "Marketing", "kind": "marketing", "isDefault": false },
    { "id": "estream_8d30aa52f6c1...", "name": "Digests", "kind": "custom", "isDefault": false }
  ]
}
```

The default is forward-only. `isDefault: false` on a `PATCH` is a no-op; the flag
moves only by promoting another stream with `set-default` (or `"isDefault": true`
on create/update), which demotes the previous holder in the same write. Treat a
promotion as a deliberate act: there is no "un-default", only moving the pin
forward to another stream.

With the pin in place, marketing and digest senders stay reserved for their own
traffic because each carries an explicit `streamId`, and the only traffic that
ever reaches the default by fallthrough is unclassified traffic, which is exactly
the traffic you want on the safe path.

## 5. Read per-stream telemetry

Because a sender belongs to exactly one stream, per-stream health is the roll-up
of that stream's senders. Subscribe `message.delivered`, `message.failed`, and
the suppression events as described in [Suppression event
payloads](/guides/email-suppression-event-payloads), then group events by the
sending address: address to sender, sender to stream.

A healthy split reads like this after a marketing incident:

```text theme={null}
Bounce rate by week, per stream

Week:           W1    W2    W3    W4    W5    W6
Transactional  0.3%  0.4%  0.3%  0.4%  0.3%  0.4%   <- flat; resets unaffected
Marketing      0.8%  1.1%  0.9%  3.1%  1.8%  1.2%   <- W4 blast on a stale list
Digests        0.7%  0.8%  0.9%  0.9%  0.8%  0.8%   <- steady aggregate traffic

Reading: the W4 spike is contained on the marketing stream. On a single
undifferentiated stream the same week would have blended to ~1.4% org-wide and
put the transactional reputation at risk.
```

Working thresholds: a complaint or hard-bounce rate above \~0.1% on the
transactional stream is an incident (something misclassified is sending there);
a several-percent spike on marketing is a list-hygiene problem that stays a
marketing problem. The dashboard's Channels → Email view carries the sending
address on every row, so you can read the same split without webhooks.

Suppressions stay org-wide on purpose: a recipient who hard-bounced a campaign is
held out of future sends on every stream. Streams separate reputation exposure,
not the suppression list.

When per-stream isolation is not enough and a class needs its own IP reputation,
the warm-up path is covered in the [Email channel lifecycle
guide](/guides/email-lifecycle-guide). The two controls compose: streams split
traffic classes, a dedicated IP splits at the IP layer.

## 6. Migration playbook

If the org currently sends everything through one stream, move in steps:

1. **Audit.** `GET /email/senders` and label each sender by what it actually
   sends: transactional, marketing, aggregate, or mixed.
2. **Provision and pin.** Create the streams per section 2 and pin the
   transactional default per section 4, so any sender you miss keeps resolving to
   the safe path.
3. **Move clean senders first.** Purely transactional or purely marketing senders
   get their `streamId` set immediately with `PUT`.
4. **Split mixed senders.** Register a second address for one class (for example
   `news@yourdomain.com` for marketing), shift a slice of that traffic onto it,
   and raise the share over one to two weeks while watching both streams'
   delivery and complaint deltas.
5. **Tighten as you go.** Once marketing volume lives on its own stream, cap it
   to the blast you actually plan:

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

Omitting `throttle` on a `PATCH` leaves the existing ceilings unchanged; sending
`{ "throttle": {} }` clears both back to `null`; each ceiling is independent, so
setting `maxPerMinute` alone keeps `dailyQuota` as it was.

Two behaviours to plan around while migrating:

* **In-flight sends keep their path.** A send that already returned **202**
  resolved its stream at acceptance and completes there. A throttle `PATCH`
  applies to sends accepted after it lands, and a sender re-assignment applies to
  that sender's subsequent sends. There is no mid-queue re-routing, so throttling
  down mid-blast slows everything still queued without changing where it sends
  from.
* **The default pin only moves forward.** If you promote the wrong stream,
  promote the right one afterwards; the flag advances rather than reverting.
  During the migration window that makes the transactional pin the safe choice:
  every un-migrated sender falls through to it.

Deleting a stream later never breaks its senders: they read as unassigned and
fall back to the default stream, and deleting the default promotes the most
recently created remaining stream.


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