# Verify an email address

`POST /api/v1/email`

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

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 | Required | Description |
| --- | --- | --- | --- |
| `email` | string | yes | 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 | no | 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 | no | 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

```bash
curl -X POST https://spaw.co/api/v1/email \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "mia@acme.com"
}'
```

## Responses

### 200 — The verdict and every field, whether or not the address is deliverable.

```json
{
    "success": true,
    "data": {
        "email": "mia@acme.com",
        "normalized_email": "mia@acme.com",
        "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"
    }
}
```

### 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"
    }
}
```

### 402 — The balance is empty. The lookup did not run.

```json
{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_CREDITS",
        "message": "Your credit balance is empty.",
        "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
- `INSUFFICIENT_CREDITS` — https://spaw.co/docs/errors/INSUFFICIENT_CREDITS
- `RATE_LIMITED` — https://spaw.co/docs/errors/RATE_LIMITED

---

Canonical page: https://spaw.co/docs/api/verify-email · OpenAPI document: https://spaw.co/openapi.json · All endpoints: https://spaw.co/docs/api
