# Monitor a list of domains on a schedule

`POST /api/v1/email/domain-monitors`

- 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.
- Group: Email

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 | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | A label for the alerts. At most 100 characters. |
| `domains` | string[] | yes | 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. |
| `cadence` | string | yes | How often the list is re-checked. One of: daily, weekly, monthly. |
| `webhook_url` | string | null | no | 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 | no | 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

```bash
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"
}'
```

## Responses

### 201 — The monitor was saved and its baseline run queued.

```json
{
    "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
        }
    }
}
```

### 401 — The key is missing, malformed, or revoked.

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

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

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

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

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

## Error codes

- `UNAUTHENTICATED` — https://spaw.co/docs/errors/UNAUTHENTICATED
- `VALIDATION_FAILED` — https://spaw.co/docs/errors/VALIDATION_FAILED
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED

---

Canonical page: https://spaw.co/docs/api/create-domain-monitor · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
