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

# Troubleshooting: email DNS verification drift and recovery

> Read the per-record DNS traffic light, fix the drifted DKIM/SPF/DMARC/Return-Path record, re-verify on demand, and resume sending — the full recovery loop for a 403 'domain not verified' or a yellow/red DNS status.

# Troubleshooting: email DNS verification drift and recovery

DNS verification is not a one-time check. The platform re-validates the
SPF, DKIM, and DMARC records behind a sending domain on a daily health
check, and every interactive re-check folds into the same verdict. When a
record drifts — a TTL expires into a changed answer, someone edits the
zone at your registrar, or a key rotates — the domain regresses: the
traffic light goes yellow or red, a bell notification flags the
regression, and sends from that domain start failing at the provider with
a 403 "domain is not verified" rejection. This page is the recovery
procedure.

If the bounce or complaint pages described your symptom instead
(recipient-side rejects after the provider accepted the send), work the
[email bounces and spam complaints](/troubleshooting/email-bounces-complaints)
page — this page covers only the sender-side DNS-verification failure
class.

## Where you see the drift

* **Email dashboard** — the DNS status widget shows one traffic-light
  chip per record type (SPF, DKIM, DMARC) for the platform-default domain
  and your custom domain, updated about once a minute. Green means the
  record resolves to the expected value; yellow is a partial or
  inconclusive verdict; red is a confirmed failure.
* **Send path** — a send through a regressed domain fails with a provider
  403: "domain is not verified." The Delivery Log row stays on your side
  of the provider handoff.
* **Bell notification** — when the daily re-check or an interactive
  re-check sees a previously-green record flip to a confirmed failure, the
  platform writes the regression to the domain's history and raises a
  notification. A resolver blip (SERVFAIL, timeout) never fires the bell —
  only confirmed failures become regressions.

## Step 1 — Read the traffic light

`GET /api/v1/email/domains/:domainId/dns-status` returns the per-record
verdict for one sending domain. Requires an owner or admin role.

```bash theme={null}
curl "https://orbit.devotel.io/api/v1/email/domains/${domain}/dns-status" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

Reading the response:

* `data.overallStatus` is the worst of the per-record verdicts:
  `valid` (green, all records pass), `warning` (yellow, partial or
  inconclusive), `invalid` (red, at least one confirmed failure), or
  `unknown` (not checked yet).
* `data.records[]` carries one entry per record with `kind`
  (`spf` / `dkim` / `dmarc`, plus `mx` and `bimi` where configured),
  `status`, the DNS value the platform saw (`recordActual`), and the value
  it expected (`recordExpected`) — compare the two to find the drift.
* For DKIM, each selector appears as its own entry with a `selector`
  field; a single failing selector turns the whole domain red.
* `data.providerSendability` (present when the provider was consulted on
  this read) tells you whether the email provider actually considers the
  domain registered. An all-green DNS grid with a "not registered"
  provider verdict still 403s — that case is Step 3 below.
* Pass `?refresh=true` to force a live re-check instead of the cached
  verdict; the interactive **Verify DNS / Re-check now** action in the
  dashboard uses the same flag and also pokes the provider to re-verify.

### Diagnose per record

Every red or yellow record maps to one fix:

| Record            | What typically drifted                                                                                                                   | Fix                                                                                                                                                |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| DKIM TXT          | Public key replaced with a placeholder, selector renamed, or `p=` now under 1,024 bits (RFC 8301 floor)                                  | Republish the full `p=` key shown under **Channels > Email > Domains**. If you rotated keys, keep the old selector live until the new one verifies |
| SPF TXT           | The `include:` clause pointing at the platform was dropped while adding another sender, or a second SPF record appeared on the same name | Keep exactly one SPF record per name and fold the platform's `include:` back in. Revert an un-flattened merge if two records resolve               |
| DMARC TXT         | Policy `_dmarc` record deleted or moved during a zone cleanup                                                                            | Republish the `p=` policy at `_dmarc.<your-domain>` exactly as the dashboard shows it                                                              |
| Return-Path CNAME | CNAME removed, or the bounce host changed provider-side                                                                                  | Repoint the record at the expected bounce host shown in the expected value                                                                         |

Two honest noise cases to rule out before you edit anything:

* **Yellow with an empty actual value** usually means DNS has not
  propagated yet. A newly published record caches at the resolver for its
  TTL; wait it out and re-poll rather than re-editing.
* **A status that flips red for minutes then recovers on its own** is a
  resolver blip, not drift — those verdicts are deliberately excluded from
  the regression bell and history trend, and need no action.

## Step 2 — Check the history before you edit

`GET /api/v1/email/domains/:domainId/dns-history?days=30` returns
timestamped snapshots of the record verdicts over the window (default 30
days, up to 90). Use it to answer two questions:

1. **When did it regress?** The first red snapshot after a green stretch
   lines up with a zone edit on your side — match the timestamp against
   your registrar change log before you revert the wrong record.
2. **Did it recover?** After a fix, this log is how you confirm the
   verdict flipped back once the TTL expired; a green live status with a
   still-red recent history means the fix published but the cached
   verdict has not rolled yet.

## Step 3 — Re-verify on demand

The daily health check catches drift eventually, but you never need to
wait for it. Run an interactive re-check after every DNS edit:

```bash theme={null}
curl -X POST "https://orbit.devotel.io/api/v1/settings/channels/email/verify-dns" \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "mail.example.com"}'
```

The same **Verify DNS / Re-check now** button lives under
**Channels > Email > Domains** and on the per-domain page. This does
three things in one call: re-resolves every record live, re-checks the
domain with the email provider (this is what clears the 403 when the
provider never registered an otherwise-green domain), and stamps the
verified state so the send path accepts the domain again.

<Note>
  Re-running verification against an unchanged, still-broken zone is a
  no-op loop. Confirm `dns-status` is green (or at minimum that
  `recordActual` now matches `recordExpected`) before you expect the 403
  to clear.
</Note>

## Step 4 — Watch the warm-up posture while the domain is down

While a domain is regressed and refusing sends, two things change under
you:

* Any dedicated-IP warm-up loses its anchor — the ramp is anchored to the
  first recorded send, and suppressed sends do not advance it. Read
  `GET /api/v1/email/warmup-status` after the regression clears: it tells
  you today's recommended cap, how much headroom is left today, and
  whether the reputation feedback loop is holding your ceiling down.
* If you queued a campaign into a regressed domain, those rows sat in the
  queue rather than bouncing — drain them after re-verification instead
  of re-sending duplicates (see
  [message stuck in queued](/reference/troubleshooting) for the queue-side
  diagnosis).

If your send burns through the post-recovery ramp cap the same day,
`GET /api/v1/email/warmup-enforcement` answers **allow / partial /
block** for the batch size — a `block` there is the ramp doing its job,
not a new fault.

## Step 5 — Resume sending

A domain is back to fully operational when all three hold:

1. `GET /api/v1/email/domains/:domainId/dns-status` returns
   `overallStatus: "valid"` with no red or yellow record.
2. `providerSendability` on the same response reports the domain as
   registered with the provider (or the interactive verify already ran
   end-to-end).
3. `GET /api/v1/email/domains/:domainId/dns-history` shows the recent
   snapshots green past the record TTL.

Send one test message through the domain, confirm it lands
(`delivered` in the Delivery Log), then resume the paused campaign or
queued traffic. The regression bell clears on its own once the daily
health check sees green again.

## Recovery checklist

1. Read `dns-status` for the domain; note which `kind` is red or yellow.
2. Match `recordActual` against `recordExpected` for that record; the
   mismatch is the drift.
3. Check `dns-history` for the regression timestamp; line it up with
   your registrar change log.
4. Fix the record at your registrar (single SPF, full DKIM `p=` key,
   DMARC policy in place, Return-Path CNAME intact).
5. Run `POST /api/v1/settings/channels/email/verify-dns` (or the
   dashboard Verify DNS button) and confirm the provider verdict.
6. Re-poll `dns-status`; confirm green, then confirm `dns-history` rolled
   past the TTL window.
7. Check `warmup-status` before resuming volume past the day's cap.
8. Resume queued congestion control (not bulk re-sends — drain the queue).

## When to escalate

Escalate when the record you published is demonstrably correct but the
verdict never moves, or when the provider disagreement will not clear:

* The DNS zone publicly resolves the exact expected value (`dig TXT <name>` matches `recordExpected`), yet `dns-status` keeps reporting
  yellow or red past a full TTL cycle.
* `providerSendability` still reports "not registered" after an
  interactive verify on an all-green grid.
* Every record reads `unknown` — that points at the platform's check
  path, not your zone.

Include these so support traces the domain without a back-and-forth:

* Your **tenant ID** (Settings → Organization in the dashboard; also
  returned as `organizationId` by `GET /api/v1/me`).
* The **sending domain** affected.
* The **record kind and the expected/actual values** the `dns-status`
  response returned for the failing record.
* The **timestamp of the first red snapshot** from `dns-history`.

## See also

* [Email channel guide](/channels/email) — domain setup, the verification
  lifecycle, and webhook events
* [Email bounces and spam complaints](/troubleshooting/email-bounces-complaints) —
  the recipient-side failure class that survives a green DNS grid
* [Message stuck in queued](/reference/troubleshooting) — drain queued
  email rows without re-sending duplicates
* [Email endpoints reference](/api-reference/endpoints/email) — request
  and response shapes for `dns-status`, `dns-history`, and `warmup-status`
