Skip to main content

SMTP relay

Orbit’s SMTP relay is an ingress endpoint: your SMTP client connects, authenticates with an API key, and submits a standard RFC 5322 message. The submission enters the same email pillar that POST /api/v1/messages/email uses — the same suppression checks, consent gates, billing, delivery tracking, and verified-sender enforcement apply either way. This is not a deliverability relay in the SES sense. Orbit does not accept arbitrary email for relay to third-party providers; it accepts your email and hands it to the same outbound pipeline the rest of the platform uses.
The SMTP relay is in early access and rolls out per account. If the dashboard surface at Developer → SMTP shows the early-access notice, request access before pointing production traffic at the endpoint.

When to use it

Use the relay when the sending system already speaks SMTP and switching it to HTTP is not practical — a CRM with built-in SMTP notifications, a point-of-sale system, monitoring software, or an ERP that can only emit email. If you are writing new code, prefer the HTTP endpoint on Channels → Email: it returns structured errors, supports attachments and cc/bcc, and is the better integration surface. The relay exists so legacy systems join the same pipeline without a rewrite.

Credential model

The relay authenticates with your existing API keys — there is no separate SMTP credential to issue. The same key gates both the HTTP send endpoint and the SMTP submission, and every relayed message is scoped and billed to the key that authenticated it. Revoke or rotate a key under Settings → API Keys and both ingress paths follow immediately. Mint or manage keys from Developer → API Keys (the relay page links straight there).

Connection profile

TLS is required on every port. Use port 587 unless your network blocks it — 2525 exists for that case — and 465 only if your client cannot do STARTTLS. The username is a fixed sentinel. Your client must send the literal string apikey as the SMTP AUTH username and the full API key as the password. Authentication failure returns a single 535 reply regardless of which check failed, so a wrong username and a revoked key look identical on the wire.

Message mapping

The relay converts the SMTP session into the same payload shape the HTTP endpoint produces:
  • Envelope recipients win. The recipient list comes from the envelope RCPT TO commands (which is what you actually addressed, including Bcc). Header To/Cc are only a fallback when the client presents no envelope recipients.
  • Sender. The From: header is preferred (it is what recipients see and what verified-sender enforcement checks); the envelope MAIL FROM is the fallback. The sender domain must be verified exactly as it would be for an HTTP send — an unverified domain is rejected either way.
  • Body. The HTML part is preferred over the plain-text part, matching the HTTP endpoint’s selection so the two ingress paths cannot drift. A message with no parseable body content returns 554.
  • Headers. Standard headers (Subject, date, message-id) map to the message record. Custom X- headers your system adds do not become contact fields — use the HTTP endpoint’s metadata for programmatic fields.

Recipients and suppression

Suppression and consent behave identically on both ingress paths because the relay dispatches into the same send pipeline. A recipient on your email suppression list (hard bounce, complaint, unsubscribe, or a manual entry) is refused at the pre-send gate, exactly as RECIPIENT_OPTED_OUT behaves on the HTTP path, and you are not charged for it. See Bounce and Complaint Handling for the suppression lifecycle, and Opt-out lists for consent configuration. After DATA, the relay returns one transaction-level reply: 250 if at least one recipient was accepted (the count is included in the reply text), or 554 if every recipient was rejected by the pipeline.

Throughput limits

Relayed sends consume the same per-tenant rate pool as HTTP sends — 200 messages per minute per tenant on the email channel by default. A burst that exceeds the pool is throttled rather than queued indefinitely; when the relay’s submission envelope exceeds one recipient, the per-recipient fanout draws from that same pool. Check the current pool and any per-tenant overrides in Rate limits. Connection-level flood protection closes abusive sessions; keep reconnecting clients on a bounded retry loop.

Example session

Probe the endpoint with swaks or verify TLS with openssl s_client:
The same session against port 587 begins with STARTTLS before EHLO. A 535 after AUTH means the key failed; a 554 after DATA means the message or every recipient was rejected — check sender verification and suppression before retrying. To see the per-recipient fanout, address two RCPT TO recipients in one probe — one live address and one suppressed address. The relay accepts the message when at least one recipient survives, and the 250 reply names the accepted count:
The suppressed address is refused at the pre-send gate after DATA — not billed, not delivered — while the accepted one delivers. Only when every recipient is rejected does the relay answer 554.

Connection troubleshooting

Work through these in the order your client hits them: a 335 stops the session before authentication, a 535 is authentication itself, and a 554 after DATA is the pipeline refusing the message.

335 StartTLS expected before Authenticate

The server requires the session to be encrypted before it accepts credentials, but your client tried to authenticate on the plaintext connection. Library-specific — Nodemailer’s secure: false is the classic case. Fix: upgrade with STARTTLS before AUTH. In Nodemailer use secure: false plus requireTLS: true; most other clients enable STARTTLS when the port maps to 587/2525. If the client cannot do STARTTLS, move it to port 465 (implicit TLS) instead.

535 Authentication failed

The relay answers a single 535 for every credential failure, so a wrong username and a dead key look identical on the wire. Check in this order:
  1. The username must be the literal string apikey — not your account id, tenant id, or an email address.
  2. The password must be an active tenant API key, full value, no prefix trimming.
  3. If the key was rotated or revoked under Developer → API Keys, point the client at the replacement key — an environment variable or secrets-injection step is the usual stale-credential source.
Fix: correct the username or substitute a live key and retry. A persistent 535 after all three checks usually means the client base64-encodes its AUTH payload with a trailing newline — strip whitespace at the client boundary.

554 after DATA — message rejected

The pre-send pipeline refused the message as a whole: either the sender failed verification, or every envelope RCPT TO recipient was turned away by suppression or consent. Two root causes, fixed in different places:
  • Sender unverified. The domain in the From: header must be a verified sender under the Email channel. Complete the DNS records in Email → Senders exactly as you would for an HTTP send — see Domain Setup on the Email channel.
  • All recipients suppressed. Every recipient on the envelope was on your suppression list (hard bounce, complaint, unsubscribe, or manual entry) or blocked by consent. The relay reports 554 only when not a single recipient survived — a mixed list returns 250 with the accepted count. Check Opt-out lists for the consent gate and Bounce and Complaint Handling for the suppression lifecycle.
Fix: verify the sender domain, or clean the recipient list and retry against a smaller target set to identify which address trips the gate.

Client libraries

Every snippet authenticates with the literal username apikey and the API key as the password — the pattern works in any SMTP library that allows a custom AUTH username.

Nodemailer (Node.js)

Python smtplib

Ruby Net::SMTP

PHP (Symfony Mailer)

Go (gomail/v2)

Java (Jakarta Mail)

A 535 after AUTH in any of these clients means the credential check failed — work through the 535 checks above before retrying. Note that PHP and Java read the key with getenv/System.getenv, so a key exported in a different shell or service context than the one the process runs in shows up as a 535, not a connection error. All six snippets submit into the same pipeline as the HTTP endpoint, so sender verification, suppression, and rate limits behave identically to POST /api/v1/messages/email.

Worked HTTP equivalent — request, response, errors, SDKs

The relay maps every accepted submission onto the same payload POST /api/v1/messages/email produces. Model or verify the full loop over HTTP first — the sequences below are the exact request/response/error trio the SMTP session compresses into reply codes. The longer guide with per-SDK catch blocks is at SMTP relay recipes.

Request (HTTP equivalent of an SMTP submission with an attachment)

Response — the 202-accepted envelope

Over the SMTP session this same outcome is the 250 after DATA with the accepted-recipient count.

Error branches worth coding against

The full catalog (including VALIDATION_ERROR, QUOTA_EXCEEDED, and MESSAGE_SEND_FAILED) lives on the Email channel page.

SDK calls (the same send in four languages)

Node.js — @devotel-orbit/node:
Python — orbit_sdk:
Go — devotel/orbit:
PHP — devotel/orbit:
Each SDK throws a typed error carrying the same code values — INVALID_RECIPIENT, CHANNEL_NOT_CONFIGURED, RATE_LIMITED — see SMTP relay recipes for the catch blocks.

Where to go next

Email channel

HTTP send endpoint, templates, domain setup, and the full error catalog.

API integration

End-to-end REST integration patterns including idempotent sends.

Opt-out lists

Configure the suppression and consent checks the relay inherits.

Rate limits

Per-tenant pools the SMTP and HTTP ingress share.