Skip to content

API reference/ip

Monitor your own addresses on a schedule

POST/api/v1/ip/monitors

authentication
Secret API key as a bearer token
billing
Creation is free. Each run bills every address like a single lookup: 1 credit per 5 fresh answered lookups; reserved and unallocated addresses and 7-day repeats are free, so a daily list costs at most one credit per five addresses a week.

Saves up to 500 of your own addresses, your egress addresses, mail servers and office connections, and re-checks them every day, week or month. Addresses are canonicalised and deduplicated. The first run starts right away and only sets the baseline; from the next run on, every address that newly lands on a threat blocklist (Spamhaus DROP, ASN-DROP or the botnet C2 list), becomes a Tor exit, or scores high is reported to the account's email with the reason, so you learn that a mail server sits on a blocklist before your recipients' inboxes do.

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": "ip_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 alerts. At most 100 characters.
ipsrequiredstring[]1 to 500 IPv4 or IPv6 addresses, or prefixes of at most 256 addresses (/24 to /32, /120 to /128), which are expanded into their addresses before the cap; duplicates and alternative spellings of one address count once. A wider prefix answers 422 — split it, because a /16 is 65,536 lookups a run.
cadencerequiredstringHow often the list is re-checked. One of: daily, 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/ip/monitors \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Mail servers",
  "ips": [
    "203.0.113.25",
    "2001:db8::25"
  ],
  "cadence": "daily"
}'
const response = await fetch('https://spaw.co/api/v1/ip/monitors', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_…',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Mail servers",
    "ips": [
      "203.0.113.25",
      "2001:db8::25"
    ],
    "cadence": "daily"
  }),
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/ip/monitors',
    headers={'Authorization': 'Bearer sk_live_…'},
    json={
        'name': 'Mail servers',
        'ips': [
            '203.0.113.25',
            '2001:db8::25'
        ],
        'cadence': 'daily'
    },
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/ip/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' => 'Mail servers',
        'ips' => [
            '203.0.113.25',
            '2001:db8::25'
        ],
        'cadence' => 'daily'
    ]),
]);
$result = json_decode(curl_exec($ch), true);
from spaw import Client

client = Client('sk_live_…')
result = client.create_ip_monitor(name='Mail servers', ips=[
    '203.0.113.25',
    '2001:db8::25'
], cadence='daily')
import Spaw from 'spaw';

const spaw = new Spaw({ apiKey: 'sk_live_…' });
const result = await spaw.createIpMonitor({
    name: 'Mail servers',
    ips: [
        '203.0.113.25',
        '2001:db8::25'
    ],
    cadence: 'daily'
});
use Spaw\Client;

$spaw = new Client('sk_live_…');
$result = $spaw->createIpMonitor(name: 'Mail servers', ips: [
    '203.0.113.25',
    '2001:db8::25'
], 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": "Mail servers",
            "cadence": "daily",
            "ip_count": 2,
            "next_run_at": "2026-09-06T10:12:44+00:00",
            "last_run_at": null,
            "last_summary": null,
            "created_at": "2026-09-05T10: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