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

# SMTP relay ingress model

> How Orbit's SMTP relay ingress works underneath your send: the wire surface that mirrors the SMPP edge for SMS, the API-key AUTH chokepoint, the envelope-to-dispatch pipeline, sandbox vs live keys, and the SMTP replies a failed handshake returns.

# SMTP relay ingress model

Orbit's SMTP relay is the email pillar's wire surface — the protocol endpoint a
standard SMTP client points at, the same way the [SMPP edge](/concepts/smpp-edge-model)
is the SMS pillar's wire surface for an SMPP client. Where SMPP terminates a
`bind_transceiver` + `submit_sm` conversation, the relay terminates an `AUTH` +
`MAIL FROM` / `RCPT TO` / `DATA` conversation, then hands the captured
submission to the same email pipeline `POST /api/v1/messages/email` uses —
with zero duplication of suppression, consent, billing, verified-sender
enforcement, and the delivery log.

The [SMTP send guide](/guides/smtp-send-email) covers the client side: ports,
the `apikey` sentinel, and client snippets. This page covers the platform
model underneath — who owns each decision in the handshake, and what a given
SMTP reply tells you about which stage rejected it.

## 1. The wire surface alongside SMPP

Both wire surfaces share one property: the protocol endpoint is a projection
of your account configuration, not a bespoke gateway you configure directly.
The SMPP edge fronts a Jasmin relay whose users mirror the `smpp_credentials`
rows you write; the SMTP relay front terminates the submission conversation
and resolves the same API-key identity your HTTP traffic uses, so no new
credential class exists — a key from **Settings → API Keys** is the only
credential the relay understands.

| | SMPP edge (SMS) | SMTP relay ingress (email) |
| - | - | - |
| Protocol | SMPP 3.4 | RFC 5321 SMTP |
| Client auth | `system_id` / password credential (reconciled to a Jasmin user) | Username `apikey` + any active API key as password |
| Wire unit | `submit_sm` PDU | One `DATA` payload (RFC 5322 message) |
| Recipient authority | PDU `destination_addr` | Envelope `RCPT TO` (headers are a fallback) |
| Downstream | SMS pillar | Email pillar — the same pipeline `POST /messages/email` drives |
| Dashboard surface | Developer → SMPP | Developer → SMTP |

Because the two surfaces are twins, the same question answers both: "do I
integrate on the wire, or over HTTP?" Pick the wire when the client already
speaks the protocol (a legacy app, a mail-enabled device, an SMPP sender);
pick HTTP when the integration is new and first-class API features
(idempotency keys, templates, URL attachments) matter.

## 2. Ingress auth — one chokepoint, one verdict

The AUTH stage resolves to exactly one verdict — accepted or rejected — and
maps to one SMTP reply when it fails.

1. **Username sentinel.** The client must present `apikey` as the AUTH
   username (SendGrid parity). Any other username is a rejection, before the
   password is even considered.
2. **Password is the API key.** The password carries the full API key value
   (`dv_live_sk_…` or `dv_test_sk_…`). The relay resolves it through the same
   API-key cache the HTTP middleware warms, so an already-recognized key
   costs no extra DB round-trip, and a revoked key fails identically on both
   surfaces the moment it is evicted from that cache.
3. **Single opaque rejection.** Every AUTH failure — wrong username, unknown
   key, revoked key, expired key — returns one `535 5.7.8` reply. The wire
   never reveals which check failed; the dashboard's API-keys surface is
   where you diagnose it.

Once a credential resolves, the relay has the same tenant scope the HTTP path
would have: organization id, tenant id, and the tenant schema name every
downstream write targets. One tenant's key can only ever land submissions in
its own schema — the isolation rule from [Tenant isolation](/concepts/tenant-isolation)
applies unchanged at the wire.

After AUTH, a single acceptance check decides whether an envelope is relayed.
The reply code tells you which stage said no:

| SMTP reply | Stage | What it means |
| - | - | - |
| `535 5.7.8` | AUTH | Credentials rejected (sentinel wrong, key unknown / revoked / expired). |
| `553 5.1.7` | `MAIL FROM` | Sender address failed syntax. An empty `<>` null sender (bounces) is valid. |
| `552 5.3.4` | `SIZE` extension | Declared message size exceeds the 30 MiB ceiling. |
| `554 5.5.1` | `RCPT TO` | No recipient was supplied. |
| `501 5.1.3` | `RCPT TO` | A recipient address failed syntax. |
| `452 4.5.3` | envelope | More than 1000 recipients — transient; re-send in batches. |
| `250 2.0.0` | accepted | Envelope accepted for the dispatch stage. |

Two guardrail defaults matter here: a submission carries at most **1000
recipients** and at most **30 MiB** of declared message size (the same
ceiling the inbound-parse path enforces, so inbound and outbound share one
limit). Both are envelope-level — they run before any MIME parsing, so an
oversize or over-recipient envelope is refused without reading the payload.

## 3. Message capture — from DATA into the email pipeline

An accepted envelope enters the send path as one transform with three steps:

1. **Parse.** The raw `DATA` payload is parsed by the same MIME parser the
   inbound-parse receiver uses — one parser, not two. An empty or
   unparseable payload maps to a single `554 5.6.0` reply, not a wire fault.
2. **Resolve recipients and body.** The envelope `RCPT TO` list is the
   authoritative recipient set — the only list that carries `Bcc` — with
   `To`/`Cc` headers as a defensive fallback when no envelope list arrived.
   The body prefers the non-empty HTML part over plain text, matching the
   HTTP controller's selection so the two ingress paths cannot drift. An
   empty body is a `554`.
3. **Dispatch per recipient.** One message send per accepted recipient, the
   same fan-out the HTTP route performs. Sender is the message `From:` header
   (what verified-sender enforcement checks), with the envelope `MAIL FROM`
   as fallback. The submission is tagged with source `smtp-relay` in metadata
   so the delivery log attribute it back to the relay.

Because the dispatch lands in the shared email pipeline, the captured message
inherits the full [email delivery lifecycle](/concepts/email-delivery-lifecycle):
it enters as `queued`, transitions to `sent` when the provider accepts it,
then resolves to `delivered`, `bounced`, or `complained` per the provider's
webhooks — exactly what an API-sent message does. Per-recipient outcomes are
collected: a suppressed or rejected recipient is reported in the relay
result, while every accepted recipient still delivers. The transaction-level
reply after `DATA` is `250 2.0.0` when at least one recipient was queued;
`554` only when every single one was rejected.

Attachment bytes are not carried through the relay — the parser records the
attachment count in metadata only. Send attachment-bearing email over the
HTTP API, which exposes attachments as a first-class field.

## 4. Sandbox vs live behavior

The relay draws no sandbox/live split of its own — it inherits one from the
API key you authenticate with:

* **Live key (`dv_live_sk_…`)** — the submission dispatches to the provider,
  bills against your wallet, and appears in the delivery log as a real send.
* **Test key (`dv_test_sk_…`)** — AUTH resolves identically, the envelope
  checks run identically, and the per-recipient dispatch runs — but the
  downstream send pipeline short-circuits the provider call, mints a
  synthetic row, and skips billing. A test-key submission proves your SMTP
  client handshake end-to-end without sending real mail.

That split means the relay itself is not a separate sandbox surface: pick
the mode by picking the key. The swaks probe in the
[send guide](/guides/smtp-send-email) is safe to run repeatedly on a test
key for exactly this reason.

## 5. Rate limiting and what a failed handshake sees

Relayed submissions draw from the **shared email rate pool** — the same
per-tenant pool the HTTP email path draws from (default 200 messages per
minute per tenant; see [Rate limits](/guides/rate-limits)). Moving traffic
between HTTP and SMTP does not dodge the limit, because both paths consume
one pool. When the pool is exhausted the send pipeline rejects the dispatch
with `RATE_LIMITED`, and the SMTP session sees it as a per-recipient
rejection — the envelope itself is already accepted at that point.

A **failed handshake** (before any envelope is accepted) is always a
protocol-level rejection the client observes directly — never a silent drop
and never an HTTP-style status. The ordering your client sees is fixed:

1. AUTH fails → `535 5.7.8`, session usable for a retry.
2. `MAIL FROM` fails → `553 5.1.7`; the transaction resets.
3. A bad `RCPT TO` → `501 5.1.3`; other recipients in the same envelope are unaffected.
4. Over-recipient envelope → `452 4.5.3`; re-send in batches under the cap.
5. Oversize `SIZE` → `552 5.3.4`; the envelope is refused before `DATA`.

For a step-2+ rejection the check that fired tells you the surface to fix:
`553`/`501` mean the client's envelope is malformed, `452` means batching,
`552` means your message is over the 30 MiB ceiling, and `535` means the key,
not the envelope. None of those failures touch the pipeline — no row is
written, nothing is billed, nothing is queued.

## Worked example — one submission end to end

The [send guide's swaks probe](/guides/smtp-send-email) is the same
handshake; here is what the relay does with it.

```
swaks --server smtp.orbit.devotel.io:587 --tls \
  --auth-user apikey --auth-password $ORBIT_API_KEY \
  --from you@your-domain.com --to customer@example.com \
  --header "Subject: Hello from Orbit"
```

Stage by stage:

1. **AUTH `apikey` / \$ORBIT\_API\_KEY** → resolves through the API-key cache →
   tenant scope (org, tenant, schema) returned → proceed.
2. **`MAIL FROM:<you@your-domain.com>`** → syntax-valid → accepted.
3. **`RCPT TO:<customer@example.com>`** → syntax-valid, one recipient →
   accepted.
4. **`DATA`** → MIME parser extracts subject, the `From:` header for
   verified-sender enforcement, and the body (HTML preferred over text).
5. **Dispatch** → one send into the shared email pipeline for
   `customer@example.com`, tagged `source: smtp-relay`.
6. **Reply `250 2.0.0`** → the swaks run prints `Message accepted for
   delivery`; the row is `queued` in your tenant schema and follows the
   [email delivery lifecycle](/concepts/email-delivery-lifecycle) from there
   on a live key, or is a synthetic no-bill row on a test key.

If step 5 rejects the recipient — the address is suppressed, the sender
domain is unverified, or the rate pool is exhausted — the reply is still the
protocol-level one: a per-recipient rejection collected into the result, with
the transaction reply dropping to `554` only when every recipient was
rejected.

## Who owns which failure

| Symptom | Owner | First check |
| - | - | - |
| `535` on AUTH | The key | Is the full `dv_*_sk_…` value sent as the password, and is the key active under Settings → API Keys? |
| TLS/connect failure before AUTH | Your network | Port `587`/`2525`/`465` and TLS verification against `smtp.orbit.devotel.io`. |
| `553 5.1.7` on `MAIL FROM` | Client envelope | The sender address syntax (or use a real sender, not a raw string). |
| `501 5.1.3` on one `RCPT TO` | Client envelope | That recipient's address syntax. |
| `452 4.5.3` | Client batching | Split submissions under the 1000-recipient cap. |
| `552 5.3.4` | Client payload | The declared `SIZE` is over 30 MiB — attachment bytes do not travel over SMTP; use the API. |
| `250` but the recipient never gets it | Pipeline stage | The delivery log — suppression, verified sender, or the provider-side outcome from the lifecycle page. |
| Per-recipient rejection on one address | Pipeline stage | Suppression entry, unverified sender domain, or rate-pool exhaustion. |

## Related

* [Send outbound email over SMTP](/guides/smtp-send-email) — the client-side
  guide: ports, the `apikey` sentinel, client snippets, and the feature
  limits.
* [SMPP edge model](/concepts/smpp-edge-model) — the SMS twin of this
  surface: reconciled credentials, DLR delivery modes, and BYO carrier
  routing on the other wire.
* [Email delivery lifecycle](/concepts/email-delivery-lifecycle) — what a
  captured message transitions through once it leaves the relay.
* [SMTP relay recipes](/guides/smtp-relay-recipes) — the HTTP-side request /
  response / error loop for the same pipeline, with the shared rate pool the
  relay draws from.
* [Tenant isolation](/concepts/tenant-isolation) — the schema-level rule an
  authenticated relay submission inherits for every downstream write.
* [Rate limits](/guides/rate-limits) — the shared per-tenant pool both HTTP
  and SMTP email traffic draw from.
