- authentication
- Secret API key as a bearer token
- billing
- Creation is free. Each run bills every domain like one domain lookup: 1 credit when the domain accepts mail; dead and invalid domains and 7-day repeats are free, so a domain is billed at most once a week whatever the cadence.
Saves up to 500 domains — your own sending domains, or the domains your list is full of — and re-checks them every day, week or month through the domain endpoint. Domains are lowercased, converted to punycode and deduplicated. The first run starts right away and only sets the baseline; from the next run on, every domain that changes mail provider, loses its SPF, DMARC, DKIM keys or MTA-STS policy, is parked, lands its mail hosts on a threat blocklist, has a registration ending within 30 days, or stops accepting mail is reported to the account's email with the reason.
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": "domain_monitor.run", "monitor": { "id", "name", "cadence", "last_run_at", "next_run_at" }, "summary": { … the same block as last_summary … }, "changes": [ … ] }, where changes names the domains 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; the original X-Spaw-Signature is sent beside it until 1 March 2027. Connection errors and 5xx answers are retried twice, 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 |
|---|---|---|
| namerequired | string | A label for the alerts. At most 100 characters. |
| domainsrequired | string[] | 1 to 500 domain names. Spellings of one domain (case, a trailing dot, an internationalised form) count once; anything that is not a hostname answers 422 naming the entry. |
| cadencerequired | string | How often the list is re-checked. One of: daily, weekly, monthly. |
| webhook_url | string | null | An 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_events | string | When to post. `changes` posts only when the run found something new or stopped early; `every_run` posts after every run. One of: changes, every_run. Default: changes. |
Example request
curl -X POST https://spaw.co/api/v1/email/domain-monitors \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Sending domains",
"domains": [
"acme.com",
"news.acme.com"
],
"cadence": "daily"
}'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": 4,
"name": "Sending domains",
"cadence": "daily",
"domain_count": 2,
"next_run_at": "2026-09-14T10:12:44+00:00",
"last_run_at": null,
"last_summary": null,
"webhook_url": null,
"webhook_events": "changes",
"webhook_status": null,
"webhook_detail": null,
"prunes_after_days": 365,
"created_at": "2026-09-13T10:12:44+00:00",
"webhook_secret": null
}
}
}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.