Verify
Check whether an email address is deliverable before you send to it, syntax, MX records, disposable/role-based detection, and a live SMTP probe, with results cached so repeat checks are free. Verify single addresses in real time, or submit a list as a bulk job for asynchronous processing.
Verify endpoints are scoped to your user account, not a project, they don't require the X-Project-Id header. Single checks and their history are recorded against your active workspace (the one named by the X-Workspace-Id header, or your own workspace if none is sent). Bulk batches belong to your user account.
#The verification object
{
"email": "jane@example.com",
"status": "valid",
"confidence": 95,
"reason": "mailbox_accepts",
"checks": {
"syntax": true,
"mx": ["aspmx.l.google.com"],
"smtpConnectable": true,
"smtpAccepted": true,
"catchAll": false,
"disposable": false,
"roleBased": false,
"freeProvider": false,
"provider": "google",
"didYouMean": null,
"smtpCode": 250,
"smtpMessage": "2.1.5 OK"
},
"durationMs": 812,
"checkedAt": "2026-07-02T10:14:03.000Z",
"cached": false,
"retryable": false
}| Field | Description |
|---|---|
status | One of valid, invalid, risky, catch_all, unknown. |
confidence | 0-100 score. Used with status to decide whether the address is safe to send to. |
reason | Short machine-readable reason code (e.g. mailbox_accepts, mailbox_rejected, disposable_domain, no_mx_record, accept_all_domain). |
checks | The individual signals behind the status/confidence, syntax validity, MX records, SMTP connect/accept results, catch-all detection, disposable/role-based/free-provider flags, and the raw SMTP response. |
cached | true if this result was served from cache rather than a fresh SMTP probe. Cached results don't count against your quota. A cached result is reused for 30 days (valid), 90 days (invalid), 14 days (catch_all), 7 days (risky) or 3 days (unknown). |
retryable | true if the check failed for a transient reason (e.g. SMTP timeout) and re-running with force: true may produce a different answer. |
#Verify a single email
POST /verifyRuns (or reuses a cached) verification for one address. Cached results are returned instantly and are not billable; a fresh check consumes one verification from your plan's quota.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The address to verify, 3 to 320 characters. |
force | boolean | No | Skip the cache and run a fresh SMTP probe even if a cached result exists. A result confirmed from real mail (for example a bounce or a reply) is never re-probed, so it is still served from cache. |
curl https://app.warmerly.com/api/v1/verify \
-X POST \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "email": "jane@example.com" }'{
"result": {
"email": "jane@example.com",
"status": "valid",
"confidence": 95,
"reason": "mailbox_accepts",
"checks": {
"syntax": true,
"mx": ["aspmx.l.google.com"],
"smtpConnectable": true,
"smtpAccepted": true,
"catchAll": false,
"disposable": false,
"roleBased": false,
"freeProvider": false,
"provider": "google",
"didYouMean": null,
"smtpCode": 250,
"smtpMessage": "2.1.5 OK"
},
"durationMs": 812,
"checkedAt": "2026-07-02T10:14:03.000Z",
"cached": false,
"retryable": false
},
"checkId": "b2b7c9a0-1e3f-4a2b-9c1d-4e8f0a2b3c4d"
}Every call (cached or not) is saved to your history as checkId. Errors: 400 bad_request (invalid_body) for an invalid body, 403 channel_not_included if your plan does not include email features, and 402 payment_required if you're out of verification quota. The 402 message is plan_limit_reached (or free_trial_limit_reached when you have no active plan), and details holds used, limit and label.
{ "error": { "code": "payment_required", "message": "plan_limit_reached", "details": { "used": 5000, "limit": 5000, "label": "email verifications" } } }#Bulk verification
Submit a list of addresses as an asynchronous batch, then poll for results. Quota for the full batch size is checked up front, before any items are processed.
#Submit a batch
POST /verify/bulk| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Optional label for the batch. |
emails | string[] | No | Array of addresses. |
text | string | No | A pasted blob of addresses separated by whitespace, commas, or semicolons. |
Provide emails, text, or both, they're merged and de-duplicated. Addresses are trimmed and lower-cased, and anything without an @ is dropped. A batch may contain at most 50,000 addresses. name is at most 120 characters.
curl https://app.warmerly.com/api/v1/verify/bulk \
-X POST \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Q3 leads", "emails": ["jane@example.com", "john@example.com"] }'{
"batch": {
"id": "6f2c6c8e-9b1d-4e3a-9c7f-2d1e8b4a6c9e",
"total": 2,
"status": "processing"
}
}201 Created on success. Errors: 400 bad_request (invalid_body, no_valid_emails or batch_too_large_max_50000), 403 channel_not_included if your plan does not include email features, and 402 quota_exhausted if the batch does not fit your remaining quota. The 402 details hold label, used, limit, topUpUrl and upgradeUrl, plus topUp when credits can cover the shortfall. Items still queued from your earlier batches count against the new one.
#Get batch status and results
GET /verify/bulk/{id}Returns the batch's progress and, for items that have been processed, their verification results.
curl https://app.warmerly.com/api/v1/verify/bulk/6f2c6c8e-9b1d-4e3a-9c7f-2d1e8b4a6c9e \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"batch": {
"id": "6f2c6c8e-9b1d-4e3a-9c7f-2d1e8b4a6c9e",
"name": "Q3 leads",
"status": "processing",
"total": 2,
"processed": 1,
"createdAt": "2026-07-02T10:00:00.000Z",
"completedAt": null
},
"items": [
{
"email": "jane@example.com",
"recommendation": "Safe to send",
"status": "valid",
"confidence": 95,
"reason": "mailbox_accepts"
},
{
"email": "john@example.com",
"recommendation": "Pending",
"status": null,
"confidence": null,
"reason": null
}
],
"itemsTruncated": false,
"itemsLimit": 1000
}batch.status is processing or completed. The JSON view returns at most 1,000 items, in a stable order; itemsTruncated says whether the list was cut and itemsLimit is the cap (1000). Add ?all=1 for every item as JSON. Each item's recommendation is Safe to send, Send with caution, Do not send, Unknown, or Pending (not yet processed), a human-readable summary derived from status/confidence/checks. 404 not_found if the batch doesn't exist or belongs to another user.
Pass ?format=csv to download all results (not capped at 1,000) as a CSV file (email,recommendation,status,confidence,reason) instead of JSON. Items not yet processed show pending in the status column:
curl "https://app.warmerly.com/api/v1/verify/bulk/6f2c6c8e-9b1d-4e3a-9c7f-2d1e8b4a6c9e?format=csv" \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-o results.csv#Verification history
GET /verify/historyReturns the recent single verifications (from POST /verify) recorded against your active workspace, most recent first, cursor-paginated.
| Query param | Type | Required | Description |
|---|---|---|---|
cursor | ISO 8601 timestamp | No | Fetch results older than this timestamp. Use the previous response's nextCursor. |
limit | integer | No | Page size, 1-100. Default 25. |
An invalid cursor or limit returns 400 bad_request (invalid_query).
curl "https://app.warmerly.com/api/v1/verify/history?limit=25" \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"checks": [
{
"id": "b2b7c9a0-1e3f-4a2b-9c1d-4e8f0a2b3c4d",
"email": "jane@example.com",
"status": "valid",
"confidence": 95,
"reason": "mailbox_accepts",
"checks": { "...": "..." },
"confirmedStatus": null,
"sendTestStatus": null,
"createdAt": "2026-07-02T10:14:03.000Z"
}
],
"nextCursor": "2026-07-02T09:58:11.000Z"
}confirmedStatus and sendTestStatus reflect a follow-up bounce-probe test send linked to the check (confirmedStatus is valid or invalid once resolved); both are null if no test was linked. nextCursor is null once you've reached the end of history.
#Send test (deliverability-grade check)
Some addresses cannot be settled by SMTP probing alone, a catch-all domain accepts everything, so "accepted" proves nothing. A send test resolves those the only way that actually works: it sends one real message from one of your own warmup mailboxes and watches for a bounce.
POST /verify/send-test
GET /verify/send-test/{id}| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Address to test, 3 to 320 characters. An address that is not valid returns 400 bad_request (invalid_email). |
checkId | string (UUID) | No | Links the result back to an earlier verification, so the original check is refined rather than duplicated. |
{
"test": {
"id": "9f1c2d3e-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
"email": "sam@example.com",
"status": "awaiting_bounce",
"fromAccount": "jane@yourdomain.com",
"bounceCode": null,
"bounceMessage": null,
"failureReason": null,
"deadlineAt": "2026-09-12T10:15:00.000Z"
}
}GET /verify/send-test/{id} is the poll: each call does a throttled IMAP check for the bounce and returns the current status, awaiting_bounce, valid, invalid or failed. The watch window is about five minutes (deadlineAt). A bounce inside it makes the test invalid. A reply from the address, or reaching deadlineAt with no bounce, makes it valid. So valid here means "no bounce came back in the window", not proof the mailbox is read. A poll made after deadlineAt is what settles the test, so keep polling until the status is no longer awaiting_bounce.
Notes worth knowing before you wire this up:
- It requires a connected sending mailbox, one of your own that is active or aging, can send, and whose inbox Warmerly can read. With none, the test comes back immediately as
failedwithfailureReason: "no_account"rather than erroring. Other send failures also come back asfailed, with the error text infailureReason. A recipient server that refuses the address while the message is being sent makes the testinvalidstraight away. - Your plan must include email features, otherwise the call returns
403 channel_not_included. - Polls are throttled: calls within 15 seconds of the last inbox check return the stored status without a new check.
GET /verify/send-test/{id}returns404 not_foundfor a test that is not yours. - It sends real email from your mailbox, so it affects that mailbox's reputation like any other send. Use it to settle a handful of ambiguous addresses, not as a bulk strategy.
- It is not billed as a separate verification. It refines a check that was already counted.
#Usage / quota
GET /verify/usageReturns your current verification usage against your plan's limit for the billing period. Checking usage does not itself consume quota.
curl https://app.warmerly.com/api/v1/verify/usage \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"usage": {
"used": 128,
"limit": 25000,
"remaining": 24872,
"plan": "growth"
}
}plan is null when no active plan applies to the account.