Migration to high deliverability emails
Bring campaigns, OTP, and transactional mail up to current inbox hygiene — visible branding, a why-you-receive line, native double opt-in, and workflows that wait for confirmation.
Inbox placement is no longer "SPF + DKIM and hope". Gmail and Yahoo expect a recognizable sender and a sentence that explains why this specific person got this specific email — including one-time passwords and receipts. Lumail's editor scores those two things. This page is the migration path.
If you only need the confirmation switch, use Enable Double Opt-In. Come back here for branding, the why-line, OTP/transactional copy, and workflow timing.
What "migrated" looks like
Every email that leaves the organization — campaign, workflow, confirmation, OTP, receipt — has:
- Visible branding in the first screen of the body (logo, organization name, or your domain as its own first line).
- A why-you-receive sentence in the same language as the email. Not the unsubscribe line.
- A real postal address (
{{org-adresse}}/@org-adresse) and an unsubscribe link on marketing mail. - Native double opt-in on public signups, with a branded confirmation email.
- Workflows that enroll
SUBSCRIBEDcontacts, not pending ones.
The content checker awards 5 points for branding and 10 points for the why-line. The default "Don't want to receive these emails?" footer does not count as a why-line. See Content Deliverability Checker.
1. Add clear branding
Put the brand where a human sees it without scrolling. The checker accepts any of:
- A logo block with a real image URL (filename-only or a hidden link does not count)
- The organization name, including the
{{org-name}}/@org-namevariable - Your domain alone as the first text block (
melvynx.com)
Snippets are checked too. Put the header (logo + name) in a snippet and reuse it in campaigns, workflow emails, and the confirmation template so the brand cannot drift.
Do this on OTP and transactional HTML as well. A bare "Your code is 482191" from noreply@ with no logo and no product name looks like phishing. Same sender display name, same logo, same domain in the first lines.
2. Say why they received this email
Gmail/Yahoo want a causal sentence. Language does not matter. Shape does.
| Channel | Line that passes |
|---|---|
| Newsletter / campaign | You're receiving this email because you signed up at example.com |
| French list | Tu reçois ce mail car tu t'es inscrit sur example.com |
| Confirmation | You're receiving this because you just signed up and need to confirm your email |
| OTP / magic link | You're receiving this because you requested a sign-in code for example.com |
| Receipt / invoice | You're receiving this because you placed an order on example.com |
| Password reset | You're receiving this because a password reset was requested for your example.com account |
Write it in the email language. Put it near the footer, not only in preview text.
Does not count:
Don't want to receive these emails?Unsubscribealone- A legal address with no causal phrase
Agents writing campaign/workflow bodies must add this sentence and visible branding. Call get_skill({ type: "docs", page: "tutorials/migration-to-high-deliverability-emails" }) before rewriting a migrated list.
Transactional and SMTP sends (html, markdown, React Email) are not scored by the editor. Add the two lines yourself. Same rule as campaigns.
3. Turn on native double opt-in and brand the confirmation
Public capture pages, forms, and API signups should confirm the inbox before marketing starts.
UI: Settings → Configuration → Double opt-in → Edit confirmation email.
Agent (admin/owner):
get_organization_settings
update_organization_settings({ doubleOptInEnabled: true })
get_confirmation_email
update_confirmation_email({ subject, preview, template })
The confirmation body must include {{confirmUrl}} (button url or link href). Missing it is rejected. Use the same logo/name snippet and a why-line. Subject/preview should name the brand (Confirm your Acme subscription), not a generic "Confirm your email address" if you can do better.
What the switch does, and what it does not:
- New public signups become
PENDING_CONFIRMATIONand get one confirmation email (24h token). - Existing
SUBSCRIBEDcontacts stay subscribed. Enabling DOI is not a retroactive purge. - Confirmation emails send from the organization's default sender.
skipDoubleOptIn: trueis for trusted backends (checkout, server sync) only. Never on a public form.triggerWorkflows: falseskips the confirmation email. It is not a substitute forskipDoubleOptIn.- Do not build a custom confirmation state machine with the transactional API.
Full source table: Enable Double Opt-In.
4. Set workflows up for confirmation
With DOI on, tag-triggered and "subscriber created" workflows wait until the click. Tags attached at signup are replayed after confirm so the welcome sequence enrolls a real SUBSCRIBED contact.
Do this:
get_organization_settings— if DOI is off and the list is public, turn it on first.get_skill({ type: "workflow" })thenget_workflow(keepupdatedAt).configure_workflow_draftwith the full graph. EMAIL steps need complete inline content: branding, why-line,{{unsubscribeUrl}},{{org-adresse}}.- Do not publish until the user confirms.
publish_workflowis dangerous.
Do not:
- Enroll
PENDING_CONFIRMATIONcontacts as if they were live. - Put
skipDoubleOptInon a capture-page or client-side signup. - Publish
subscriber.tag.addedwith emptytagIds. - Call
create_campaignfor a workflow email.
Manual enrollment (add_subscribers_to_workflow) still requires a subscribed contact. dryRun: true first.
5. Other current recommendations
These are the other recent product rules that belong in the same migration, not a second project:
| Item | Do |
|---|---|
| Physical address | @org-adresse in the footer. CAN-SPAM / inbox filters. |
| Unsubscribe | {{unsubscribeUrl}}. Type / → Unsubscribe Link. Translate the label if the email is not English. |
| No shorteners | bit.ly, t.co, and friends fail the checker. Use your own domain. Lumail already tracks clicks. |
| Click domain | Settings → Click domain. Dedicated host for /r/<id> tracked links, with DNS verification. Shared tracking is the fallback. |
| Sending subdomain | Prefer mail.example.com over the root. Isolate reputation. |
| Content checker | Fix the eleven checks before send. Unspam rewrites trigger words. |
| Transactional list | Keep Add transactional recipients to the marketing list off unless you have consent. OTP/receipt recipients must not become SUBSCRIBED by accident. |
| Trusted imports | CSV import writes SUBSCRIBED and does not send confirmation. Clean the file first. |
| Footer snippet | One branded footer (why-line + address + unsubscribe) reused everywhere. Settings → Language does not rewrite snippet copy. |
DNS, bounces, and DMARC stay on Deliverability and Email Domains. This page is content and consent, not SPF.
Agent sequence
For a migration request ("make our emails high deliverability"):
get_skill({ type: "docs", page: "tutorials/migration-to-high-deliverability-emails" })get_writing_stylethenget_email_snippets— reuse the header/footer if they exist, otherwise create them.get_organization_settings→ enable DOI if public signups are on.- Brand
update_confirmation_email(must keep{{confirmUrl}}). - Rewrite campaign/workflow bodies with branding + why-line.
get_skill({ type: "campaign" })/get_skill({ type: "workflow" })first. - For OTP/transactional templates in the customer's app, give them the two sentences to paste.
send_emaildoes not invent them.
Members can read settings and the confirmation template. Only owners/admins can flip DOI or save the confirmation email.