Accept the cost of publishing, so the record is offered

Search Documentation

Search across all developer documentation

domain-auth

Accept the cost of publishing, so the record is offered

POST /v1/domain-auth/domains/{domain}/readiness/acknowledge
Auth required api.domain_auth.manage

Records that a person was shown what tightening this domain's DMARC policy will cost them, and agreed to it.

Some of a domain's failing mail is its own, broken on the way by a forwarder the customer does not control. That mail is not a fault they can fix, so it does not block the verdict — and precisely because nothing is judging it, nothing else would ever make them look at it. This endpoint is what makes it visible: a domain in the ready_pending_acknowledgement state has met every criterion and is offered no record until the cost is accepted.

There is no request body and no figure to send. The server takes the cost from the domain's current assessment — the same volumes, the same named senders and the same sentence readiness.cost.sentence returns — and freezes them onto the domain's timeline. A client-supplied number would record what the client chose to send rather than what the customer was shown, and the record would then say somebody accepted a cost they never saw.

It records a person, so a client-credentials token is refused, the same way decideDomainAuthSource refuses one. Re-authorize with a user-delegated token.

Only the pending state has anything to acknowledge. A domain with blocking criteria outstanding, one already ready, and one that would cost nothing to publish all return 422.

An acknowledgement covers ONE RUNG. Accepting a p=quarantine cost is not accepting a p=reject one — same mail, different consequence — so the next rung asks again. So does a cost that has since grown by more than half AND by at least 100 messages over the window.

Reversible in the only sense that matters: it publishes nothing and changes no DNS. It is classified non-destructive and a test-environment credential may call it. What it unlocks is a string for a person to paste; no endpoint on this API publishes DNS.

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 What was recorded, and the ladder as it stands once it counts application/json
acknowledgement object required

One recorded acceptance of a publishing cost, read back exactly as it was frozen. EVERY FIGURE HERE IS WHAT WAS ON THE SCREEN, not what is true now. The volumes move daily and this record must not: it exists to answer "what did they actually agree to?" from one row, a year later. sentence is the wording that was shown, verbatim — "will go to a spam folder" and "will be refused" are different agreements about the same mail, and only the wording tells them apart afterwards.

Fields of acknowledgement:

acknowledged_at string<date-time> required
actor_email string required

The person who gave it. There is always one: a client-credentials token is refused, because a machine cannot agree on somebody's behalf to lose some of their mail.

target_policy string enum required

The rung it was about. Accepting a quarantine cost is not accepting a reject one, so the next rung asks again.

One of: ``, none, quarantine, reject

relay_broken_messages integer<int64> required

The forwarding volume accepted, over window_days. Staleness is measured against this figure and no other.

relay_broken_sources integer required
relay_broken_label string required
reviewed_messages integer<int64> required
reviewed_sources integer required
sources array<string> required

The senders named on the screen, BY NAME rather than by id: a source group can be re-keyed, merged or dropped, and an id that resolves to nothing is worse than no id because it looks like an answer.

sources_truncated boolean required

True when more senders were shown than are listed here.

window_days integer required
sentence string required
readiness object required

The DMARC policy ladder for one domain.

Fields of readiness:

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
422 Query parameter or path value failed validation 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