Did this change work? The same figures either side of it

Search Documentation

Search across all developer documentation

domain-auth

Did this change work? The same figures either side of it

GET /v1/domain-auth/domains/{domain}/timeline/{annotationId}/comparison
Auth required api.domain_auth.view

Recomputes the domain's figures over a window before and a window after one timeline moment: pass rate raw and adjusted with all three exclusion buckets, volume, the spoofing campaign, what publishing would have affected, what receivers did, and source counts by class and review state.

This is the only place that can answer "did the change work". Every other figure in the product is a rolling thirty days that mixes both sides of a change together.

Read delta_available before subtracting anything. It is false, and no_delta_reason says why in a sentence, in three cases — each of which is a refusal rather than a gap:

  • a side reported fewer than min_messages_per_side messages, so any difference is noise. "Too early to tell" and "too quiet" are different answers and the reason says which (after_incomplete distinguishes them);
  • another change happened inside one of the windows, so nothing here can be attributed to this one. attribution_declined is set and neighbours names the other changes. The figures are still real — what is withheld is the claim about cause;
  • both at once.

guard_band_days are excluded from the "after" side. Aggregate reports arrive 24–48 hours after the mail they describe, so the days immediately following a change carry mail sent and judged under the OLD policy. Including them would attribute the tail of the old policy to the new one.

truncations names any window report history could not fill, and by how many days. That is different from after_incomplete, which means the window has not finished yet — nothing was lost, the evidence has not arrived.

annotationId comes from a timeline event's sightings. Where both detectors saw the change, anchor_annotation_id names the one the windows were actually split on — the dns sighting wins, because its timestamp is a poll rather than a midnight report bucket.

The classification and the customer's decisions are as of now on both sides. A source reclassified last week would otherwise move between the columns and read as mail that changed hands, when what changed was our opinion of it.

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.

annotationId string<uuid> required

An annotation id from a timeline event's sightings.

Responses

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

200 The figures either side of the change application/json
annotation_id string<uuid> required

What was asked about.

anchor_annotation_id string<uuid> required

What the windows were actually split on. Differs from annotation_id whenever both detectors saw the change: the dns sighting wins, because its timestamp comes from a poll rather than a day bucket, and splitting on the coarser date would put up to a day of post-change mail into the "before" side.

anchor_origin string enum required

One of: dns, reports, user, system

occurred_at string<date-time> required

The anchor's — the instant the two windows meet.

kind string required
old_policy string required
new_policy string required
old_record string optional
new_record string optional
before object required

One window's figures.

Fields of before:

from string<date-time> required
to string<date-time> required
days integer required

The window's length. Shorter than window_days means it was truncated — read truncations.

measured boolean required

Whether this side cleared min_messages_per_side. False means no delta is offered.

volume integer<int64> required
pass_volume integer<int64> required
dkim_volume integer<int64> required
spf_volume integer<int64> required
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.

dispersion object | null optional

The spoofing campaign measured OVER THIS WINDOW, not the stored thirty-day figure (which spans both sides of any change and so can be attributed to neither). Null means the read failed — NOT that there was no campaign.

would_affect_messages integer<int64> required

What publishing a tighter policy would have stopped in this window: the failing volume of every contributing source, less the ones the customer has disowned.

enforcement object | null optional

What receivers did with the policy published DURING this window — the old policy before the change, the new one after. Null below quarantine, where every disposition is legitimately none and the figure would be a tautology.

sources_by_class object required

Registry rows that sent in this window, by effective class. Every class is present even at zero, so the two sides can be subtracted straight across — a missing key and a zero are the same fact and only one of them is legible.

sources_by_review_state object required
after object required

One window's figures.

Fields of after:

from string<date-time> required
to string<date-time> required
days integer required

The window's length. Shorter than window_days means it was truncated — read truncations.

measured boolean required

Whether this side cleared min_messages_per_side. False means no delta is offered.

volume integer<int64> required
pass_volume integer<int64> required
dkim_volume integer<int64> required
spf_volume integer<int64> required
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.

dispersion object | null optional

The spoofing campaign measured OVER THIS WINDOW, not the stored thirty-day figure (which spans both sides of any change and so can be attributed to neither). Null means the read failed — NOT that there was no campaign.

would_affect_messages integer<int64> required

What publishing a tighter policy would have stopped in this window: the failing volume of every contributing source, less the ones the customer has disowned.

enforcement object | null optional

What receivers did with the policy published DURING this window — the old policy before the change, the new one after. Null below quarantine, where every disposition is legitimately none and the figure would be a tautology.

sources_by_class object required

Registry rows that sent in this window, by effective class. Every class is present even at zero, so the two sides can be subtracted straight across — a missing key and a zero are the same fact and only one of them is legible.

sources_by_review_state object required
window_days integer required

How long each side was meant to be.

guard_band_days integer required

Days after the change excluded from the "after" side, because reports lag 24–48 hours and those days carry mail sent and judged under the OLD policy. Always stated rather than left to be inferred from the dates.

min_messages_per_side integer required
delta_available boolean required

Whether the two sides may be subtracted. The field to branch on — a consumer that subtracts regardless will be wrong on a real domain.

no_delta_reason string optional

Why not, in a sentence a customer can act on.

attribution_declined boolean required

Another change fell inside one of the windows. The figures are still real; what is withheld is the claim that this change caused them.

neighbours array<object> required

Each entry in neighbours:

annotation_id string<uuid> required
kind string required
origin string required
occurred_at string<date-time> required
old_policy string optional
new_policy string optional
side string enum required

One of: before, after

truncations array<object> required

Each entry in truncations:

side string enum required

One of: before, after

requested_from string<date-time> required
from string<date-time> required
lost_days integer required
reason string required

Prose a customer can act on.

after_incomplete boolean required

The "after" window has not finished yet. DISTINCT FROM A TRUNCATION: nothing was lost, the evidence has not arrived, and the difference is the whole of what a reader does next.

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