Email authoring and seed measurement
Writing the email and proving it lands are two different jobs, and Orbit keeps them in two different lenses. This page covers the authoring half — the drag-and-drop block builder and MJML compilation, the billed test-send you fire before launch, and the seed-lab measurement that answers where it actually landed. The pre-send rating half — “will carriers and filters score this content as risky, before anything sends?” — lives on the Deliverability Lab page. Author here; rate against scoring there; measure against seeds here again.1. Authoring: drag-and-drop blocks and MJML
The email builder consumes an ordered list of typed blocks —header, text, image, button, divider, spacer, columns, footer — plus an interactive amp-form / amp-carousel / amp-list set. Three endpoint surfaces serve the authoring loop under /api/v1/messages/email-builder/*:
- Render.
POST /api/v1/messages/email-builder/rendercompiles the block list into a complete HTML document: table-based layout with inline styles, the shape every major client (Gmail, Outlook, Apple Mail) renders consistently. - Render MJML.
POST /api/v1/messages/email-builder/mjml/renderaccepts MJML source (<mjml>…</mjml>) instead of blocks and compiles it to the same cross-client HTML. If you author MJML by hand or import it from another tool, compile through this endpoint; the compiler reports unsupported components as explicit errors rather than silently dropping them. - Save and list.
POST /api/v1/messages/email-builder/savepersists the definition;GET /api/v1/messages/email-builder/listpages through what you have saved.
multipart/alternative text/html part, and the AMP-for-Email renderer for interactive blocks. AMP-only blocks (forms, carousels, live lists) emit as the text/x-amp-html alternative part; non-AMP clients fall back transparently to the static HTML — exactly as a well-formed multipart email should degrade.
Multipart HTML/text, and the save-rate bucket
The builder writes a{ body, blocks } envelope on save. body is a plain-text summary extracted in canvas order from the text-bearing blocks (header, text, button, footer) — that is what the unified template list renders as the snippet and what a send-time plain-text fall-back can draw on. blocks re-hydrates the canvas when you reopen the template. The extraction is capped at 8,192 characters, matching the template-content ceiling, so a full template cannot overflow the canonical /messages/templates path either.
Re-saves run on their own per-tenant rate bucket (email-builder-save, 60/minute) so a save loop doesn’t fight the compose dialog’s preview traffic for the shared write budget. A re-save of the same name with no id updates the row in place; passing an id that collides with another template’s name returns 409 TEMPLATE_NAME_ALREADY_EXISTS instead of a raw error.
The URL and escape safety contract is the same in both renderers
Every URL a block or MJML tag can carry —logoUrl, href, src, an AMP form’s action endpoint — goes through one normalization predicate. It allows http:, https:, mailto:, tel:, sms:, protocol-relative //host, same-document #anchor, and a single {{merge_tag}} substituted at send time. Anything else — javascript:, data:, vbscript:, file:, strings with smuggling-prone characters — collapses to a safe # in rendered HTML. Legacy-saved ftp: URLs still render (the read side is forgiving), but new writes are rejected at the schema boundary (the write side is strict). This two-sided guard is why the render pipeline can escape every text value and every attribute without breaking older templates.
2. Test sends: billed, gated, webhook-visible
Before launch you want a real message to land in a real mailbox.POST /api/v1/messages/email-builder/send-test renders your blocks and sends them as a real paid send — not a synthetic row. That choice is deliberate: a preview that never left the platform wouldn’t let you verify rendering, and a preview that bypassed the billing gate would leak a real paid send with no wallet charge. The endpoint therefore runs the same safety chain as a production send:
- Billing. The wallet debit lands before the provider send (submit-based ordering) at the channel rate — fail closed on insufficient funds, and never a free pass via an “I’m just testing” claim. A client retry of the same logical send replays one idempotency key instead of double-charging.
- Recipient allow-list. On the platform’s shared email account the test recipient must be a verified org member; a per-tenant daily cap applies so a preview loop can’t burn shared-reputation sender status. Your own senders (BYO) skip the shared-account gate but the billing debit still runs.
- Audit row + webhooks. Each test send persists a
messagesrow (test_mode=true,status=test_sentorfailedon provider error) so the send appears in the dashboard Messages tab beside your live traffic. That row drives the normalMESSAGE_SENTwebhook fan-out, so anything listening for preview sends sees the same event shape you consume for real sends. The response is committed as soon as the provider verdict is known; the row persist and cache sweep happen after the reply returns, so a slow tail never holds the operator’s answer hostage.
isDefault row, or the channels-page verify-DNS source), falling back to the platform sender when nothing is set. A test send therefore exercises the same sender-resolution chain a real send would.
3. Seed-lab placement: the measured counterpart
A rate limit of “predicted” tells you what your history says; a seed lab answers what actually happened. Orbit’s seed surfaces work by registering your own test destinations and then scoring your real sends to them — the seed path never routes an outbound message; it only reads the receipts your normal sends generated. The SMS/RCS seed lab (campaigns route group) is the handset variant. Register a test handset withPOST /api/v1/campaigns/deliverability-seed-lab/recipients (E.164, a friendly label, channel, and optionally the expected operator MCCMNC / country); read it back with GET /api/v1/campaigns/deliverability-seed-lab; remove with DELETE …/recipients/:id. The roster lives in your organization settings (messaging_seed_recipients), so it survives under row-locked read-modify-write and can never double-submit.
For every registered handset the report aggregates your own terminal delivery receipts to that exact destination:
- delivery rate on a terminal-status denominator (delivered over terminal), so a pile of in-flight rows can’t flatten the rate;
- delivery latency (median, P95) from the timestamps the receipts carried;
- the observed operator by mode-most-common MCCMNC, joined to the shared operator catalogue for a human label;
- an inbound-reply corroboration — a reply from the handset is the strongest counter-signal to a delivery-receipt-that-says-delivered-but-never-arrived silent drop.
healthy ≥ 90 / watch ≥ 75 / poor below, untested when no terminal receipts yet) with a confidence level that is honest about sample size (low under 3 receipts, high at 20+). Operators roll up per observed MCCMNC so one bad seed row surfaces as a per-carrier verdict, not a per-handset anomaly. The campaign review surfaces this same shape: a low-confidence row tells you which step is missing data, not just how the number reads.
Email has its own seed surface (the email deliverability service): registered mailboxes seed a proof send, and the receiver’s captured headers plus parsed Authentication-Results classify where it landed — an authoritative folder header when the seed receiver reported one, Microsoft’s mailbox-delivery destination header as a strong signal, or a spam/bulk/inbox inference when neither was present. The authoring half above feeds this same surface: the test send renders your blocks, the receiver’s IMAP-aware seed captures the folder it landed in, and the placement report classifies it rather than predicting.
4. Boundary: authoring vs pre-send rating
Two lenses, two pages:
The boundary matters because a page about authoring should never promise a pre-send verdict, and a page about pre-send scoring should never promise proof-from-the-wire. Author and measure here; rate and predict there. The campaign launch chain wires both — the composer calls the rating lens before the review step admits a send — but document and operate them as separate lenses.
5. Worked sample: MJML to launch
A draft you want to verify before committing an audience:http:// link, the fix happens in the same composer — the authoring and rating lenses share the save surface so the rewrite is never a second save you forget to make.
See also
- Deliverability Lab: pre-send content-risk lint and per-carrier delivery prediction — the rating lens this page deliberately does not replace.
- Email delivery lifecycle: bounces, complaints, and the retry queue — what happens after the send leaves the platform.
- Outbound-send gating and quiet hours — the runtime per-message admission chain that fires after launch.
- Sender warming and reputation — the identity/reputation axis the authoring lens does not score.