---
title: "Readiness"
method: GET
path: "/readiness"
tags: ["monitoring"]
---

# Readiness

`GET /readiness`

## Request

- Base URL: `https://dnsdoctor.dev/api/v1`
- URL: `https://dnsdoctor.dev/api/v1/readiness`
- Auth: HTTP bearer

The DMARC enforcement-readiness verdict for one of the caller's domains.

Token required. `domain` must be a verified domain the token's account owns —
anything else is the same opaque 404 the rest of the surface gives, whether it
belongs to another account or to nobody (the gate is the SHARED
`api/monitor.py::require_owned_verified_row`, so this read and the dashboard's
own answer the ownership question from one definition). A malformed domain is
a 422 from `normalize_domain`, consistent with every other v1 path.

The body is the session-authed `/rua/domains/{id}/readiness` body, field for
field: the same `readiness_service.readiness_for_domain` computation and the
same `serialize_readiness` mapping, so the agent and the human can never be
told different things about the same window. `next_record` is
`ReadinessVerdict.next_record` VERBATIM — only ever `build_upgrade_record`
output, never composed here and never the LLM (the invariant) — and it is
null while blocked, which is an ANSWER: the blockers say why, and a caller
must never compose a stronger record to fill the gap.

Read-only, and it takes no plan gate: D42 read-only-degrade keeps a lapsed
account's own data readable. Lapsing stops scans, alerts and new RUA
ingestion; the verdict over already-ingested reports is not new ingestion.
The one live read it can perform is the bounded DMARC TXT fallback inside
`current_dmarc_record` (only when no stored scan carries the record), which is
a read of the caller's own published DNS.

## Query parameters

- `domain` string, required

## Response `200`

Successful Response

- ReadinessOut: The serialized `ReadinessVerdict` — `next_record` only when ready. D125/D130: `record_withheld` marks a ready `p=reject` step whose record the plan does not include; `withheld_reason` (`"plan"`) and `pricing_url` (a `/pricing` page, never a checkout) say why and where, null otherwise.
  - `ready` boolean, required
  - `current_step` string, nullable, required
  - `next_step` string, nullable, required
  - `next_record` string, nullable, required
  - `blockers` BlockerOut[], required
    - `source` string, required
    - `metric` string, required
    - `fix_hint` string, nullable, required
    - `ip` string, nullable
    - `senders` integer, nullable
    - `ips` string[], nullable
  - `window_days` integer, required
  - `total_messages` integer, required
  - `rejected_unknown` RejectedUnknownOut: D113 — unaligned unknown mail the published `p=reject` already rejects. Present only at the terminal rung, where the unknown-sources blocker is not emitted: the same D50 aggregate, read as the policy's receipt rather than as a reason to withhold a step that does not exist.
    - `sources` integer, required
    - `messages` integer, required
  - `progress` ProgressOut, required: The serialized D48 progress bar — the dashboard renders, never recomputes.
    - `progress_pct` number, required
    - `step_base_pct` integer, required
    - `evidence_fraction` number, required
    - `days_with_data` integer, required
    - `worst_source_aligned_pct` number, nullable, required
    - `unknown_volume_pct` number, required
    - `stage` string, required
    - `eta_days` integer, nullable
  - `enrollment` EnrollmentStatusOut, required: The four-state RUA enrollment verdict (design §3.4). Named `EnrollmentStatusOut` because `EnrollmentOut` is already the enrollment route's model (`rua/enroll.py`). Carries **no record field by construction**, so an LLM-shaped record string can never reach this surface (the invariant).
    - `state` 'not_published' | 'published_waiting' | 'published_silent' | 'receiving', required: Whether a monitored domain's owner has actually published our rua= address. `total_messages == 0` alone conflates three very different situations; this enum separates them so the dashboard can stop promising reports that can never arrive (design §3.2): - `NOT_PUBLISHED`: our address is absent from the live rua= list. - `PUBLISHED_WAITING`: present, no reports yet, < `silent_hours` since first seen. - `PUBLISHED_SILENT`: present, no reports yet, ≥ `silent_hours` since first seen. - `RECEIVING`: reports arrived in the window (decided by volume, so it can never contradict the readiness verdict derived from the same read).
    - `published_at` string, nullable, required
    - `checked_at` string, nullable, required
  - `evidence` DnsDoctorRuaReadinessOutEvidenceOut, required: What the verdict was computed from (D111) — counts and names, no prose. Additive: every readiness surface (dashboard, `GET /v1/readiness`, the `get_readiness` MCP tool) serializes through `serialize_readiness`, so this object appears on all three at once and can never differ between them. `concentrated` is a flag, not a gate — one reporter supplying nearly all the evidence is worth saying, and nothing is withheld for it.
    - `trusted_reports` integer, required
    - `untrusted_reports` integer, required
    - `reporters` string[], required
    - `dominant_report_share` number, nullable, required
    - `days_with_trusted_data` integer, required
    - `concentrated` boolean, required
  - `record_withheld` boolean
  - `withheld_reason` 'plan', nullable
  - `pricing_url` string, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/dnsdoctor/apis/dns-doctor-api.md) · [All operations](https://skmtc.dev/dnsdoctor/apis/dns-doctor-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dnsdoctor/apis/dns-doctor-api/revisions/1d0114806e76?raw)
