Skip to content

API reference/monitors

Monitor a list on a schedule

POST/api/v1/email/monitors

authentication
Secret API key as a bearer token
billing
Creation is free. Each run bills every address like a single lookup — 1 credit for a fresh deliverable or risky verdict whose mailbox check was definite; unverified answers, suppressed addresses and 7-day repeats are free, and undeliverable verdicts are free within your email fair use and 1 credit each past it.

Saves up to 500 addresses and re-verifies them every week or every month. The first run starts right away and only sets the baseline; from the next run on, every address that was deliverable last time and no longer is counts as decayed and is reported to the account's email. Runs honor the suppression list and the 7-day repeat cache, so a stable list costs little to keep watching.

Answers 201 with the monitor. The baseline run is queued, not finished: poll the monitor for last_run_at and last_summary.

With a webhook_url, a finished run is POSTed there as { "event": "email_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as last_summary … }, "changes": [ … ] }, where changes names the addresses the alert email names. The X-Spaw-Signature-V2 header carries t={unix seconds},v2={lowercase hex}, where the hex is the HMAC-SHA256 of v2:{t}:{raw body} keyed with webhook_secret, which this response returns once; a receiver should refuse anything whose t is more than five minutes from its own clock, since a signature over the body alone would let a captured delivery be replayed at it forever. The original X-Spaw-Signature, that HMAC of the body alone, is sent beside it until 1 March 2027. Connection errors and 5xx answers are retried twice with a short backoff, three attempts in all, and the outcome is readable afterwards as webhook_status and webhook_detail. A URL that resolves onto a private network is never posted to.

Request body

field type description
namerequiredstringA label for the dashboard and the decay alerts. At most 100 characters.
emailsrequiredstring[]1 to 500 addresses; each item at most 254 characters.
cadencerequiredstringHow often the list is re-verified. One of: weekly, monthly.
webhook_urlstring | nullAn https URL every reported run is POSTed to. The signing secret is returned once, in this response, and never again. At most 2,048 characters.
webhook_eventsstringWhen to post. `changes` posts only when the run found something new — decay, a status change, a newly flagged entry — or stopped early; `every_run` posts after every run, which also says the monitor is still running. One of: changes, every_run. Default: changes.

Example request

curl -X POST https://spaw.co/api/v1/email/monitors \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Newsletter list",
  "emails": [
    "[email protected]",
    "[email protected]"
  ],
  "cadence": "weekly"
}'
const response = await fetch('https://spaw.co/api/v1/email/monitors', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_…',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Newsletter list",
    "emails": [
      "[email protected]",
      "[email protected]"
    ],
    "cadence": "weekly"
  }),
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/email/monitors',
    headers={'Authorization': 'Bearer sk_live_…'},
    json={
        'name': 'Newsletter list',
        'emails': [
            '[email protected]',
            '[email protected]'
        ],
        'cadence': 'weekly'
    },
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/email/monitors');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer sk_live_…', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'name' => 'Newsletter list',
        'emails' => [
            '[email protected]',
            '[email protected]'
        ],
        'cadence' => 'weekly'
    ]),
]);
$result = json_decode(curl_exec($ch), true);
from spaw import Client

client = Client('sk_live_…')
result = client.create_monitor(name='Newsletter list', emails=[
    '[email protected]',
    '[email protected]'
], cadence='weekly')
import Spaw from 'spaw';

const spaw = new Spaw({ apiKey: 'sk_live_…' });
const result = await spaw.createMonitor({
    name: 'Newsletter list',
    emails: [
        '[email protected]',
        '[email protected]'
    ],
    cadence: 'weekly'
});
use Spaw\Client;

$spaw = new Client('sk_live_…');
$result = $spaw->createMonitor(name: 'Newsletter list', emails: [
    '[email protected]',
    '[email protected]'
], cadence: 'weekly');

This endpoint has no console on its page. It writes to your account. A documentation page can show you the request; making the change is for the dashboard or for a call you make yourself.

Responses

201The monitor was saved and its baseline run queued.

{
    "success": true,
    "data": {
        "monitor": {
            "id": 41,
            "name": "Newsletter list",
            "cadence": "weekly",
            "email_count": 2,
            "next_run_at": "2026-09-10T10:12:44+00:00",
            "last_run_at": null,
            "last_summary": null,
            "created_at": "2026-09-03T10:12:44+00:00"
        }
    }
}

401The key is missing, malformed, or revoked.

{
    "success": false,
    "error": {
        "code": "UNAUTHENTICATED",
        "message": "Provide a valid API key as a bearer token.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

422The request body could not be validated; `error.errors` lists the fields.

{
    "success": false,
    "error": {
        "code": "VALIDATION_FAILED",
        "message": "The email field is required.",
        "errors": {
            "email": [
                "The email field is required."
            ]
        },
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

429Over 5 requests per second for the key. Retry after the limit resets.

{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Retry after the limit resets.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

Error codes

Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.

markdown version·openapi.json