Get the DMARC policy ladder for a domain

Search Documentation

Search across all developer documentation

domain-auth

Get the DMARC policy ladder for a domain

GET /v1/domain-auth/domains/{domain}/readiness
Auth required api.domain_auth.view

Assesses the domain against the policy rung above its current one, with every criterion carrying the value actually measured beside its threshold — met: false comes with "pass rate 97.2%, needs 98%", which is a to-do list, rather than a bare cross.

record_to_publish is populated ONLY when every criterion is met. It is a string to hand a person, not an action. SendOps does not hold your DNS credentials and no endpoint here publishes DNS. Advancing a policy on incomplete evidence starts discarding legitimate mail at every receiver on the internet, and the failure is invisible from our side: the policy tightens, the pass rate holds, and the mail stops arriving.

policy_history_unavailable says a false regressed means "we do not know" rather than "no". Do not report a domain as un-regressed while it is true.

Path parameters

domain string required

The domain, by NAME (acme.com). A UUID is also accepted. Matched case-insensitively, and scoped to the calling org — a domain belonging to another org is a 404, never a 403.

Responses

Errors follow the RFC 7807 problem format — see the error reference.

200 The policy ladder and its blocking criteria application/json
current_policy string enum required

One of: no_record, none, quarantine, reject

target_policy string enum required

One of: no_record, none, quarantine, reject

ready boolean required

True only when every BLOCKING criterion is met and no cost the verdict stopped blocking on is outstanding.

ready_pending_acknowledgement boolean required

Every criterion is met and a cost the verdict stopped blocking on has not been accepted. A DISTINCT STATE, not a seventh unmet criterion: the domain has done everything it can do, and what is outstanding is a sentence to agree to. ready stays false and record_to_publish is empty; cost.sentence is what is owed.

cost object | null optional

What publishing would actually cost, or null when it would cost nothing. Read it beside record_to_publish.

record_diff array<object> | null optional

What publishing record_to_publish would change about the record live in DNS now, field by field. Null or empty when nothing is published — that domain is publishing its first record, not changing several fields — or when nothing differs.

Each entry in record_diff:

tag string required

The DMARC tag name, lowercased — p, sp, rua, and so on.

kind string enum required

One of: added, changed, removed

from string required

The published value. Empty for an added tag.

to string required

The recommended value. Empty for a removed tag.

note string required

One sentence on what this tag does and what the change means.

ttl_advice object | null optional

The step to take BEFORE publishing. Present alongside record_to_publish.

at_strongest boolean required

Already at p=reject; there is no rung above.

needs_record boolean required

No DMARC record is published at all

awaiting_first_report boolean required
regressed boolean required

The domain used to enforce a stronger policy than it does now — usually a DNS edit nobody meant to make.

regressed_from string enum optional

One of: none, quarantine, reject

policy_history_unavailable boolean required

A false regressed means "unknown" rather than "no" while this is true.

criteria array<object> required

Each entry in criteria:

key string enum required

no_unreviewed_sources was RETIRED and is no longer emitted. Its job is absorbed by acceptable_impact, where an unreviewed sender blocks by contributing impact rather than by existing.

One of: coverage_days, message_volume, distinct_providers, acceptable_impact, pass_rate, no_unaligned_sending, time_at_current_policy, no_new_unknown_sources

label string required
met boolean required
actual string required

The value actually measured, populated whether or not the criterion is met — "97.2%" beside a required "98%" is a to-do list; a bare false is not.

required string required
detail string optional
blocking boolean required

Whether failing this criterion withholds the rung. pass_rate is the only one where it is false: it explains the impact figure rather than gating on it, so a domain can be ready with pass_rate.met false. Never list a non-blocking criterion as a reason a domain is not ready.

pass_rate object required

What the pass_rate criterion was actually measured over. Populated at p=reject too, where there is no criterion to judge but the rate is still real. A distributed spoofing campaign sits in the denominator of the reported pass rate, so the domain most in need of enforcement shows the worst readiness and the ladder blocks on a number its owner cannot move. When a campaign is identified the rate is recomputed over the mail that could have authenticated, and campaign_excluded says so. THIS IS NOT LENIENCY. The excluded mail is mail you never sent, from addresses that authenticated as nobody, and tightening the policy is exactly what stops it being delivered. There is nothing to confirm, reject or publish about it. A second bucket comes out for a different reason: failing mail from sources you have explicitly REJECTED. You have said that mail is not yours, the impact preview already leaves it out on those grounds, and leaving it in the rate had two halves of one answer disagreeing about whose mail it is. It is reported separately from the campaign because "somebody is forging your domain" and "you told us this sender is not yours" are different facts, and only the second is a decision you can revisit. A THIRD BUCKET COMES OUT, AND IT IS THE ONLY ONE THAT IS A COST RATHER THAN A REASON TO PUBLISH: your own mail, forwarded onward by a forwarder we can name and re-sent with a broken signature. Recipients choose to forward and you do not control that path, so it is not evidence of a fault you can fix and it does not decide the verdict. It stays in impact.would_affect_messages at full weight, because at p=quarantine that mail really is quarantined at receivers that do not override — and publishing past it requires a recorded acknowledgement. Read the *_excluded flag beside each bucket before comparing adjusted against a pass rate from any other endpoint. A count may be non-zero while its flag is false — a bucket identified but not subtracted is named and not applied, and both rates are then the same number.

Fields of pass_rate:

raw number<double> required

The pass rate over everything reported, in [0,1]. Every other endpoint reports this one.

adjusted number<double> required

The rate the criterion was judged on, in [0,1]. Equal to raw when nothing was excluded.

messages integer<int64> required
authenticable_messages integer<int64> required

messages less the excluded campaign — the denominator adjusted was computed over. message_volume is judged on messages, not on this: a campaign never withdraws a rung the domain earned.

excluded_messages integer<int64> required
excluded_sources integer<int64> required

How many distinct addresses the excluded messages came from.

campaign_excluded boolean required

Whether the campaign adjustment was applied. Not implied by excluded_messages being non-zero.

disowned_messages integer<int64> required

Failing mail from sources you have rejected, with anything already counted in excluded_messages removed — the two overlap, and the campaign figure is a domain-level aggregate rather than a sum over source rows, so subtracting both whole would count the overlap twice.

disowned_sources integer<int64> required

How many rejected sources contributed disowned_messages.

disowned_excluded boolean required

Whether the disowned adjustment was applied. Not implied by disowned_messages being non-zero.

relay_broken_messages integer<int64> required

Your own mail, broken on the way through a forwarder we can name. Out of this rate, IN impact.would_affect_messages at full weight: a cost of publishing, not a reason to publish.

relay_broken_sources integer<int64> required

How many recognised forwarders contributed relay_broken_messages.

relay_broken_label string required

The largest contributing forwarder by failures, so copy can name it. Empty when no name was available.

relay_broken_excluded boolean required

Whether the relay adjustment was applied. Not implied by relay_broken_messages being non-zero.

top_failure_contributor object | null optional

Null when no single source owns most of the domain's failures, which is the common case.

impact object | null optional

What publishing target_policy would have done to the last thirty days of mail. Null for a domain already at p=reject, one awaiting its first report, and one with no record at all.

enforcement object | null optional

What receivers actually DID with the published policy. Present only at p=quarantine or p=reject, and null when no failing mail was reported.

recommendation object | null optional

What to do now: the record, the diff, the TTL step, the watch window, the way back, and copy specific to the moment. Null for a domain at the strongest rung and one awaiting its first report, where there is nothing to recommend. PREFER IT OVER record_to_publish, which it supersedes. In particular, a domain that has just finished a watch window badly has no record_to_publish at all and a rollback_record that is the most useful string on the payload.

record_to_publish string optional

Present only when ready. A record for YOU to publish — SendOps holds no DNS credentials and nothing on this API writes DNS. A COMPATIBILITY FIELD, always equal to recommendation.record. A bare record is still the right answer to "what do I paste"; it is the wrong answer to every other question the recommendation answers.

recipient_impact string optional
next_action object | null optional

The single largest lever between this domain and target_policy, or null (SND-1398). ONE, NEVER A LIST, and null is the common answer. criteria is already the list; what it cannot say is which of them costs the most and which row in sources to go and open. Quote this before the criteria when it is present, and do not manufacture one from the criteria when it is not — a weak next action is worse than none.

alignment_breadth object optional

How much of this domain's PASSING mail passes on BOTH mechanisms rather than on one (SND-1383). A domain at 99% whose passes are almost all SPF-only is one forward away from failing: SPF is the leg forwarding destroys, and a recipient setting up a forward is not something the sender can prevent or even see. A domain at 99% on both mechanisms is solid. The pass rate is identical for both. NOT A CRITERION AND NEVER BLOCKING. A fragility warning, not a fault — the remedy is a DKIM key at a third party on their timetable, and a customer forwarded to by strangers cannot make forwarding stop. It is reported beside the verdict and is no part of it.

Fields of alignment_breadth:

measured boolean required

False when the window carried no passing mail. Render nothing rather than "0% on both mechanisms", which reads as a finding about a domain nobody has heard from.

both_messages integer<int64> required

Aligned on DKIM and SPF — the only mail that survives both a forward (which breaks SPF) and a reseller rewriting the body (which breaks DKIM).

dkim_only_messages integer<int64> required

Aligned on DKIM alone. Normal for forwarded mail, and the durable half: a DKIM signature travels with the message.

spf_only_messages integer<int64> required

Aligned on SPF alone — the fragile population. The moment anybody forwards it, the envelope sender changes and it fails.

passing_messages integer<int64> required

The denominator: mail that passed on at least one mechanism. The three counts above are DISJOINT and sum to it. Failing mail is the pass rate's business and is not here.

both_share number<double> required
dkim_only_share number<double> required
spf_only_share number<double> required
fragile boolean required

True when enough of the pass rate rests on SPF alone to be worth saying so. It gates the sentence, never a verdict.

suite_signing_as_itself string required

The mailbox suite signing with its own domain instead of the customer's, when a source carries that diagnosis; empty otherwise. The same fact as the per-source suite_signing_as_itself finding at domain altitude — and the half with a fix attached.

sentence string required

What "aligns only on SPF" means, in the same words that use the phrase. Empty when there is nothing worth saying.

summary string required
401 Missing, malformed, or unknown API key application/problem+json
403 Key lacks the required scope or plan limit violated application/problem+json
404 Resource not found application/problem+json
429 Per-org rate limit exceeded application/problem+json
500 Unexpected server-side failure. The code is internal_error. The request_id field can be quoted to SendOps support to investigate. application/problem+json