<!-- Verify — https://docs.warmerly.com/verify -->

# 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 bulk batches are recorded
against your currently active workspace automatically.

## The verification object

```json
{
  "email": "jane@example.com",
  "status": "valid",
  "confidence": 92,
  "reason": "smtp_accepted",
  "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. `smtp_accepted`, `mailbox_not_found`, `disposable_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. |
| `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 /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. |
| `force` | boolean | No | Skip the cache and run a fresh SMTP probe even if a cached result exists. |

```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": 92,
    "reason": "smtp_accepted",
    "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](#verification-history) as
`checkId`. Errors: `400 bad_request` for an invalid body, `402 payment_required` if you're
out of verification quota.

```json
{ "error": { "code": "payment_required", "message": "quota_exceeded", "details": { "used": 5000, "limit": 5000 } } }
```

## 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. A batch may
contain at most 50,000 addresses.

```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` (`no_valid_emails` or
`batch_too_large_max_50000`), `402 payment_required`
(`quota_exceeded`, with `requested`/`used`/`limit`/`remaining` in `details`).

### Get batch status and results

```
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": 92,
      "reason": "smtp_accepted"
    },
    {
      "email": "john@example.com",
      "recommendation": "Pending",
      "status": null,
      "confidence": null,
      "reason": null
    }
  ]
}
```

`batch.status` is `processing` or `completed`. 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 the results as a CSV file
(`email,recommendation,status,confidence,reason`) instead of JSON:

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

```
GET /verify/history
```

Returns your recent single verifications (from `POST /verify`), 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`. |

```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": 92,
      "reason": "smtp_accepted",
      "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
kicked off from the dashboard to confirm an uncertain SMTP result (`valid` or
`invalid` once resolved); both are `null` if no test was run. `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. |
| `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. With none, the test comes back immediately as
  `failed` with `failureReason: "no_account"` rather than erroring.
- 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/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": 500,
    "remaining": 372,
    "plan": "growth"
  }
}
```
