# Multi-record and email records (https://dodomain.io/docs/multi-record-and-email)

Sessions that need several DNS records (CNAME plus ownership proof, apex A/AAAA, SPF/DKIM/DMARC): what each connect path does and how email records verify.

`POST /api/v1/sessions` has always taken `records` as an array, and the guided-manual and Cloudflare one-click paths have always written and verified every record in it. What changed in September 2026 is the Domain Connect one-click path: it used to express exactly one record, and now applies whole record sets, provided the set matches one of the templates DoDomain publishes. This page is the honest map of what each path does with a multi-record session, and how email records are verified.

## The shapes that get one-click on Domain Connect [#the-shapes-that-get-one-click-on-domain-connect]

Domain Connect providers apply **pre-approved templates**, never arbitrary record lists. DoDomain publishes five (the JSON lives in [`docs/domain-connect/templates`](https://github.com/DevinoSolutions/dodomain/tree/main/docs/domain-connect/templates)); a session gets the one-click button when its records (in any order, every one of them) fill exactly one template:

| Template                                   | Records the session sends                                                                                                                                | Notes                                                                                                                                                                                                                                                                      |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `custom-subdomain-cname`                   | one `CNAME` at a non-apex host                                                                                                                           | served by Cloudflare, Glauca HexDNS and Vercel (apply proven on all three), and by Domain Chief, NameSilo, Simply.com and Danego DNS (apply not yet proven)                                                                                                                |
| `domain-verification`                      | one `TXT` at `_dodomain-challenge` whose value starts with `dodomain-verify=`                                                                            | served by the same seven; apply proven on Cloudflare and Glauca HexDNS, not yet proven on the other five                                                                                                                                                                   |
| `custom-subdomain-cname-with-verification` | the `CNAME` above **plus** the ownership `TXT` at `_dodomain-challenge` _under the same host_ (`_dodomain-challenge.app.acme.com` for `app.acme.com`)    | the "A/CNAME + TXT" pattern most custom-domain products use; served by Cloudflare, Domain Chief, Glauca HexDNS, NameSilo, Simply.com and Danego DNS (not Vercel); apply proven on Cloudflare (2026-09-26) and Glauca HexDNS (2026-10-22), not yet proven on the other four |
| `apex-a-with-verification`                 | one `A` at `@`, optionally one `AAAA` at `@`, plus the ownership `TXT` at `_dodomain-challenge`                                                          | the apex path, never a CNAME (see [Apex domains](https://dodomain.io/docs/apex-domains)); served by the same six as the row above, apply proven on Cloudflare (2026-09-26) and Glauca HexDNS (2026-10-23), not yet proven on the other four                                                        |
| `email-authentication`                     | any subset of: an SPF `TXT` at `@` with exactly one `include:`, one DKIM record at `<selector>._domainkey` (`CNAME` or `TXT`), a DMARC `TXT` at `_dmarc` | applied group by group; **never MX**; served by the same six, apply proven for every group on Cloudflare (2026-09-26) and Glauca HexDNS (2026-10-22), not yet proven on the other four                                                                                     |

A session that matches none of them still works: it takes the guided-manual path (every provider) or Cloudflare one-click (Cloudflare zones), both of which handle any combination of the five record types.

More templates are prepared for those sessions. No provider serves them yet, so today these sessions keep the paths above; the one-click button appears only where a provider serves the template a session needs. `email-sending` covers a full sending setup in one consent (SPF, up to three DKIM records, DMARC, and the MAIL FROM subdomain's MX and SPF), `website` covers up to four A and two AAAA addresses with `www`, a wildcard and the certificate challenge CNAME, and `site-verification` adds third-party ownership tokens (Google, Microsoft 365 and others). A universal template, `dns-records`, takes any other combination of the five record types; it is offered to DNS providers directly and is not in the public template registry.

<Callout title="Served is not proven">
  The three multi-record templates are in the Domain Connect template registry, and Cloudflare (to
  every customer since 2026-09-25), Domain Chief, Glauca HexDNS, NameSilo, Simply.com and Danego DNS
  serve them. Vercel serves only the two single-record templates. On Cloudflare all three were
  applied and verified for real on 2026-09-26, including the SPF merge into an existing SPF record;
  on Glauca HexDNS `custom-subdomain-cname-with-verification` and every `email-authentication` group
  were applied and verified on 2026-10-22, and `apex-a-with-verification` on 2026-10-23, so all five
  are proven there; on Domain Chief, NameSilo, Simply.com and Danego DNS they are served, apply not
  yet proven. The same goes for the two single-record templates on Domain Chief, NameSilo,
  Simply.com and Danego DNS, and for `domain-verification` on Vercel, where `custom-subdomain-cname`
  was proven on 2026-10-20. [State of Domain Connect](https://dodomain.io/state-of-domain-connect)
  measures this nightly. A provider only applies a template it has onboarded; on any provider that
  has not, a multi-record session on a Domain Connect zone shows the manual records and verifies
  them exactly like tier 3. The dashboard's **Domain preflight** tool probes each template against a
  zone's provider and reports onboarded / not onboarded / not probed per template, so you never have
  to guess.
</Callout>

## What each connect path does with several records [#what-each-connect-path-does-with-several-records]

| Path                              | Multi-record sessions                                                                                                                                                                                                                                                     |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cloudflare one-click (tier 1)     | Writes every record through the user's OAuth grant, reads each back from Cloudflare, and finalizes only when every one matches. Unchanged.                                                                                                                                |
| Domain Connect one-click (tier 2) | Compiles the whole set into one template and one provider approval; the provider writes every record; DoDomain then re-verifies **each record** against the zone's authoritative nameservers before the session is `verified`. A redirect back is never proof on its own. |
| Guided manual (tier 3)            | Lists every record with copy buttons, checks each one independently, and shows `found` / `not found yet` per record. A session is `verified` only when all records are found. Unchanged.                                                                                  |

Partial progress is reported honestly: after a check, the hosted page shows `2/3 found` and marks exactly which record is still missing, with what is actually published at that name when something is there but does not match.

## Email records: how verification reads them [#email-records-how-verification-reads-them]

Ownership tokens are compared byte for byte, case-sensitively; that is the whole point of a token. Email policies are not tokens, and comparing them byte for byte would fail the healthy outcome. Verification therefore reads the three email record types by their meaning:

- **SPF** (`TXT` at `@`, `v=spf1 …`). A zone may hold only **one** SPF record: two of them is a permanent error that fails every sender (RFC 7208 §3.2). So the correct result of "add `include:_spf.yourproduct.com`" is the zone's _existing_ SPF record now containing that mechanism, which is exactly what Domain Connect's `SPFM` merge does and what a user does by hand. Verification passes when every mechanism your session asked for is present in the published `v=spf1` record; the terminal `~all`/`-all` is the zone owner's choice and is not compared. One caveat for one-click applies: a provider's `SPFM` merge may rewrite that qualifier when it rebuilds the record (one provider has told us its merge always lands on `~all`), so if your product depends on a strict `-all`, read the published record back rather than assuming it survived the merge. When an SPF record exists but lacks your mechanism, the hosted page says so and tells the user to edit that record rather than add a second one. The page also warns when the published record makes more than 10 DNS lookups on its own terms (the RFC 7208 evaluation limit): a warning beside a found record, never a verification failure.
- **DKIM** (`CNAME` or `TXT` at `<selector>._domainkey`). A CNAME is compared like any hostname. A key `TXT` is compared tag by tag: `v` and `k` fall back to their RFC 6376 defaults when the provider omits them, and the key in `p=` is compared with whitespace removed, because long keys come back split across strings.
- **DMARC** (`TXT` at `_dmarc`). Compared tag by tag: the published record must be `v=DMARC1` and carry every tag your session asked for with the same value (`p=`, `rua=`, and so on); report addresses are compared as sets. Extra tags the zone owner adds are not a mismatch. A `_dmarc` record with a different policy is reported as a different value; waiting will not change it.

Request them the way you would document them for a customer:

```json
{
  "domain": "acme.com",
  "records": [
    { "type": "TXT", "host": "@", "value": "v=spf1 include:_spf.yourproduct.com ~all" },
    { "type": "CNAME", "host": "yp1._domainkey", "value": "yp1.dkim.yourproduct.com" },
    {
      "type": "TXT",
      "host": "_dmarc",
      "value": "v=DMARC1; p=none; rua=mailto:dmarc@yourproduct.com"
    }
  ]
}
```

<Callout type="warn" title="MX is deliberately manual">
  Sessions may include `MX` records and every path verifies them (exchange and priority), but the
  Domain Connect one-click path never writes one: a wrong MX write black-holes a domain's mail the
  moment it propagates, and a connect session knows nothing about the domain's existing mail
  routing. If your product receives mail on the customer's domain, the MX stays a manual record,
  verified against authoritative DNS like every other, and watched by the monitor afterwards.
</Callout>

## Constraints worth designing around [#constraints-worth-designing-around]

- **One `include:` per SPF session.** The Domain Connect template merges exactly one mechanism, so a session whose SPF value carries more (`ip4:`, `a`, a second `include:`) is entered by hand. Keep your SPF footprint to a single include host you control. The free [SPF record checker](https://dodomain.io/tools/spf-checker) parses a domain's record, walks its includes, and counts the DNS-querying terms against RFC 7208's limit of ten, worth running against a customer's domain before adding your include to it, since a record already at the limit will `permerror` the moment yours lands. [The SPF 10-lookup limit](https://dodomain.io/guides/spf-10-lookup-limit) lists what the includes common email vendors document actually cost, measured.
- **One DKIM selector per session.** Providers that rotate between two or three selectors (Microsoft 365, Amazon SES) need one session per selector for one-click, or the manual path for all of them at once.
- **Byte-exact prefixes.** For one-click, a DKIM `TXT` must start with `v=DKIM1; ` and a DMARC `TXT` with `v=DMARC1; ` (with the space): the template fixes that prefix and appends the rest. Other spellings still work on the manual and Cloudflare paths.
- **The ownership TXT sits under the host** for `custom-subdomain-cname-with-verification`: `host: "_dodomain-challenge"` on a session for `app.acme.com` verifies at `_dodomain-challenge.app.acme.com`. An ownership TXT at the zone apex pairs with `domain-verification` on its own.
- **Apex sessions never get a CNAME.** `apex-a-with-verification` writes `A`/`AAAA`; Cloudflare's Domain Connect implementation rejects the `APEXCNAME` record type outright, and the Cloudflare one-click (OAuth) path remains the more capable option on Cloudflare zones.

Nothing about the API shape changed for any of this: `records[]` is the same array it always was, and the SDKs are untouched.
