Skip to main content

Email

Send beautiful, reliable emails through Orbit’s email infrastructure. Build templates with the drag-and-drop block builder for pixel-perfect rendering across all clients, or send simple HTML and plain text. This page covers the full channel: send, template lifecycle, domain setup, limits, engagement tracking, bounce and complaint handling, the error catalog, and troubleshooting. For a day-one-to-at-scale walkthrough that sequences these sections into a plan, see the Email channel lifecycle guide.

Send an Email

The send is accepted asynchronously and confirmed by a message.delivered webhook. A fully-clean dispatch returns 202; a partially-failed multi-recipient dispatch returns 207:
succeeded carries one row per dispatched recipient; when any recipient failed, each failed row keeps to plus the actionable code / message / details so the dashboard renders a per-recipient hint. to accepts a single address or an array. from and reply_to accept a bare address (support@acme.io) or the RFC-5322 mailbox form with a display name (Acme Support <support@acme.io>); cc and bcc accept either form or an array of either. Either html or text must be non-empty — a body-less send is rejected before any message is queued.

Using Templates

Templates are a two-step flow: render your email-builder blocks into a complete HTML document, then send that HTML through the standard send endpoint. The direct /messages/email endpoint takes a finished html (or text) body — it does not accept a template_id or variables. Step 1 — render blocks to HTML with POST /api/v1/messages/email-builder/render. Merge values inline using {{...}} tags in your block content:
The response carries the static HTML plus an AMP-for-Email alternative (text/x-amp-html, which non-AMP inboxes ignore), and a templateId you can correlate with a later save:
Step 2 — send the rendered HTML through POST /api/v1/messages/email:

Save, list, and reuse templates

Rendering is stateless — nothing is persisted by render. To reuse a definition, save it and list your saved templates:
  1. Save with POST /api/v1/messages/email-builder/save — body shape mirrors render (name, blocks, optional id, previewText, metadata). The content is stored as a {body, blocks} envelope: the plain-text body is extracted from header, text, button, and footer blocks (capped at 8,192 characters) and serves list snippets and send-time fallbacks; blocks re-hydrates the builder canvas. Re-saving the same name without an id updates the existing template in place; passing an id that collides with a different template’s name returns 409 TEMPLATE_NAME_ALREADY_EXISTS.
  2. List with GET /api/v1/messages/email-builder/list — returns your tenant’s saved email-builder templates (channel email), freshest-first, each with its editable blocks array, rendered plain-text body, previewText, and metadata. Cursor-paginate with the opaque pagination.cursor value; limit defaults to 50 (max 50). This is the same list the dashboard “Load template” picker uses to re-hydrate the canvas.
  3. Render the loaded template by passing its blocks (and optional id) back to /render, then send as above.
Save caps at 60 per minute per tenant. Listing requires the templates:read or templates:write scope.

Personalization with merge tags

Inline {{...}} tags in block content are preserved verbatim by the renderer so the personalization layer can substitute values at send time. URL-bearing fields (url, logoUrl, href, src, action) pass through when they contain a single merge tag like {{contact.unsubscribeUrl}}; every other URL value is normalized — bare hosts get https:// prepended, and unsafe schemes (javascript:, data:, file:, etc.) are neutralized to #. A tag that was never resolved lands on the wire as the literal {{...}} string, so resolve every merge before you send. If name in the render payload matches an AMP amp-list block’s titleField, the {{title}} substitution happens inside the AMP document. Keep merge keys to safe identifier characters (letters, digits, dots, underscores) — anything else falls back to title.

Features

  • Transactional email — receipts, notifications, password resets
  • Marketing email — campaigns, newsletters, announcements
  • Block-builder templates — save, list, and render drag-and-drop block templates
  • Attachments — capped sizes with a MIME allowlist (see Limits). Each attachment requires a content_type.
  • Custom domains — send from your own domain with DKIM, SPF, and DMARC (see Domain Setup)
  • Tracking — open tracking and click tracking with engagement webhooks (see Engagement Tracking)
  • Suppression lists — automatic bounce and complaint handling plus self-service management (see Bounce and Complaint Handling)

Domain Setup

To send from your own domain, add the following DNS records in Channels > Email > Domains: Orbit validates your DNS — verification typically completes within minutes. The DKIM p= public key is extracted and bit-length-validated (minimum 1,024 bits per RFC 8301), so a placeholder record with no real signing key never flips to verified.

Verification fails or regresses

Verification is not one-shot: a daily health check re-validates your DNS and the interactive Verify DNS button (POST /api/v1/settings/channels/email/verify-dns) re-checks on demand. If a record drifts (TTL expired, host edited, key rotated), the failure is written to the domain’s dns-history and a bell notification flags the regression. Resolution: fix the record, re-run Verify DNS, and confirm the traffic-light DNS status (GET /api/v1/email/domains/:domainId/dns-status) is green before resuming sends. A 403 from the provider with “domain is not verified” is the same class — re-verify in Senders. Trigger a re-check and poll the per-record traffic-light view:
Each record grades valid | warning | invalid | unknown and the roll-up overallStatus is the worst-of:
When a record returns warning or invalid the same payload lists the fix under issues; an unknown verdict is a resolver timeout — re-poll after a few seconds.

Multiple domains and subdomains

You manage multiple sending domains through the Email/Senders surface (GET|POST|PUT|DELETE /api/v1/email/senders), each with its own verification state, and any verified domain is available in the compose dropdown. The id default always resolves to the platform orbit.devotel.io domain. Subdomains (e.g. mail.example.com) are registered as their own sender entries with their own DNS records.

Warm-up posture

New domains have no sender reputation — do not send marketing volume immediately, even though the platform will accept it. Ramp gradually and watch the aggregate soft-bounce and complaint rates on GET /api/v1/email/suppressions/reputation. If you plan dedicated-IP volume, the warmup ramp endpoints (GET /api/v1/email/warmup-plan and GET /api/v1/email/warmup-status) return a recommended daily cap schedule and today’s headroom, and GET /api/v1/email/dedicated-sending/eligibility returns the dedicated-IP eligibility check.

Limits

  • Send rate — 200 sends per minute per tenant on the email channel, with an idle per-tenant override surface (folder-shared pool; a request burst that trips the cap returns 429 RATE_LIMITED/channel-limited). Template saves cap at 60/min; send-test caps at its own bucket.
  • Attachments — up to 20 attachments per email, 25 MB total, 10 MB per file. The refinement runs against declared size values; oversize or over-count requests fail validation.
  • Attachment MIME allowlist — PDF, Word (doc/docx), Excel (xls/xlsx), PowerPoint (ppt/.pptx), text/csv, text/plain, application/zip, image/jpeg, image/png, image/gif, image/webp, common audio (mpeg, ogg, wav, aac, amr), and video/mp4/3gpp. image/svg+xml is explicitly blocked — SVG may embed scriptable XML, so it stays off the same way the file-upload service excludes it. Executables, scripts, and unknown MIME types are rejected. A blocked attachment fails with 422 VALIDATION_ERROR at the schema edge and never reaches the provider.
  • Body — either html or text must be non-empty; there is no byte cap on the body itself, but the 998-character caps on subject and each address field apply.
  • Recipient count per sendto/cc/bcc accept arrays within the same per-request rate limit; large recipient sets belong in the bulk/batch endpoint (POST /messages/bulk), which admits up to 10,000 recipients per call with per-row outcomes.

Engagement Tracking

Tracking is configured per sending domain, not per send — the email provider applies open/click tracking at the domain level. Passing metadata.track_opens or metadata.track_clicks in a send is deprecated and has no effect (accepted for backward compatibility; a deprecation warning is logged). On the platform’s own side, Orbit additionally mints a tenant-side open pixel and rewrites click-tracking links so the open/click signal hits Orbit’s edge directly regardless of the provider’s per-domain toggle.
  • Open tracking — a 1×1 transparent pixel <img> is injected just before </body> (or appended when the HTML has no closing body tag; the plaintext alt-body is never touched). The URL is HMAC-signed and bound to (tenant, message, timestamp). Machine-open classification (Apple Mail Privacy Protection, Gmail Image Proxy, bot prefetch) stamps is_machine_open so dashboards can distinguish inflated from genuine engagement; first-event-wins dedup keeps opened_at accurate against proxy fan-out.
  • Click tracking — tracked links are rewritten to an Orbit redirect endpoint; the original URL rides as an HMAC-verified payload. If a link can’t safely be rewritten, it ships untracked rather than failing the send. Click payloads carry link, ipAddress, userAgent, and the click’s own timestamp. email.clicked fans out on every distinct click, while email.opened records first-open-only.
Disable tracking at the domain (the provider dashboard links from the deliverability page point at the per-domain toggles).

Bounce and Complaint Handling

Delivery outcomes arrive on the channel-agnostic message events (message.delivered, message.failed with channel: "email"). A recipient-side bounce resolves to the terminal bounced status; the DSN classification drives the suppression lifecycle below. The diagnostic tree for decoding 5.x.x hard bounces vs 4.x.x soft vs complaints vs spam-traps lives in Troubleshooting: email bounces and spam complaints.

Automatic suppression lifecycle

Suppression reasons the platform writes automatically: Sends to a suppressed address flip to 422 RECIPIENT_OPTED_OUT at the pre-send gate, and the monthly quota slot the send consumed is refunded — you are never charged for a send we refused to hand to the provider.

Viewing and managing the list

The suppression list is self-service under Email > Suppressions (owner/admin). The aggregate reputation read is the one you poll while you ramp:
  • GET /api/v1/email/suppressions — page/search/filter by reason
  • GET /api/v1/email/suppressions/reputation — aggregate sent / delivered / bounced / complaint counts and rates, health tier and score, and current list size
  • GET /api/v1/email/suppressions/export — CSV download
  • POST /api/v1/email/suppressions — add a manual suppression (a recipient who asked by phone, a known-bad import, etc.)
  • POST /api/v1/email/suppressions/bulk-import — import a CSV or address list from a previous ESP
  • DELETE /api/v1/email/suppressions/:id — un-suppress an address
Removing a suppression makes the address mailable again — the platform does not hard-block re-send after removal, but a hard-bounce suppression that re-bounces re-suppresses, and re-sending to a complaint-suppressed address typically re-complains. Treat removal as a deliberate, auditable action (each add/remove writes an audit entry).

Error Catalog

Every API error returns the standard envelope { error: { code, message, status }, meta: { request_id, timestamp } }. The email send path can throw: Provider error passthrough. A 403 from the provider (for example, a sending domain that is not yet verified) reaches you as 403 VALIDATION_ERROR with a platform-safe message and the upstream detail preserved on details.provider_message. An attachment rejected over size/MIME fails 422 VALIDATION_ERROR locally and never reaches the provider. A Resend 429 carries status 429 end-to-end so burst traffic is treated as safe backpressure rather than a provider outage.

Webhook Events

Email engagement fires its own event types: Delivery outcomes are reported on the channel-agnostic message events, not on email-specific types. Subscribe to these to track delivery, bounces, and complaints: Each message event carries channel: "email" and the originating message_id, so you can filter for email traffic and correlate back to the send.

Troubleshooting

Map the symptom to the gate that fires it, then work the linked page:
Sender reputation decides which of these levers exists. Keep complaint rate near zero and prune hard bounces after the first return — the reputation endpoint’s health tier and score reflect both. The diagnostic tree is in Troubleshooting: email bounces and spam complaints.