# Verify that a mailbox exists

> POST /v1/verify: ask the domain's mail server whether an exact address exists (no email is sent), with catch-all detection, and read status, reason and confidence.

Source: https://isbusinessemail.com/docs/verify-endpoint

```text
POST https://api.isbusinessemail.com/v1/verify
GET  https://api.isbusinessemail.com/v1/verify?email=…
```

`/v1/check` tells you what kind of address something is. `/v1/verify` answers a different question: **does this exact mailbox exist and accept mail?** Use it before you send to an address you collected, when cleaning a list, or when a bounce would cost you.

It works the way a sending mail server does, minus the message:

1. Syntax, typos and the domain's classification (the same engine as `/v1/check`).
2. The domain's MX records. No domain, no MX or a [null MX](https://www.rfc-editor.org/rfc/rfc7505) means nothing can be delivered.
3. An SMTP conversation with the primary mail server: `EHLO`, `MAIL FROM`, `RCPT TO:<the address>`. We read the reply and hang up. **No `DATA`, no message, nothing reaches the person.**
4. A second `RCPT TO` with a random address at the same domain. If the server accepts that too, the domain is **catch-all** and its "yes" proves nothing.

Try it without a key on the [free email verifier](https://isbusinessemail.com/tools/email-verifier).

## Request

```bash
curl https://api.isbusinessemail.com/v1/verify \
  -H "Authorization: Bearer $IBE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "jane@acme.io"}'
```

- **Secret keys only.** Publishable keys (`ibe_pub_…`) get `403`: from a browser this endpoint would be a free tool for guessing which addresses exist, on your quota.
- **Takes seconds, not milliseconds.** Most answers arrive in 1 to 5 seconds; a slow mail server can take up to about 30. Call it from a background job or after the form is submitted, not on every keystroke. Use `/v1/check` for instant sign-up decisions.
- Prefer `POST`, so addresses stay out of URLs and logs.

## Response

```json
{
  "email": "jane@acme.io",
  "normalized_email": "jane@acme.io",
  "domain": "acme.io",
  "status": "deliverable",
  "reason": "mailbox_exists",
  "confidence": 0.97,
  "mailbox": "exists",
  "is_catch_all": false,
  "is_disposable": false,
  "is_role_account": false,
  "is_free_provider": false,
  "category": "business",
  "did_you_mean": null,
  "mail": { "provider": "google_workspace", "has_mx": true, "mx": ["aspmx.l.google.com", "alt1.aspmx.l.google.com"] },
  "smtp": { "mx_host": "aspmx.l.google.com", "stage": "rcpt", "code": 250, "enhanced_code": "2.1.5", "reply": "2.1.5 OK", "used_backup_mx": false },
  "cached": false,
  "took_ms": 1840,
  "request_id": "req_4f2a…"
}
```

| Field | Meaning |
| --- | --- |
| `status` | `deliverable`, `undeliverable`, `risky` or `unknown`. Act on this. |
| `reason` | Why, as a stable code: see [reasons](#reasons). |
| `confidence` | 0 to 1. A `deliverable` from the primary mail server with a negative catch-all test is 0.97; the same answer from a backup MX is lower. |
| `mailbox` | `exists`, `not_found`, `disabled`, `full` or `unknown`. |
| `is_catch_all` | `true` when the domain accepted a random address, `false` when it refused one, `null` when we couldn't tell. |
| `category`, `is_disposable`, `is_role_account`, `is_free_provider`, `did_you_mean` | From the classification, as in [`/v1/check`](https://isbusinessemail.com/docs/check-endpoint). |
| `smtp` | What the mail server said: the stage we reached (`greeting`, `ehlo`, `mail_from`, `rcpt`), the reply code, the enhanced status code and the reply text with addresses and IPs removed. `null` when we didn't need to ask (a cached catch-all domain, no MX). |
| `retry_after` | Only when `reason` is `greylisted`: seconds after which asking again should give the real answer. |
| `cached` | `true` when the answer came from a recent check of the same address (kept up to 24 hours; catch-all domains 12 hours, unknown answers 15 minutes). |

## Statuses

| Status | What it means | What to do |
| --- | --- | --- |
| `deliverable` | The mail server accepts this address and refuses made-up ones. | Send. |
| `undeliverable` | No such mailbox, a disabled one, or a domain that can't receive mail. | Don't send. Ask for another address, or fix the typo in `did_you_mean`. |
| `risky` | It may accept mail, but nobody can confirm a person reads it: a catch-all domain, a full mailbox or a disposable inbox. | Send carefully: confirm with a link, or leave out of campaigns. |
| `unknown` | The mail server gave no answer we trust: greylisting, a timeout, or a refusal to answer checks. | Retry later (`retry_after`), or fall back to a confirmation email. Never treat it as invalid. |

## Reasons

| Reason | Status | Meaning |
| --- | --- | --- |
| `mailbox_exists` | deliverable | 250 for the address, refusal for a random one. |
| `accepted_unconfirmed` | deliverable | 250 for the address; the catch-all test was inconclusive (the server hung up or deferred). Lower confidence. |
| `invalid_syntax` | undeliverable | Not an email address. |
| `domain_not_found` | undeliverable | The domain doesn't exist (NXDOMAIN). |
| `no_mail_server` | undeliverable | No MX records, and either no address record to fall back to or a host there that doesn't accept SMTP connections. |
| `null_mx` | undeliverable | The domain publishes `MX 0 .`: it accepts no mail. |
| `dns_error` | undeliverable | The domain's own DNS fails (SERVFAIL), so no one can deliver to it right now. |
| `mailbox_not_found` | undeliverable | The server says there is no such user (5.1.1, "user unknown", Microsoft's 5.4.1 recipient rejection, …). |
| `mailbox_disabled` | undeliverable | The mailbox exists but is disabled (5.2.1). |
| `mailbox_rejected` | undeliverable | A plain 550/551/553 for this recipient without wording that says why. Confidence 0.8. |
| `catch_all` | risky | The domain also accepted a random address. |
| `mailbox_full` | risky | Over quota (x.2.2). Mail bounces until it is emptied. |
| `disposable` | risky | A temporary inbox service. We don't probe these. |
| `greylisted` | unknown | The server deferred us (4xx). We ask again in the background after about 90 seconds and 5 minutes; check again after `retry_after`. |
| `probe_blocked` | unknown | The server refused to talk to us (a block list, a reputation or policy rule). Says nothing about the mailbox. |
| `smtp_unreachable` | unknown | No mail server answered on port 25. |
| `smtp_timeout` | unknown | The server was too slow, or our checks for that provider were queued too long. |
| `smtp_rejected` | unknown | An unclear refusal (e.g. a 554 with no explanation). |
| `dns_unavailable` | unknown | Our DNS lookups failed; retry. |
| `verification_unavailable` | unknown | Mailbox verification is switched off on our side. |

## How accurate is it?

For most domains, including Google Workspace, Gmail, Microsoft 365 and Outlook.com, the mail server's answer to `RCPT TO` is the same answer a real message would get, so `deliverable` and `undeliverable` are reliable. We are careful in four places:

- **Catch-all domains** accept everything. We detect them with the random address and answer `risky`, never `deliverable`.
- **Blocks look like rejections.** A 550 can mean "no such user" or "we don't accept your IP". We read the enhanced status code and the wording: anything that looks like a policy or block-list refusal is `unknown`, so a real person's address is never marked undeliverable because a server didn't like the check.
- **Backup MX servers** often accept any recipient because they can't see the user directory. We only fall back to them when the primary can't be reached, and lower the confidence when we do.
- **Greylisting** defers first-time senders on purpose. We answer `unknown` with `retry_after` and retry in the background, so asking again a few minutes later returns the real answer.

A mailbox that exists today may be gone next month, and only a confirmation email proves that someone reads it. For sign-ups, a link in a welcome email remains the best final check.

## Limits and cost

- Each verification counts as **1 check** against your quota (per day on the free tier, per month on paid plans). **An `unknown` answer gives its check back**, so you only pay for answers: greylisting, blocks and timeouts cost nothing (`X-RateLimit-Remaining` already includes the refund).
- Verifications also have their own daily cap, because each one is an SMTP conversation from our servers: 100 a day on tier 0, 500 on tier 1, 2,000 on tier 2 and 10,000 on tier 3. Every verification counts toward it, `unknown` ones too. Over it you get `429 quota_exceeded`. Need more? [Ask](https://isbusinessemail.com/contact).
- The answer for the same address is cached (see `cached`) and doesn't open a new SMTP conversation.
- **Verifying a whole list?** Upload the CSV on your dashboard's [Bulk CSV](https://isbusinessemail.com/app/bulk) page. It runs in your browser on the same quota, retries greylisted addresses once, and gives you your file back with the results added as columns.

## Test addresses

Addresses at `test.isbusinessemail.com` return fixed answers, need no key and are never counted:

| Address | Status | Reason |
| --- | --- | --- |
| `deliverable@test.isbusinessemail.com` | deliverable | mailbox_exists |
| `undeliverable@test.isbusinessemail.com` | undeliverable | mailbox_not_found |
| `disabled@test.isbusinessemail.com` | undeliverable | mailbox_disabled |
| `catch-all@test.isbusinessemail.com` | risky | catch_all |
| `full@test.isbusinessemail.com` | risky | mailbox_full |
| `disposable@test.isbusinessemail.com` | risky | disposable |
| `unknown@test.isbusinessemail.com` | unknown | probe_blocked |
| `greylisted@test.isbusinessemail.com` | unknown | greylisted (with `retry_after`) |

## Privacy

We never store or log the address. Results are cached under a keyed hash of the address so a repeated check doesn't contact the mail server again. The reply text we return has addresses and IP addresses removed. See [privacy and data handling](https://isbusinessemail.com/docs/privacy-and-data).

## MCP

AI assistants connected to the [isBusinessEmail MCP server](https://isbusinessemail.com/docs/mcp) get the same check as the `verify_email` tool.
