Skip to main content

SMTP relay recipes: send email over HTTP

The SMTP relay capability is a second ingress path for the email channel: your client submits an RFC 5322 message over the authenticated SMTP edge described on Channels → SMTP relay, and the relay maps the session onto the same HTTP payload POST /api/v1/messages/email produces. This guide walks the equivalent HTTP flow end to end — the request body with an attachment, the response a happy path returns, the error branches worth coding against, and each SDK’s call — so you can model or verify client behaviour without touching an SMTP socket. Use this flow before writing SMTP client code, or when you want the SMTP relay’s pipeline guarantees (suppression, verified-sender enforcement, shared rate pool) while keeping payloads and error codes fully visible.

1. Send with an attachment

POST /api/v1/messages/email accepts either html or text (at least one is required) plus an optional attachments array — each entry carries filename, content_type, and either a url (an Orbit Files API URL) or inline base64 content.
Two limits to design around: attachments cap at 10 MB per file, 25 MB per email, 20 files per send, and content_type must be on the MIME allowlist — see Email attachments for the full list and for when to use a Files URL instead of inline base64. The Idempotency-Key header makes client retries safe: the same key replays the original response instead of double-sending. The same body shape is exactly what the SMTP relay produces after mapping an RFC 5322 submission — envelope RCPT TO recipients become to, the From: header becomes from, and the HTML part (preferred over plain text) becomes html.

2. The 202 response envelope

A successful send returns 202 with the message record under data plus a meta block that also carries the request id you saw in the X-Request-Id response header:
status at acceptance time is queued (or scheduled when you pass scheduled_at); poll GET /api/v1/messages/{id} or consume the message-status webhooks to follow it to sent/delivered. price is the credits cost of the send.

3. Error branches to handle

Every error returns the standard envelope { "error": { "code", "message", "status" }, "meta": { ... } }. Three code values account for most production failures on this path:

INVALID_RECIPIENT — 422

The recipient address failed the per-recipient gate (malformed mailbox, list-hygiene check, or suppression). The message is refused before queueing and you are not charged.
Fix: validate recipient addresses before sending, or scrub the list with the address-validation endpoint. A suppressed address (hard bounce, complaint, or unsubscribe) is deliberately refused — re-engage it only after removing the suppression entry (see Bounce and Complaint Handling).

CHANNEL_NOT_CONFIGURED — 503

The tenant has no working email provider: a sender domain was never verified, the provider credentials were revoked, or the channel bootstrap skipped the lane. Nothing is queued.
Fix: complete sender and domain setup under Channels → Email (the same verified domain governs the SMTP relay — a relayed 554 after DATA is the SMTP-session equivalent of this branch). Surface details.provider_message if present for the provider-side detail.

RATE_LIMITED — 429

The shared email rate pool (default 200 messages per minute per tenant, shared with SMTP-relay ingress and burst fanout) is exhausted. The response usually carries a retry_after hint in details.
Fix: back off using the returned retry_after window, then retry. Because HTTP and SMTP relay traffic draw from one pool, moving sends between the two paths does not dodge the limit — see Rate limits. Other codes you may see on this path: VALIDATION_ERROR (bad payload shape or attachment limit), QUOTA_EXCEEDED (billing cap), and MESSAGE_SEND_FAILED (provider-side failure — retry once, then alert). The full catalog lives on the Email channel page.

4. SDK snippets

Each SDK wraps POST /api/v1/messages/email; the examples below send the same attachment email and read the same code branches. Attachments with URL url fields are supported on all four SDKs; inline base64 content follows the same body shape shown in the curl above.

Node.js — @devotel-orbit/node

Python — orbit_sdk

Go — devotel/orbit

PHP — devotel/orbit

The retry_after value in details is your best backoff hint — use it rather than a fixed sleep.

Where it connects back to SMTP

Once the client works over HTTP, point it at the SMTP edge only if HTTP is not practical — see Connection profile on the SMTP relay channel page for ports and the apikey sentinel. The same API key gates both paths, the same sender domain gates them both, and suppression drawn from either path applies to the other.