# The verdict

A verdict is one JSON document per received message, with the schema name `sendscore.verdict/v1`. The MCP tools, the API, and `sendscore check --json` return the same document. Within a schema version a field is never renamed or given a new meaning; new fields may be added.

## An example

An example, trimmed to a few of its checks. DKIM passes, but the signature belongs to the provider's domain, so DMARC does not align, where it did on the previous issue.

```json
{
  "schema": "sendscore.verdict/v1",
  "id": "rpt_8f2k1q9d",
  "message": {
    "received_at": "2026-10-28T13:12:04Z",
    "from": "Daily Photo Tips <tips@dailyphototips.com>",
    "subject": "Shooting into the sun",
    "message_id": "<abc123@kit-mail.com>",
    "size_bytes": 48211
  },
  "verdict": "fail",
  "ready_to_send": false,
  "summary": "1 failure and 1 warning. DMARC does not align.",
  "top_fixes": [
    {
      "check": "auth.dmarc",
      "who": "you",
      "fix": "Set up DKIM with your own domain (dailyphototips.com) in your email platform, so messages are signed as you rather than as the platform."
    },
    {
      "check": "changes.regressions",
      "who": "you",
      "fix": "Something changed since the last issue. Check recent template, DNS, or platform changes."
    }
  ],
  "checks": [
    {
      "id": "auth.dkim",
      "category": "auth",
      "status": "pass",
      "title": "DKIM signature verified",
      "evidence": {
        "signatures": [
          { "d": "kit-mail.com", "s": "k1", "a": "rsa-sha256", "result": "pass", "aligned": false, "key_bits": 2048, "key_type": "rsa" }
        ]
      },
      "docs": "https://support.google.com/mail/answer/81126"
    },
    {
      "id": "auth.dmarc",
      "category": "auth",
      "status": "fail",
      "title": "DMARC does not align",
      "detail": "Neither a passing DKIM signature nor a passing SPF check uses the From domain dailyphototips.com.",
      "evidence": {
        "from_domain": "dailyphototips.com",
        "policy": "none",
        "spf_aligned": false,
        "dkim_aligned": false
      },
      "fix": {
        "who": "you",
        "text": "Set up DKIM with your own domain (dailyphototips.com) in your email platform, so messages are signed as you rather than as the platform."
      },
      "docs": "https://www.rfc-editor.org/rfc/rfc7489"
    },
    {
      "id": "content.links",
      "category": "content",
      "status": "skipped",
      "title": "Links not checked",
      "reason": "Link checking is off."
    },
    {
      "id": "changes.regressions",
      "category": "changes",
      "status": "warn",
      "title": "Worse than the previous issue: auth.dmarc (pass to fail)",
      "evidence": { "changes": ["auth.dmarc (pass to fail)"] },
      "fix": {
        "who": "you",
        "text": "Something changed since the last issue. Check recent template, DNS, or platform changes."
      }
    }
  ],
  "changes": [
    { "field": "auth.dmarc", "before": "pass", "after": "fail", "worsened": "auth.dmarc" },
    { "field": "dkim.d", "before": "dailyphototips.com", "after": "kit-mail.com" }
  ],
  "fingerprint": {
    "dkim_domains": ["kit-mail.com"],
    "dkim_selectors": ["k1._domainkey.kit-mail.com"],
    "return_path_domain": "bounce.kit-mail.com",
    "remote_ip": "203.0.113.10",
    "ptr": "mta10.kit-mail.com",
    "list_unsubscribe": true,
    "one_click": true,
    "link_hosts": ["dailyphototips.com", "kit-mail.com"],
    "html_bytes": 41022,
    "links": 14,
    "images": 3,
    "auth": { "dkim": "pass", "dmarc": "fail", "spf": "pass" }
  },
  "explanation": null,
  "note": "Receipt at SendScore does not prove inbox placement at Gmail or any other provider."
}
```

## Fields

| Field | What it holds |
| --- | --- |
| `schema` | Always `sendscore.verdict/v1` for this version. |
| `id` | The report's id, when the app stored it (`rpt_` and a token). The CLI leaves it out. |
| `message` | `received_at`, `from`, `subject`, `message_id`, and `size_bytes`. |
| `publication` | The newsletter it belongs to, when there is one. Left out otherwise. |
| `verdict` | `pass`, `warn`, or `fail`, for the message as a whole. |
| `ready_to_send` | Whether nothing in a blocking category failed, and DKIM, DMARC, and the unsubscribe signature could be evaluated. |
| `summary` | The counts of failures and warnings, and the titles of the first two failures (or warnings, when nothing failed). It is "Every check that ran passed." when nothing failed or warned. When authentication could not be evaluated, a sentence saying so comes first. |
| `top_fixes` | At most three fixes, each with the `check` it fixes, `who` fixes it, and the `fix`. |
| `checks` | Every check, with its `id`, `category`, `status`, and `title`. Each may add `detail`, `evidence`, a `fix` (with `who` and `text`), and a `docs` link; a skipped check adds `reason`. A list in `evidence` keeps its first 20 items, and when there were more, the full count is beside it under the same key with `_total` added. |
| `changes` | What changed since the previous issue. Always present, and empty when there is nothing to compare with. |
| `fingerprint` | What the comparison looks at, stored with every verdict so the next issue can be compared without keeping this message. |
| `explanation` | Room for an optional plain-language write-up. The engine never fills it, so it is `null`. |
| `note` | A reminder that receipt at SendScore does not prove inbox placement. |

Check categories are `auth`, `unsub`, `struct`, `content`, and `changes`. See [the checks](https://sendscore.ai/docs/checks/) for every id.

## The overall verdict

- `fail` if any check fails.
- Otherwise `warn` if any check warns, if `auth.dkim` or `auth.dmarc` was skipped, or if the check was stopped before it finished.
- Otherwise `pass`.

Skipped checks don't count otherwise, so a `pass` means every check that ran passed. The CLI's exit code follows the verdict, with `0` for `pass`, `1` for `warn`, and `2` for `fail`.

## ready_to_send

`ready_to_send` is `true` unless a check in a blocking category fails. The blocking categories are authentication (`auth`) and unsubscribe (`unsub`).

It is also `false` when `auth.dkim` or `auth.dmarc` is `skipped`, or when `unsub.signed` is `skipped` because the signature that decides it could not be verified. Then nothing shows the message is authenticated, so the verdict is at least `warn`. The same goes for a check that was stopped before it finished, whose summary is "The check was stopped before it finished, so this is not ready to send."

- When DNS lookups failed or the CLI ran with `--offline`, the summary starts with "Authentication could not be checked because DNS lookups failed, so this is not ready to send.", and the first fix is to run the check again once DNS lookups work.
- When nothing failed in DNS but a result was still hidden, the summary starts with "Authentication could not be fully checked, so this is not ready to send." This happens when a saved file or an upload has no envelope and its Return-Path domain aligns with the From domain, or when a DKIM signature expired after the message was received. There is no fix to run it again, since running it again would give the same answer. For a saved file, pass the envelope to [the CLI](https://sendscore.ai/docs/cli/#give-it-the-envelope).

So a message can have the verdict `fail` and still be ready to send, when the only failure is editorial, such as a broken link. An agent should read `ready_to_send` to decide whether to send, and `verdict` to decide whether to tell someone.

## top_fixes

At most three, in order. When `auth.dkim` or `auth.dmarc` was skipped because DNS lookups failed, the fix to run the check again comes first. After it come failures in a blocking category, then other failures, then warnings. A fix with the same text as one already chosen is left out. Each has the `check` it fixes, `who` fixes it (`you` or `provider`), and the `fix` itself.

## changes

Each change has the `field` that changed and its value `before` and `after`. Every change in an authentication result is listed, including a fix. When it made a check worse, `worsened` names the check, and the `changes.regressions` check warns. An authentication result that went from `pass` to `skipped` is a change with no `worsened`, since it was not checked rather than broken. The fields are `auth.spf`, `auth.dkim`, `auth.dmarc`, `dkim.d`, `dkim.selector` (with selectors in the same `selector._domainkey.domain` form as the fingerprint), `return_path.domain`, `sending.ip`, `sending.ptr`, `unsub.list_unsubscribe`, `unsub.one_click`, `links.hosts` (with the hosts removed as `before` and those added as `after`), `html.bytes`, `links.count`, and `images.count`.

## fingerprint

| Field | What it holds |
| --- | --- |
| `dkim_domains`, `dkim_selectors` | The `d=` domains of the DKIM signatures, and their selectors, each written as the full key name, such as `k1._domainkey.kit-mail.com` |
| `return_path_domain` | The envelope sender's domain |
| `remote_ip`, `ptr` | The sending IP and its verified reverse DNS name, when known |
| `list_unsubscribe`, `one_click` | Whether the message had `List-Unsubscribe`, and working one-click unsubscribe |
| `link_hosts` | The hosts the message links to |
| `html_bytes`, `links`, `images` | The HTML size, and the counts of links and images |
| `auth` | The `spf`, `dkim`, and `dmarc` statuses. The CLI leaves them out of the previous issue's fingerprint, so `diff` and `--previous` do not compare them |
