# Verify

> API reference for Warmerly's email verification: check syntax, MX records, disposable/role addresses, and live SMTP deliverability, single or in bulk jobs.

Source: https://warmerly.com/docs/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

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

| 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

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

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

```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](https://warmerly.com/docs/verify#verification-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" } } }
```

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

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

```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
```

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

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

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

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

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

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

## Each part of this reference has its own page (6)

- [The verification object](https://warmerly.com/docs/verify/the-verification-object)
- [Verify a single email](https://warmerly.com/docs/verify/verify-a-single-email)
- [Bulk verification](https://warmerly.com/docs/verify/bulk-verification)
- [Verification history](https://warmerly.com/docs/verify/verification-history)
- [Send test (deliverability-grade check)](https://warmerly.com/docs/verify/send-test-deliverability-grade-check)
- [Usage / quota](https://warmerly.com/docs/verify/usage-quota)

## More in API reference

- [Warmup](https://warmerly.com/docs/warmup): API reference for Warmerly warmup: pull mailbox health scores and the inbox-placement emails sent between…
- [Email Finder](https://warmerly.com/docs/email-finder): API reference for the Warmerly Email Finder: find and verify a person's or company's email from a domain,…
- [Leads](https://warmerly.com/docs/leads): API reference for the Warmerly lead database: search millions of crawled companies, export matching rows to…
