- authentication
- Secret API key as a bearer token
- billing
- 1 credit for a fresh deliverable or risky verdict; undeliverable verdicts, invalid input, 7-day repeats and test addresses are free.
Runs the full pipeline on one address and answers with a verdict, a machine-readable reason, an auditable 0–100 risk score, and 27 named fields. The checks run in order: address extraction (display names, mailto: prefixes, spreadsheet quotes and invisible characters are stripped), RFC 5322 syntax, live MX resolution including the null-MX and implicit-MX rules, the disposable, role, free-provider and typo-squat lists, provider-specific username rules, SPF and DMARC records, the domain's registration age from RDAP, and finally a mailbox-level SMTP handshake that ends before any message is transmitted.
The mailbox probe runs only when the domain accepts mail and no free signal already settled the answer, so a disposable domain or a typo-squat never reaches the mail server. Confirmed mailbox answers are shared across customers for up to a week, which is why smtp_checked_at can predate the lookup. When mail servers refuse to say whether an inbox exists (greylisting, timeouts), the answer degrades honestly to smtp_checked: false and mailbox_exists: null, plus a recomputable mailbox_confidence estimate.
A single lookup never consults your suppression list: an explicit re-verify is the only way an address earns its way off it. The batch, bulk and monitor paths do honor it.
Settling an unverified answer later
Pass callback_url and callback_secret and an unverified answer is re-checked after 5 and 20 minutes. The settled verdict is POSTed to the callback as { "event": "email.settled", "request_id", "settled", "attempt", "data", "meta" }, signed with the HMAC-SHA256 of the exact raw body in the X-Spaw-Signature header. The response to this request then carries meta.settling: true. Re-checks are repeats of a charged lookup, so they cost nothing; settled is false when the last re-check still could not say.
Request body
| field | type | description |
|---|---|---|
| emailrequired | string | The address to verify. Wrappers people paste (a display name, a `mailto:` prefix, quotes, trailing punctuation) are stripped before checking. At most 254 characters. |
| callback_url | string | null | An https URL. When the answer comes back unverified (greylisted, timed out, provider unavailable), the address is re-checked after 5 and 20 minutes and the settled verdict is POSTed here. At most 2,048 characters. |
| callback_secret | string | null | Required with `callback_url`. Signs the callback body — `X-Spaw-Signature` is the HMAC-SHA256 of the exact raw body, like the bulk webhook. 16 to 128 characters. |
Example request
curl -X POST https://spaw.co/api/v1/email \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]"
}'Responses
200The verdict and every field, whether or not the address is deliverable.
{
"success": true,
"data": {
"email": "[email protected]",
"normalized_email": "[email protected]",
"is_alias": false,
"is_gibberish": false,
"deliverable": "deliverable",
"reason": null,
"risk_score": 0,
"risk_level": "low",
"syntax_valid": true,
"domain": "acme.com",
"mx_found": true,
"mx_implicit": false,
"mx_provider": "google",
"has_spf": true,
"dmarc_policy": "reject",
"domain_registered_at": "1998-03-12",
"domain_age_days": 10402,
"disposable": false,
"role": false,
"free_provider": false,
"smtp_checked": true,
"smtp_checked_at": "2026-09-03T10:12:44+00:00",
"mailbox_exists": true,
"catch_all": false,
"smtp_reason": null,
"mailbox_confidence": null,
"did_you_mean": null,
"sources": [
{
"dataset": "rfc-5322-syntax",
"version": "2026-09-03"
},
{
"dataset": "live-dns",
"version": "2026-09-03"
},
{
"dataset": "disposable-domains",
"version": "2026-09-01"
},
{
"dataset": "mx-provider-patterns",
"version": "2026-08-30"
},
{
"dataset": "mailbox-smtp",
"version": "2026-09-03"
}
]
},
"meta": {
"credits_used": 1,
"credits_remaining": 9,
"cache_hit": false,
"suppressed": false,
"request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
}
}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"
}
}402The balance is empty. The lookup did not run.
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "Your credit balance is empty.",
"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.