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

# Email authoring and the seed lab: compose, compile, then measure where it lands

> The arc from the email builder's drag-and-drop blocks and MJML source through test-send billing safety to the seed lab's measured placement — plus the boundary between this authoring lens and the campaign pre-send rating pages.

# 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](/concepts/deliverability-lab-pre-send-scoring) 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/render` compiles 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/render` accepts 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/save` persists the definition; `GET /api/v1/messages/email-builder/list` pages through what you have saved.

Every block type maps onto one of two renderers: the **static HTML renderer** for the `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 `messages` row (`test_mode=true`, `status=test_sent` or `failed` on provider error) so the send appears in the dashboard Messages tab beside your live traffic. That row drives the normal `MESSAGE_SENT` webhook 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.

The From address resolves the way production sends resolve it: your configured default sender (Settings → Email Senders `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 with `POST /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.

Each handset rolls up to a health verdict (`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:

| Surface                                  | What it answers                                                                                              | Where it lives                                                                                 |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| **Email authoring (this page)**          | Did I write the right thing, render it correctly, and see where it landed?                                   | The builder endpoints + the seed lab above                                                     |
| **Deliverability Lab (pre-send rating)** | Will carriers filter this *content*, and how will it land on my audience's carriers, before any send exists? | [/concepts/deliverability-lab-pre-send-scoring](/concepts/deliverability-lab-pre-send-scoring) |

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:

```bash theme={null}
# 1. Compile the MJML source (no dependency on a third-party runtime).
POST /api/v1/messages/email-builder/mjml/render
{
  "source": "<mjml><mj-head><mj-preview>50% off this weekend</mj-preview></mj-head><mj-body><mj-section><mj-column><mj-text>Hi {{contact.firstName}},</mj-text><mj-button href='https://your-bran.example/sale'>Shop the sale</mj-button></mj-column></mj-section></mj-body></mjml>"
}
# → { html, errors: [], previewText }

# 2. Save the block definition for editing (or skip and keep the source).
POST /api/v1/messages/email-builder/save
{ "name": "weekend-sale", "blocks": [/* rendered blocks */], "previewText": "50% off this weekend" }

# 3. Fire a billed test send to a verified member on your org.
POST /api/v1/messages/email-builder/send-test
{ "blocks": [...], "templateName": "weekend-sale", "to": "reviewer@your-domain.example" }
# → wallet debit lands before the send; a messages row and MESSAGE_SENT webhook fire.

# 4. Register a seed handset / mailbox and re-run the same probe.
POST /api/v1/campaigns/deliverability-seed-lab/recipients
{ "e164": "+15551234567", "label": "QA handset", "channel": "sms", "expected_mccmnc": "310260" }

# 5. Read the measured placement back.
GET /api/v1/campaigns/deliverability-seed-lab?window=30d
# → recipients[].health / recipients[].delivery_rate / recipients[].observed_carrier,
#    rolled up per operator. Then launch to the real audience.
```

If the pre-send rating flags a URL shortener or insecure `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](/concepts/deliverability-lab-pre-send-scoring) — the rating lens this page deliberately does not replace.
* [Email delivery lifecycle: bounces, complaints, and the retry queue](/concepts/email-delivery-lifecycle) — what happens after the send leaves the platform.
* [Outbound-send gating and quiet hours](/concepts/send-gating-and-quiet-hours) — the runtime per-message admission chain that fires after launch.
* [Sender warming and reputation](/concepts/sender-warming-and-reputation) — the identity/reputation axis the authoring lens does not score.
