Skip to content

Verify an email

Submit an address, poll the job, and read its checks, honest verdict, and confidence score: inconclusive checks are refunded automatically.

Submit a job

POST /api/verify reserves the cost, queues the job, and returns 202 Accepted with a job_uuid. The response sets a Location header with the poll URL and a Retry-After hint while the job runs.

Request
curl -X POST https://api.pricklymails.com/api/verify \
  -H "Authorization: Bearer pm_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1234" \
  -d '{ "email": "someone@example.com" }'

Pass an optional Idempotency-Key header to make retries safe: for 24 hours, the same key replays the original job instead of billing a new one (the response then carries Idempotent-Replayed: true).

Poll the result

GET /api/verify/{uuid} returns the job and its per-check results, scoped to your account. It keeps setting Retry-After until the job reaches a terminal status (completed, failed, or canceled).

Once completed, verifications holds the result: the verdict (status), its reason, the score, the action recommendation, and the per-check outcome. The address itself is never stored, so the result identifies it only by keyed hashes. A check your key does not enable reads { "status": "skipped" }. Here, an address at a provider that accepts every recipient, where the mailbox probe is skipped:

Response
{
    "job_uuid": "0192f3c4-8a6e-7b1d-9c2e-5f4a3b2c1d0e",
    "status": "completed",
    "verifications": [
        {
            "email_hash": "3b8f1c0d9e7a",
            "domain_hash": "a41e77c2d0b5",
            "status": "valid",
            "reason": "accept_all_provider",
            "score": 76,
            "recommendation": "send",
            "duration_ms": 412,
            "verified_at": "2026-10-01T09:12:45.120+00:00",
            "checks": {
                "syntax": true,
                "mx": { "valid": true, "records": ["gmail-smtp-in.l.google.com", "alt1.gmail-smtp-in.l.google.com"] },
                "disposable": false,
                "role": false,
                "free": true,
                "typo": { "detected": false },
                "smtp": { "status": "skipped" },
                "catchall": { "status": "skipped" }
            },
            "durations": {
                "syntax": 0,
                "mx": 38,
                "smtp": null,
                "disposable": 0,
                "role": 0,
                "free": 0,
                "catchall": null,
                "typo": 0
            }
        }
    ]
}

recommendation turns the honest verdict into an action for list cleaning: remove, send, or review (see Batches).

Get the result in the same call

A request that can wait a few seconds, such as a signup form, can skip the polling: send Prefer: wait=N, the number of seconds it can wait, 12 at most. The job is queued and billed exactly as without the header; only the response waits for it. If the job finishes in time, the response is 200 OK with the result in verification, in the shape of a polled result; if not, it is the usual 202 Accepted, and the job keeps running. Either way, the response carries Preference-Applied: wait=N.

Request
curl -X POST https://api.pricklymails.com/api/verify \
  -H "Authorization: Bearer pm_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Prefer: wait=4" \
  -d '{ "email": "someone@example.com" }'
Response
{
    "job_uuid": "0fdfff58-5f5a-4c52-b66c-ee78705db27c",
    "status": "completed",
    "estimated_cost": 7,
    "idempotency_key": "14f4a9b6-9f45-4579-9bba-d4a282a08289",
    "replayed": false,
    "email": "someone@example.com",
    "verification": {
        "email_hash": "28ba1aff51a7",
        "domain_hash": "4ac0ea9fe911",
        "status": "invalid",
        "reason": "null_mx",
        "score": 0,
        "recommendation": "remove",
        "duration_ms": 277,
        "verified_at": "2026-10-21T20:35:30.938480+00:00",
        "checks": {
            "syntax": true,
            "mx": { "valid": false, "records": ["."] },
            "disposable": { "status": "skipped" },
            "role": { "status": "skipped" },
            "free": { "status": "skipped" },
            "typo": { "detected": false },
            "smtp": { "status": "skipped" },
            "catchall": { "status": "skipped" }
        },
        "durations": {
            "syntax": 0,
            "mx": 277,
            "smtp": null,
            "disposable": null,
            "role": null,
            "free": null,
            "catchall": null,
            "typo": 0
        }
    }
}

A mail server that asks to try again later delays the result by a minute or more. Where nothing can wait that long, as in a signup hook, take a 202 as no answer yet and let the step go on.

The checks

Each address runs through the checks your key enables:

  • syntax: RFC 5322 format.
  • mx: the domain has usable mail servers (a missing, null, parked, or inactive mail exchanger fails here).
  • smtp: the mailbox accepts mail (deliverability probe).
  • disposable: a known throwaway provider, matched on the address domain or its mail exchanger.
  • role: a role address such as info@ or support@.
  • free: a free webmail provider.
  • catchall: the domain accepts any address. Available from the Pro plan up; it sends one extra random recipient in the same session as the mailbox probe, and only when it is requested. Without it, checks.catchall reads { "status": "skipped" } and no extra recipient is ever sent.

Typo detection always runs, regardless of the checks your key enables: when a likely misspelling is found, the response echoes a suggested correction.

To run other checks on one request, send them in checks, for example { "email": "jane@example.com", "checks": ["mx", "smtp", "disposable"] }. Each one must be allowed on your plan, or the request is a 403; an unknown name is a 400. Dependencies are added for you: smtp brings mx, and catchall brings both, and the cost follows the checks that run.

Verdict and score

Each result carries a deliverability verdict in its status field (valid, invalid, risky, unknown) and a 0-100 quality score. When a typo is detected, the submit response echoes a suggestion (for example someone@gmial.com suggests someone@gmail.com); the polled result carries only the corrected domain, in checks.typo.suggested_domain, since the address is never stored.

The score of a mailbox the server accepted tops out at 88 when the catch-all check did not run (Free and Starter), and at 96 when it ran and ruled out a catch-all domain (Pro and up): the extra 8 points reward the one thing only that check can establish.