> Reverse Contact API documentation. This is the Markdown copy of https://docs.reversecontact.com/endpoints/contact-email-verify
> Every page is listed at https://docs.reversecontact.com/llms.txt, every endpoint with its credit cost and status at https://docs.reversecontact.com/catalog.json

# Verify Email

> Check whether an email address can receive mail and get a verdict (valid, risky, invalid or unknown) by polling or webhook.

- Endpoint: `POST https://api.reversecontact.com/v2/contact/email/verify`
- Credits: 1 credit per verdict, whatever it says. A check that fails is refunded.
- Answer: asynchronous. The request returns a job (`data.webhookId`): poll `GET /v2/webhooks/{webhookId}?wait=20` until `status` is `succeeded` or `errored`, or receive the result on a webhook. Each poll answers as soon as the job ends, or after at most 20 seconds with the job still running; while it runs, start two polls at least `Retry-After` seconds apart (2 if the header is missing).
- Available in: REST API, TypeScript and Python SDKs.
- Last verified: 2026-11-03

## Introduction

The Verify Email endpoint checks whether an email address can receive mail, and returns one verdict: `valid`, `risky`, `invalid` or `unknown`. Processing is asynchronous: the API acknowledges the request with a `webhookId`, then you retrieve the verdict by [polling](https://docs.reversecontact.com/guides/polling) (recommended, no public endpoint required) or through a [webhook callback](https://docs.reversecontact.com/guides/webhooks) (advanced, push delivery).

Use this endpoint to clean a list before a send, or to check an address that comes from a form, a CRM or another source. Addresses returned by [Find Email](https://docs.reversecontact.com/endpoints/contact-email) are already verified, so you do not need to check them again.

This endpoint costs **1 credit** per verdict, whatever the verdict says, `unknown` included. A check that fails without a verdict is refunded. See [Verdicts](#verdicts).

> [!NOTE]
> Verify Email is asynchronous. We aim to deliver each verdict in under 15 seconds, but it can take tens of minutes depending on conditions. Use it to update databases and feed async pipelines, not to answer a request a user is waiting on. See [Polling](https://docs.reversecontact.com/guides/polling) for delivery details.

## Authorization

| Name     | Type   | Required | Description                                                                                         |
| -------- | ------ | -------- | --------------------------------------------------------------------------------------------------- |
| `apikey` | string | Yes      | Your API key from the developer dashboard. Pass it in the `Authorization: Bearer YOUR_API_KEY` header. |

## Request body

| Name         | Type   | Required | Description                                                                                         |
| ------------ | ------ | -------- | --------------------------------------------------------------------------------------------------- |
| `email`      | string | Yes      | The email address to check, such as `jane@acme.com`.                                                |
| `webhookUrl` | string | Optional | HTTPS URL that receives the result, including error outcomes. Omit it to poll for the verdict instead. |

A request with a missing or malformed `email` is refused with a `422` and costs nothing: no job starts.

## Idempotency-Key header

A check is debited when its job is created. If the response gets lost (a timeout, a dropped connection), sending the request again would start and bill a second check. To retry safely, send an `Idempotency-Key` header with a key you choose for this check, and send the same key again when you retry.

| Name              | Type   | Required | Description                                                                                                                      |
| ----------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `Idempotency-Key` | string | Optional | A key you choose for this check: 1 to 255 printable ASCII characters without spaces, such as a UUID. Use a new key for each new check. |

For 24 hours, a request with the same key and the same body returns the response of the first one, unchanged, with the same `webhookId` and the header `Idempotent-Replayed: true`. No new check starts and no credits are debited: the replayed `quotas` describe the first request. Keys are scoped to your workspace and to this endpoint. A replayed request still counts toward your per-minute rate limit.

| Status | Code                      | When                                                                                                                  |
| ------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `409`  | `IDEMPOTENCY_KEY_IN_USE`  | The first request with this key is still being processed. Retry after the `Retry-After` seconds to receive its response. |
| `422`  | `IDEMPOTENCY_KEY_REUSED`  | The key was already used with a different body. Use a new key for a new check.                                      |
| `400`  | `INVALID_IDEMPOTENCY_KEY` | The key is empty, longer than 255 characters, or contains a space or a non-ASCII character.                           |

A first request that ends in an error is not kept: it started nothing and cost nothing, so a retry with the same key runs as a new request.

**Idempotency-Key**

```bash [cURL]
curl -X POST https://api.reversecontact.com/v2/contact/email/verify \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" \
  -d '{"email":"jane@acme.com"}'
```
```typescript [TypeScript SDK]
import ReverseContact from "@reverse-contact/sdk";

const client = new ReverseContact({ apiKey: process.env.RC_API_KEY });

// The SDK sends a new Idempotency-Key on each call and reuses it on its own retries.
// To retry after a restart, store your own key with the request and pass it again.
const job = await client.contact.verifyEmail(
  { email: "jane@acme.com" },
  { idempotencyKey: "4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d" }
);
console.log(job.id, job.replayed); // replayed: true when the first response came back
```
```python [Python SDK]
import os

from reversecontact import ReverseContact

with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
    # The SDK sends a new Idempotency-Key on each call and reuses it on its own retries.
    # To retry after a restart, store your own key with the request and pass it again.
    job = client.contact.verify_email(
        email="jane@acme.com",
        idempotency_key="4f1c2a9e-3b7d-4c55-9a8e-1d2f3a4b5c6d",
    )
    print(job.id, job.replayed)  # replayed: True when the first response came back
```

## Verdicts

The verdict is the `status` field of the result. It is not the status of the job: when you poll, `data.status` is the job (`succeeded`) and `data.result.status` is the verdict.

| Verdict   | Meaning                                                                                    | What to do                                     |
| --------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------- |
| `valid`   | The address can receive mail.                                                              | Send.                                          |
| `risky`   | The address may receive mail, but delivery is not guaranteed. A catch-all domain is a common cause. | Send with care, or keep it out of a cold send. |
| `invalid` | The address cannot receive mail.                                                           | Do not send.                                   |
| `unknown` | The check was inconclusive.                                                                | Treat it as unverified. You can check it again later. |

Every verdict costs 1 credit.

## Response structure

This endpoint replies immediately with a `webhookId`. You then retrieve the result in one of two ways:

1. Polling (recommended): call `GET /v2/webhooks/:webhookId` until the status is `succeeded`. With `?wait=20`, each call waits up to 20 seconds and answers as soon as the result is ready. No webhook URL is needed. See the [Polling guide](https://docs.reversecontact.com/guides/polling).
2. Webhook callback (advanced): if you provided a `webhookUrl` (or a workspace default is configured), Reverse Contact POSTs the verdict to that URL once the check completes. See [Webhook callback payload (advanced)](#webhook-callback-payload-advanced) below.

| Field     | Type    | Description                                |
| --------- | ------- | ------------------------------------------ |
| `success` | boolean | `true` if the job was created successfully |

**data** (JobAck): Initial job acknowledgement. Save the `webhookId` to retrieve the result via polling (`GET /v2/webhooks/:webhookId`) or to correlate an upcoming webhook callback (`null` on error responses).

| Field       | Type   | Description                                                                                                                            |
| ----------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `status`    | string | Job status, always `"created"` for the initial response                                                                                |
| `webhookId` | string | Unique identifier (UUID). Save it to poll the result at `GET /v2/webhooks/:webhookId`, or to correlate an incoming webhook callback. |
| `pollUrl`   | string | Convenience hint pointing to the polling endpoint for this job (e.g. `/v2/webhooks/{webhookId}`).                                      |

| Field   | Type   | Description              |
| ------- | ------ | ------------------------ |
| `error` | `null` | Always `null` on success |

**quotas** (V2QuotaInfo): Credits and rate limit usage after this request

| Field             | Type   | Description                      |
| ----------------- | ------ | -------------------------------- |
| `creditsConsumed` | number | Credits debited when the job is created: 1 |

**workspace** (Workspace): Workspace-level limits

| Field                 | Type    | Description                                    |
| --------------------- | ------- | ---------------------------------------------- |
| `id`                  | string  | Your workspace identifier                      |
| `hasUnlimitedCredits` | boolean | Whether workspace credits are unlimited        |
| `allowDailyOvercost`  | boolean | Whether the workspace may exceed its daily cap |

**credits** (Credits): Credit balance

| Field   | Type   | Description                |
| ------- | ------ | -------------------------- |
| `total` | number | Total credits in your plan |
| `used`  | number | Credits consumed so far    |
| `left`  | number | Remaining credits          |

**dailyLimit** (DailyLimit): Daily request limit (`null` if not configured)

| Field       | Type   | Description                             |
| ----------- | ------ | --------------------------------------- |
| `limit`     | number | Maximum requests per day                |
| `used`      | number | Requests made today                     |
| `left`      | number | Remaining requests today                |
| `nextReset` | string | When the daily window resets (ISO 8601) |

**minuteRateLimit** (MinuteRateLimit): Per-minute rate limit

| Field       | Type   | Description                              |
| ----------- | ------ | ---------------------------------------- |
| `limit`     | number | Maximum requests per minute              |
| `used`      | number | Requests made in the current minute      |
| `left`      | number | Remaining requests in the current minute |
| `nextReset` | string | When the minute window resets (ISO 8601) |

**key** (ApiKey): API key-level limits (`null` when request is from dashboard)

| Field | Type   | Description        |
| ----- | ------ | ------------------ |
| `id`  | string | API key identifier |

**dailyLimit** (KeyDailyLimit): Daily limit for this key (`null` if not configured)

| Field       | Type   | Description                             |
| ----------- | ------ | --------------------------------------- |
| `limit`     | number | Maximum requests per day                |
| `used`      | number | Requests made today                     |
| `left`      | number | Remaining requests today                |
| `nextReset` | string | When the daily window resets (ISO 8601) |

**minuteRateLimit** (KeyMinuteRateLimit): Minute rate limit for this key

| Field       | Type   | Description                              |
| ----------- | ------ | ---------------------------------------- |
| `limit`     | number | Maximum requests per minute              |
| `used`      | number | Requests made in the current minute      |
| `left`      | number | Remaining requests in the current minute |
| `nextReset` | string | When the minute window resets (ISO 8601) |

**metadata** (V2ResponseMetadata): Request tracking information

| Field             | Type   | Description                          |
| ----------------- | ------ | ------------------------------------ |
| `requestId`       | string | Unique identifier for this request   |
| `executionTimeMs` | number | Total execution time in milliseconds |

## Webhook callback payload (advanced)

When the check returns a verdict, Reverse Contact sends a JSON `POST` request to your `webhookUrl`. The callback is not wrapped in the V2 response envelope. It always includes the `webhookId` from the initial acknowledgement and a `data` value.

**Success payload**

```json [Webhook callback (success)]
{
  "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
  "data": {
    "email": "jane@acme.com",
    "status": "valid"
  }
}
```

**data** (EmailVerification): The verdict for the address you sent.

| Field    | Type   | Description                                                          |
| -------- | ------ | -------------------------------------------------------------------- |
| `email`  | string | The address that was checked                                         |
| `status` | string | The verdict: `valid`, `risky`, `invalid` or `unknown`. See [Verdicts](#verdicts). |

## Webhook callback errors (advanced)

When the check cannot return a verdict, `data` holds an `errorCode` that gives the reason:

**Error payload**

```json [Webhook callback (error)]
{
  "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
  "data": {
    "errorCode": "fetch-data-error"
  }
}
```

`data.errorCode` takes one of these values:

| Code                      | Billed | Description                                                  |
| ------------------------- | ------ | ------------------------------------------------------------ |
| `invalid-request`         | No     | The address has a form the check cannot read                 |
| `fetch-data-error`        | No     | The check failed before a verdict; safe to retry             |
| `webhook-url-invalid`     | Yes    | The callback URL returned a 4xx response                     |
| `webhook-url-errored`     | Yes    | The callback URL returned a 5xx response or a network error  |
| `webhook-url-timeout`     | Yes    | The callback URL did not respond in time                     |
| `webhook-url-unreachable` | Yes    | The callback URL could not be reached                        |

> [!NOTE]
> A check that returns `invalid-request` or `fetch-data-error` costs 0 credits: the credit debited at creation is refunded. A delivery failure remains billed, because the verdict was reached: you can still read it by polling.

## Code examples

**Code Examples**

```typescript [TypeScript SDK - Polling (recommended)]
import ReverseContact, { JobFailedError } from "@reverse-contact/sdk";

const client = new ReverseContact({ apiKey: process.env.RC_API_KEY });

const job = await client.contact.verifyEmail({ email: "jane@acme.com" });
console.log("Save this job id:", job.id); // resume after a restart with client.jobs.wait(id)

try {
  // Long polls until the job ends, 30 minutes at most by default.
  const done = await job.wait();
  console.log(done.data.result?.email, done.data.result?.status);
} catch (error) {
  if (error instanceof JobFailedError) console.log("Ended without a verdict:", error.errorCode, "credits:", error.creditsConsumed);
  else throw error;
}
```
```typescript [TypeScript SDK - Webhook]
import ReverseContact, { parseWebhookDelivery } from "@reverse-contact/sdk";

const client = new ReverseContact({ apiKey: process.env.RC_API_KEY });

const job = await client.contact.verifyEmail({
  email: "jane@acme.com",
  webhookUrl: "https://your-app.com/webhooks/reversecontact"
});
console.log(job.status, job.id);

// The route behind webhookUrl receives { webhookId, data }: answer within 10 seconds.
async function handleWebhook(request: Request): Promise<Response> {
  const delivery = parseWebhookDelivery(await request.json());
  console.log(delivery.webhookId, delivery.isError ? "failed" : "succeeded", delivery.data);
  return new Response(null, { status: 200 });
}
```
```bash [cURL - Polling (recommended)]
KEY="YOUR_API_KEY"
ID=$(curl -sX POST https://api.reversecontact.com/v2/contact/email/verify \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"email":"jane@acme.com"}' | jq -r .data.webhookId)

while :; do
  S=$(curl -s https://api.reversecontact.com/v2/webhooks/$ID \
    -H "Authorization: Bearer $KEY" | jq -r .data.status)
  [ "$S" = "succeeded" ] && break
  [ "$S" = "errored" ] && echo "job failed" && exit 1
  sleep 2
done

curl -s https://api.reversecontact.com/v2/webhooks/$ID \
  -H "Authorization: Bearer $KEY" | jq .data.result
```
```bash [cURL - Webhook]
curl -X POST https://api.reversecontact.com/v2/contact/email/verify \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@acme.com",
    "webhookUrl": "https://your-app.com/webhooks/reversecontact"
  }'
```
```python [Python SDK - Polling (recommended)]
import os

from reversecontact import JobFailedError, ReverseContact

with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
    job = client.contact.verify_email(email="jane@acme.com")
    print("Save this job id:", job.id)  # resume after a restart with client.jobs.wait(id)

    try:
        # Long polls until the job ends, 30 minutes at most by default.
        done = job.wait()
        if done.data and done.data.result:
            print(done.data.result.email, done.data.result.status)
    except JobFailedError as error:
        print("Ended without a verdict:", error.error_code)
```
```python [Python SDK - Webhook]
import os

from reversecontact import ReverseContact, parse_webhook_delivery

with ReverseContact(api_key=os.environ["RC_API_KEY"]) as client:
    job = client.contact.verify_email(
        email="jane@acme.com",
        webhook_url="https://your-app.com/webhooks/reversecontact",
    )
    print(job.status, job.id)

# The route behind webhook_url receives {webhookId, data}: answer within 10 seconds.
def handle_webhook(body: bytes) -> None:
    delivery = parse_webhook_delivery(body)
    print(delivery.webhook_id, "failed" if delivery.is_error else "succeeded", delivery.data)
```

## Example response

**Example Response**

```json [Initial response]
{
  "success": true,
  "data": {
    "status": "created",
    "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
    "pollUrl": "/v2/webhooks/c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f"
  },
  "error": null,
  "metadata": {
    "requestId": "req_v1e2r3i4f5y6",
    "executionTimeMs": 95
  },
  "quotas": {
    "creditsConsumed": 1,
    "workspace": {
      "id": "ws_abc123",
      "credits": {
        "total": 10000,
        "used": 4537,
        "left": 5463
      },
      "dailyLimit": null,
      "minuteRateLimit": {
        "limit": 60,
        "used": 10,
        "left": 50,
        "nextReset": "2025-01-20T14:16:00.000Z"
      },
      "hasUnlimitedCredits": false,
      "allowDailyOvercost": false
    },
    "key": {
      "id": "key_abc123",
      "dailyLimit": null,
      "minuteRateLimit": {
        "limit": 60,
        "used": 10,
        "left": 50,
        "nextReset": "2025-01-20T14:16:00.000Z"
      }
    }
  }
}
```
```json [Polling result]
{
  "success": true,
  "data": {
    "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
    "status": "succeeded",
    "eventType": "FETCH_CONTACT_DATA",
    "errorCode": null,
    "createdAt": "2026-11-03T12:43:02.017Z",
    "result": {
      "email": "jane@acme.com",
      "status": "valid"
    },
    "creditsConsumed": 1
  }
}
```
```json [Webhook callback]
{
  "webhookId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
  "data": {
    "email": "jane@acme.com",
    "status": "valid"
  }
}
```
