Less boo. More booked. Halloween offer: 30 days free on any paid plan. Claim 30 days freeEvery new account gets one free mailbox for warmup and replies. No domain to buy. See how it works

Skip to content

Verify

API reference8 min read6 sections

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.

API reference

#The verification object

json
{
  "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
}
FieldDescription
statusOne of valid, invalid, risky, catch_all, unknown.
confidence0-100 score. Used with status to decide whether the address is safe to send to.
reasonShort machine-readable reason code (e.g. mailbox_accepts, mailbox_rejected, disposable_domain, no_mx_record, accept_all_domain).
checksThe 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.
cachedtrue 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).
retryabletrue if the check failed for a transient reason (e.g. SMTP timeout) and re-running with force: true may produce a different answer.
API reference

#Verify a single email

text
POST /verify

Runs (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.

FieldTypeRequiredDescription
emailstringYesThe address to verify, 3 to 320 characters.
forcebooleanNoSkip 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.
bash
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" }'
json
{
  "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.

json
{ "error": { "code": "payment_required", "message": "plan_limit_reached", "details": { "used": 5000, "limit": 5000, "label": "email verifications" } } }
API reference

#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

text
POST /verify/bulk
FieldTypeRequiredDescription
namestringNoOptional label for the batch.
emailsstring[]NoArray of addresses.
textstringNoA 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.

bash
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"] }'
json
{
  "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

text
GET /verify/bulk/{id}

Returns the batch's progress and, for items that have been processed, their verification results.

bash
curl https://app.warmerly.com/api/v1/verify/bulk/6f2c6c8e-9b1d-4e3a-9c7f-2d1e8b4a6c9e \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "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:

bash
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
API reference

#Verification history

text
GET /verify/history

Returns the recent single verifications (from POST /verify) recorded against your active workspace, most recent first, cursor-paginated.

Query paramTypeRequiredDescription
cursorISO 8601 timestampNoFetch results older than this timestamp. Use the previous response's nextCursor.
limitintegerNoPage size, 1-100. Default 25.

An invalid cursor or limit returns 400 bad_request (invalid_query).

bash
curl "https://app.warmerly.com/api/v1/verify/history?limit=25" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "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.

API reference

#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.

text
POST /verify/send-test
GET  /verify/send-test/{id}
FieldTypeRequiredDescription
emailstringYesAddress to test, 3 to 320 characters. An address that is not valid returns 400 bad_request (invalid_email).
checkIdstring (UUID)NoLinks the result back to an earlier verification, so the original check is refined rather than duplicated.
json
{
  "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 failed with failureReason: "no_account" rather than erroring. Other send failures also come back as failed, with the error text in failureReason. A recipient server that refuses the address while the message is being sent makes the test invalid straight 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} returns 404 not_found for 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.
API reference

#Usage / quota

text
GET /verify/usage

Returns your current verification usage against your plan's limit for the billing period. Checking usage does not itself consume quota.

bash
curl https://app.warmerly.com/api/v1/verify/usage \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "usage": {
    "used": 128,
    "limit": 25000,
    "remaining": 24872,
    "plan": "growth"
  }
}

plan is null when no active plan applies to the account.

Was this page helpful?