`POST /api/v2/verify` · `lumail.verify()`

Checks syntax, disposable and blocked domains, free mailbox providers, privacy aliases, recurring suspicious domains, spam-like or role-based mailbox names, domain typos, trusted global history, and DNS mail routing. Permission: `emails`. Dashboard, API, CLI, and `verify_email` share one monthly organization quota.

HTTP 200 for both accept and reject. Transport / auth failures use `{ name, message, statusCode }`. Every problem lives in `issues`. `suggestion` is the corrected address when the domain looks like a typo (`user@gmial.com` → `user@gmail.com`).

`accept` is the policy decision. It can be `true` when the monthly quota is exhausted (`status: "skipped"`, `issues[0].code: "quota_exceeded"`, `score: null`). That fallback skips history and DNS and does not change sending eligibility elsewhere.

DNS routing does not prove the mailbox exists. A clean address on a domain that routes mail is `result: "risky"` with `accept: true`. `result: "deliverable"` only appears on historical SMTP-confirmed rows.

Disposable domains are rejected with `disposable_email`. Free providers are informational. Privacy aliases stay accepted and return a `privacy_alias` warning because they can forward to a real mailbox. Recurring suspicious domains return `suspicious_domain` for manual review without being rejected. These list checks are local and do not call another verification service.

## Score

`score` is a deterministic 0–100 confidence from the signals that ran. It is not a delivery guarantee. Use it to rank or threshold addresses; use `accept` for the hard policy decision.

| Range  | Meaning                                                                                                                                                                  |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 80     | Every executed check passed (syntax, lists, history, DNS routing). Mailbox not proven. `result: "risky"`.                                                                |
| 90+    | Historical SMTP-confirmed rows only.                                                                                                                                     |
| 30     | DNS could not be resolved. `result: "unknown"`.                                                                                                                          |
| 0–15   | Rejected, including no mail destination and null MX. `mailbox_full` scores 15 and `previously_complained` 5 because the mailbox existed when those events were recorded. |
| `null` | Nothing was checked (quota exhausted or internal failure).                                                                                                               |

Spam-like mailbox names subtract 30, domain typos 35, role-based mailboxes (`info@`, `support@`) 10.

## Routing cache

MX + resolved addresses are cached per domain (12 h; 7 days for pinned consumer providers such as Gmail, Outlook, Yahoo, iCloud, Orange, Free, SFR, La Poste, Proton, and GMX). Temporary DNS failures are not cached. No SMTP session is opened and no email is sent.

## Body Parameters

| Name    | Type   | Description                     |
| ------- | ------ | ------------------------------- |
| `email` | string | **Required.** Address to check. |

## Response Fields

| Field        | Type                   | Description                                                                                                     |
| ------------ | ---------------------- | --------------------------------------------------------------------------------------------------------------- |
| `object`     | `"email_verification"` | Resource type.                                                                                                  |
| `id`         | string                 | Tenant-owned `verify_result` id.                                                                                |
| `email`      | string                 | Normalized address that was checked.                                                                            |
| `result`     | string                 | `deliverable`, `risky`, `undeliverable`, or `unknown`.                                                          |
| `accept`     | boolean                | Policy decision. Can be `true` when quota is exhausted.                                                         |
| `status`     | string                 | `completed`, `skipped`, or `failed`.                                                                            |
| `score`      | integer \| null        | 0–100 deliverability confidence.                                                                                |
| `suggestion` | string \| null         | Better address when the domain looks like a typo.                                                               |
| `traits`     | object                 | Stable booleans for disposable, free-provider, privacy-alias, role-based, and suspicious-domain classification. |
| `issues`     | object[]               | Errors and warnings only. Empty when the address is clean. Primary reason is first.                             |
| `quota`      | object                 | `{ limit, used, remaining, resets_at }` for the UTC month.                                                      |
| `retry_at`   | string \| null         | ISO timestamp when a retryable check should be tried again.                                                     |
| `cache_hit`  | boolean                | `true` when the result was served from cache.                                                                   |

### `issues[]`

| Field       | Type                     | Description                                                                       |
| ----------- | ------------------------ | --------------------------------------------------------------------------------- |
| `code`      | string                   | Stable reason (`mailbox_not_found`, `domain_typo`, `catch_all`, …).               |
| `message`   | string                   | Human-readable reason.                                                            |
| `severity`  | `"error"` \| `"warning"` | `error` blocks `accept`. `warning` does not.                                      |
| `retryable` | boolean                  | `true` for `mailbox_full`, `mailbox_unknown`, `quota_exceeded`, `internal_error`. |

Common `code` values: `invalid_format`, `disposable_email`, `spam_domain`, `invalid_domain`, `test_email`, `mailbox_not_found`, `mailbox_full`, `mailbox_unknown`, `previously_bounced`, `previously_complained`, `globally_suppressed`, `quota_exceeded`, `internal_error`, `domain_typo`, `role_based`, `spam_like`, `privacy_alias`, `suspicious_domain`, `catch_all`.

## Errors

| Status | `name`               | When                                |
| ------ | -------------------- | ----------------------------------- |
| 401    | `missing_api_key`    | Missing or invalid bearer token     |
| 403    | `missing_permission` | Token lacks the `emails` permission |
| 422    | `validation_error`   | Missing `email`                     |

## Related

- [SDK: lumail.verify](/docs/sdk/v2/verify)

_Older emails.verify path: [emails.verify](/docs/api-reference/v2/emails-verify)._


## API Reference
**Method:** POST
**Endpoint:** /api/v2/verify

### SDK
```ts
import { Lumail } from "lumail";

const lumail = new Lumail({ apiKey: process.env.LUMAIL_API_KEY });
const { data, error } = await lumail.verify("user@example.com");
if (error) {
  throw error;
}
if (!data.accept) {
  throw data.issues[0];
}
console.log(data.result, data.score, data.suggestion);
```

### cURL
```bash
curl -X POST https://lumail.io/api/v2/verify \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}'
```

### JavaScript
```javascript
const response = await fetch("https://lumail.io/api/v2/verify", {
  method: "POST",
  headers: {
    Authorization: "Bearer YOUR_API_TOKEN",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email: "user@example.com" }),
});
const data = await response.json();
```

### Success Response
```json
{
  "object": "email_verification",
  "id": "vr_abc123",
  "email": "user@example.com",
  "result": "risky",
  "accept": true,
  "status": "completed",
  "score": 80,
  "suggestion": null,
  "traits": {
    "domain_category": "custom_domain",
    "disposable": false,
    "free_provider": false,
    "privacy_alias": false,
    "role_based": false,
    "suspicious_domain": false
  },
  "issues": [],
  "quota": {
    "limit": 10000,
    "used": 12,
    "remaining": 9988,
    "resets_at": "2026-10-01T00:00:00.000Z"
  },
  "retry_at": null,
  "cache_hit": false
}
```

### Error Response
```json
{
  "name": "validation_error",
  "message": "Email is required",
  "statusCode": 422
}
```
