DocumentationSearch docs
  1. 01Quick start
  2. 02The checks
  3. 03The verdict
  4. 04MCP setup
  5. 05Monitoring
  6. 06API
  7. 07CLI
  8. 08Self-hosting

API

The app serves a small JSON API, with one request for a test address, one that waits for the message, and one for a report. It serves them at your app’s SENDSCORE_BASE_URL, written here as https://sendscore.example.com.

Authentication

Every /api/v1/ request needs an API token. Sign in at your app’s /signin page with your email address. There is no password, because the app emails you a sign-in link. Then create a token on the Account page. It starts with ss_live_ and is shown once, so copy it then. The Account page lists each token’s prefix, when it was created, and when it was last used, and a token can be revoked there at any time.

Send the token in the Authorization header:

export SENDSCORE_TOKEN=ss_live_...
curl -s -H "Authorization: Bearer $SENDSCORE_TOKEN" https://sendscore.example.com/api/v1/reports/<report_id>

A missing, unknown, or revoked token answers 401. Each account sees only its own test addresses and reports, and asking for another account’s answers 404.

Get a test address

curl -s -X POST -H "Authorization: Bearer $SENDSCORE_TOKEN" \
  https://sendscore.example.com/api/v1/test-addresses

It answers 201 with:

Field What it holds
address Where to send the preview, t-<token>@ the mail domain
token The address’s token, for the wait request
expires_at When the address stops accepting mail, 24 hours after it was made
wait_url The wait request’s URL
page_url A page that shows the report when the message arrives

Wait for the message

curl -s -H "Authorization: Bearer $SENDSCORE_TOKEN" \
  "https://sendscore.example.com/api/v1/test-addresses/<token>/wait?timeout=30"

This long-polls until a message reaches the address or the timeout passes. timeout is in seconds, 30 by default and at most 50, so call it again to wait longer. It answers 200 either way:

{ "status": "received", "report_id": "rpt_...", "report_url": "https://sendscore.example.com/r/rpt_...", "verdict": { } }
{ "status": "not_received" }

verdict is the verdict JSON in full.

Waiting keeps working after the address stops accepting mail, for as long as its reports are kept. Once the address has expired with no matching message, the wait answers 410 at once, so get a new address. An unknown address token, one whose reports were deleted, or one made by another account answers 404. An account can hold 5 waits open at once, and another answers 429 until one finishes.

It returns the newest message at the address, even one that arrived before the call. To check a second send to the same address, for example after a fix, add the earlier report’s id as after, and only a newer message counts:

curl -s -H "Authorization: Bearer $SENDSCORE_TOKEN" \
  "https://sendscore.example.com/api/v1/test-addresses/<token>/wait?timeout=30&after=<report_id>"

An after that is not a report from this address answers 400.

Each message is checked for one test address. A message sent to two test addresses in one SMTP transaction is accepted for the first, and the second gets a 452, so the platform delivers it again separately.

The receiver also limits how much mail it takes, and answers with an SMTP code a platform understands.

Limit Reply past it
20 messages to one test address, over its life 550 at RCPT
1000 messages a UTC day to all of an account’s test addresses together 452 at RCPT, so the platform tries again later
10 messages in one SMTP connection 451 at MAIL, so the platform reconnects
50 connections open at once, and 5 from one sending IP 421

Read a report

curl -s -H "Authorization: Bearer $SENDSCORE_TOKEN" \
  https://sendscore.example.com/api/v1/reports/<report_id>

It answers with the verdict JSON, or 404 when there is no such report or it belongs to another account.

A report also has a page at its report_url, https://sendscore.example.com/r/<report_id>, and its JSON is at https://sendscore.example.com/r/<report_id>/json. Neither needs a token. The id is long and unguessable, so anyone with the link can read the report, and nobody without it can find it. The original message is shown only to the account that owns it, or for an anonymous test address or upload, to the browser that made it.

Daily limits

An account gets 500 test addresses a day, counted per UTC day, through the API, MCP, and the app’s pages while signed in, and its test addresses receive up to 1000 messages a day together. Past that, POST /api/v1/test-addresses answers 429, and the error says the allowance resets at midnight UTC. Self-hosting lists the settings that change them. Waiting and reading reports do not count. Visitors who are not signed in can get 5 test addresses a day per IP from the app’s home and upload pages, with uploads counted in the same 5.

Uploads

The app’s upload page checks a saved .eml file instead of a delivered message. An upload made while signed in belongs to the account, so GET /api/v1/reports/<report_id> and get_report return it. Its report says it came from an upload, and marks the sending IP and envelope sender as given by the uploader, since the app did not see the delivery. SPF and reverse DNS are checked only when the sending IP is given, and TLS is never checked for an upload.

Monitored newsletters

Monitoring gives each newsletter its own address. Two requests read them.

curl -s -H "Authorization: Bearer $SENDSCORE_TOKEN" https://sendscore.example.com/api/v1/publications

It answers {"publications": [...]}, one entry for each newsletter the account monitors.

Field What it holds
id The newsletter’s id, starting with pub_
name Its name
address Its monitoring address, m-<token>@ the mail domain
schedule days (such as ["monday", "thursday"]), time as HH:MM, time_zone, grace_minutes, and description, a sentence saying the schedule. It is null when no schedule is set.
alert_email Where alerts go
paused true when alerts are paused
page_url The newsletter’s page in the app
latest The latest issue, or null before the first one
last_alert The last alert sent, with kind (worse, missed, or duplicate), sent_at, subject, and report_id when it was about an issue. It is null before the first alert.
curl -s -H "Authorization: Bearer $SENDSCORE_TOKEN" https://sendscore.example.com/api/v1/publications/<id>

It answers with the same fields for one newsletter, plus reports, its 50 newest messages. Each one in reports, like latest, has these fields.

Field What it holds
report_id The report’s id, for GET /api/v1/reports/<report_id>
kind issue, or signup for a confirmation or welcome email, which is not compared
received_at When it arrived
subject Its subject
verdict The overall verdict, pass, warn, or fail
ready_to_send The verdict’s ready_to_send
changes How many changes it had from the issue before it
worsened true when one of those changes made a check worse
late true when it arrived after the schedule’s grace period
report_url The report’s page

latest is always an issue, never a signup message. An id that is not one of the account’s newsletters answers 404.

Errors

An error answers with a status code and {"error": "..."}, a sentence saying what went wrong.

How long things last

A test address accepts mail for 24 hours. Reports from test addresses and uploads are kept for 7 days, and a monitored newsletter’s for 365 days. The original of a message is kept for 30 days. To compare an issue with one from longer ago, save both as .eml files and use sendscore diff from the CLI. A message or upload can be at most 25 MB, and a larger one is refused.

From an agent

An agent that speaks MCP can use the MCP tools instead, which wrap these requests.