---
title: Bulk verification endpoint
description: Submit up to 25,000 emails and domains in one request, then read their results when the verification finishes.
---

> [!NOTE]
> Bulk verification is available on all paid plans.

The bulk verification endpoints check a list of up to 25,000 email addresses and domains in one request. UserCheck verifies the list in the background, and each item gets the response body the [email](/docs/api/email-endpoint) or [domain](/docs/api/domain-endpoint) endpoint returns, without `blocklisted` (see [the result item object](#the-result-item-object)).

These are the same verifications the dashboard's bulk tool creates. Only production keys list the ones started from the dashboard and read their results.

## How it works

1. Submit the list with `POST /bulk-verifications`. The response is `202 Accepted` with the verification's `id`.
2. Fetch the verification with `GET /bulk-verifications/{id}` until its `status` is `completed` or `cancelled`. Its `items` counts show progress.
3. Page through the results with `GET /bulk-verifications/{id}/results`.

There is no webhook for a finished verification. Poll the verification instead.

Your account runs one verification at a time: a verification submitted while another is running waits with the status `queued` and starts when the earlier one finishes. An account can have at most 10 verifications queued or running at once, counting the ones started from the dashboard.

A verification belongs to the [environment](/docs/get-started/environments) of the key that submitted it, like blocklist entries and Gates. Verifications started from the dashboard belong to production. A key only lists and reads the verifications of its own environment: an ID from another environment returns `404`, the same as an ID from another account. The queue and the limit of 10 are shared across environments, like credits.

Each item that is verified costs 1 [credit](/docs/get-started/credits), the same as a single lookup. Items that are rejected as invalid, cancelled, or not verified cost nothing. Submitting, polling, and reading results are free, and submitting does not reserve credits: if your account runs out of monthly credits partway through, the remaining items are reported as `rate_limited`.

## Endpoints

| Method and path | Purpose |
|---|---|
| `POST /bulk-verifications` | [Create a bulk verification](#create-a-bulk-verification) |
| `GET /bulk-verifications` | [List bulk verifications](#list-bulk-verifications) |
| `GET /bulk-verifications/{id}` | [Get a bulk verification](#get-a-bulk-verification) |
| `GET /bulk-verifications/{id}/results` | [List results](#list-results) |
| `POST /bulk-verifications/{id}/cancel` | [Cancel a bulk verification](#cancel-a-bulk-verification) |

All endpoints live under `https://api.usercheck.com` and authenticate with your API key, like the rest of the API. See [Authentication](/docs/api/authentication).

## The bulk verification object

```json
{
  "id": "bv_x7Kq2m",
  "status": "in_progress",
  "items": {
    "total": 3,
    "pending": 1,
    "success": 1,
    "invalid": 1,
    "rate_limited": 0,
    "error": 0,
    "cancelled": 0
  },
  "created_at": "2026-11-03T10:15:42.000000Z",
  "updated_at": "2026-11-03T10:15:43.000000Z"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | The verification's ID, starting with `bv_`. Treat it as an opaque string. |
| `status` | string | `queued`, `in_progress`, `completed`, or `cancelled`. See [statuses](#verification-statuses). |
| `items` | object | How many items are in each [item status](#item-statuses). Every key is always present, and `total` is their sum. |
| `created_at` | string | ISO 8601 timestamp of the submission. |
| `updated_at` | string | ISO 8601 timestamp of the last change. It moves while the verification runs. |

Endpoints that return one verification wrap it in a top-level `data` key. The list endpoint returns the same object without `items`.

### Verification statuses

| Status | Meaning |
|---|---|
| `queued` | Waiting for another verification on your account, in any environment, to finish. |
| `in_progress` | Being verified. |
| `completed` | Every item has reached a final status. |
| `cancelled` | Stopped by a cancel before every item was verified. |

`completed` and `cancelled` are final. A verification in either status never changes again.

## The result item object

```json
{
  "value": "octocat@github.com",
  "type": "email",
  "status": "success",
  "result": {
    "status": 200,
    "email": "octocat@github.com",
    "normalized_email": "octocat@github.com",
    "domain": "github.com",
    "domain_authority": 96,
    "tld_trust": 1,
    "domain_age_in_days": 6871,
    "mx": true,
    "mx_records": [
      { "hostname": "github-com.mail.protection.outlook.com", "priority": 0 }
    ],
    "mx_providers": [
      { "slug": "microsoft", "type": "mailbox", "grade": "professional" }
    ],
    "spf": "relaxed",
    "dmarc": "quarantine",
    "disposable": false,
    "public_domain": false,
    "relay_domain": false,
    "free_subdomain": false,
    "alias": false,
    "role_account": false,
    "spam": false,
    "did_you_mean": null
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `value` | string | The item as it was verified: lowercased and trimmed. See [request body](#request-body). |
| `type` | string | `email` when the value contains `@`, otherwise `domain`. |
| `status` | string | The item's status. See [item statuses](#item-statuses). |
| `result` | object or null | The response body the [email](/docs/api/email-endpoint) or [domain](/docs/api/domain-endpoint) endpoint returned for the item, including the fields paid plans add, except `blocklisted`. Present for `success` and `invalid` items, `null` for every other status. |

Results never include `blocklisted`. To block the domains on your [blocklist](/docs/api/blocklist-endpoint), use the `domain.blocklisted` condition in [Gates](/docs/gates/rules-conditions).

### Item statuses

| Status | `result` | Credits | Meaning |
|---|---|---|---|
| `pending` | `null` | 0 | Not verified yet. |
| `success` | Response body | 1 | Verified. `result` is the `200` body. |
| `invalid` | Error body | 0 | Rejected as an invalid email address or domain. `result` is the `400` body, for example `{"status": 400, "error": "The domain is invalid."}`. |
| `rate_limited` | `null` | 0 | Not verified because the lookup was refused with `429`, which happens when your account has used its monthly credits. |
| `error` | `null` | 0 | Not verified because of a failure on UserCheck's side. |
| `cancelled` | `null` | 0 | Not verified because the verification was cancelled first. |

Items are not retried once they reach `rate_limited` or `error`. To verify them, submit them again in a new verification.

## Create a bulk verification

Submit a list of email addresses and domains to verify.

### Endpoint

```
POST /bulk-verifications
```

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `items` | array | Yes | A JSON array of 1 to 25,000 strings. Each string is an email address or a domain, and the two can be mixed. |

Each item is lowercased, and surrounding whitespace, commas, and semicolons are removed. After that:

- An item must be 255 characters or fewer.
- An item that is blank, or not a string, fails the whole request rather than being skipped, so results always line up one to one with the array you sent.
- Duplicates are kept. Each copy is verified and billed.
- Values are not checked for format at submission. An item that is not a valid email address or domain is reported as `invalid` in the results and costs nothing.

A request with invalid items lists up to 10 of them in its `422` response, keyed by their position in the array (`items.0` is the first item).

### Headers

| Header | Required | Description |
|--------|----------|-------------|
| `Idempotency-Key` | No | A key of your choosing, 1 to 255 printable ASCII characters without spaces. See [idempotent submissions](#idempotent-submissions). |

### Request example

```bash
curl -X POST "https://api.usercheck.com/bulk-verifications" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      "octocat@github.com",
      "github.com",
      "not-a-domain"
    ]
  }'
```

### Success response (202)

The `Location` header holds the verification's URL.

```json
{
  "data": {
    "id": "bv_x7Kq2m",
    "status": "in_progress",
    "items": {
      "total": 3,
      "pending": 3,
      "success": 0,
      "invalid": 0,
      "rate_limited": 0,
      "error": 0,
      "cancelled": 0
    },
    "created_at": "2026-11-03T10:15:42.000000Z",
    "updated_at": "2026-11-03T10:15:42.000000Z"
  }
}
```

`status` is `queued` instead when another verification on your account is running, in any environment.

### Idempotent submissions

Send an `Idempotency-Key` header to make a submission safe to retry. If a verification with the same `Idempotency-Key` was already submitted in the key's environment, the API does not create a second one. It answers `200` with the existing verification as it is now, a `Location` header, and `Idempotent-Replayed: true`.

The key is matched before anything else is checked: a retry is answered with the original verification even if its body differs, and is never refused by the [limit on queued verifications](#too-many-verifications-queued-or-running-429). An `Idempotency-Key` is unique within one environment of your account and does not expire. Sent from another environment, the same `Idempotency-Key` creates a separate verification.

## List bulk verifications

Get a paginated list of the verifications in the key's environment, newest first. For a production key, that includes the ones started from the dashboard. The list returns each verification without its `items` counts; get a single verification for those.

### Endpoint

```
GET /bulk-verifications
```

### Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `page` | integer | No | Page to return (default: 1) |
| `per_page` | integer | No | Verifications per page, 1 to 100 (default: 25). A value outside that range returns `422`. |

### Request example

```bash
curl -X GET "https://api.usercheck.com/bulk-verifications" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Success response (200)

```json
{
  "data": [
    {
      "id": "bv_x7Kq2m",
      "status": "completed",
      "created_at": "2026-11-03T10:15:42.000000Z",
      "updated_at": "2026-11-03T10:15:44.000000Z"
    }
  ],
  "links": {
    "first": "https://api.usercheck.com/bulk-verifications?page=1",
    "last": "https://api.usercheck.com/bulk-verifications?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "links": [
      {
        "url": null,
        "label": "&laquo; Previous",
        "active": false
      },
      {
        "url": "https://api.usercheck.com/bulk-verifications?page=1",
        "label": "1",
        "active": true
      },
      {
        "url": null,
        "label": "Next &raquo;",
        "active": false
      }
    ],
    "path": "https://api.usercheck.com/bulk-verifications",
    "per_page": 25,
    "to": 1,
    "total": 1
  }
}
```

## Get a bulk verification

Get one verification with its status and item counts. Poll this endpoint to follow a verification's progress.

### Endpoint

```
GET /bulk-verifications/{id}
```

### Path parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | The verification's ID, for example `bv_x7Kq2m` |

### Request example

```bash
curl -X GET "https://api.usercheck.com/bulk-verifications/bv_x7Kq2m" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Success response (200)

```json
{
  "data": {
    "id": "bv_x7Kq2m",
    "status": "completed",
    "items": {
      "total": 3,
      "pending": 0,
      "success": 2,
      "invalid": 1,
      "rate_limited": 0,
      "error": 0,
      "cancelled": 0
    },
    "created_at": "2026-11-03T10:15:42.000000Z",
    "updated_at": "2026-11-03T10:15:44.000000Z"
  }
}
```

## List results

Get one page of a verification's items with their results, in the order they were submitted.

### Endpoint

```
GET /bulk-verifications/{id}/results
```

### Path parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | The verification's ID |

### Query parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `per_page` | integer | No | Items per page, 1 to 1,000 (default: 1,000) |
| `cursor` | string | No | Where the page starts. Pass the `meta.next_cursor` of the previous page; omit it for the first page. |

### Request example

```bash
curl -X GET "https://api.usercheck.com/bulk-verifications/bv_x7Kq2m/results?per_page=2" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Success response (200)

```json
{
  "data": [
    {
      "value": "octocat@github.com",
      "type": "email",
      "status": "success",
      "result": {
        "status": 200,
        "email": "octocat@github.com",
        "normalized_email": "octocat@github.com",
        "domain": "github.com",
        "domain_authority": 96,
        "tld_trust": 1,
        "domain_age_in_days": 6871,
        "mx": true,
        "mx_records": [
          { "hostname": "github-com.mail.protection.outlook.com", "priority": 0 }
        ],
        "mx_providers": [
          { "slug": "microsoft", "type": "mailbox", "grade": "professional" }
        ],
        "spf": "relaxed",
        "dmarc": "quarantine",
        "disposable": false,
        "public_domain": false,
        "relay_domain": false,
        "free_subdomain": false,
        "alias": false,
        "role_account": false,
        "spam": false,
        "did_you_mean": null
      }
    },
    {
      "value": "not-a-domain",
      "type": "domain",
      "status": "invalid",
      "result": {
        "status": 400,
        "error": "The domain is invalid."
      }
    }
  ],
  "meta": {
    "per_page": 2,
    "next_cursor": "2"
  },
  "links": {
    "next": "https://api.usercheck.com/bulk-verifications/bv_x7Kq2m/results?per_page=2&cursor=2"
  }
}
```

`links.next` is the URL of the next page, with your `per_page` carried over. On the last page, `meta.next_cursor` and `links.next` are both `null`.

The cursor only moves forward. An item still `pending` when its page was read is not shown again on a later page, so the results are complete once the verification is `completed` or `cancelled`. Read them after that.

## Cancel a bulk verification

Stop a queued or running verification. Its items that have not been verified yet become `cancelled` and cost nothing. Items verified before the cancel keep their results and their credits.

### Endpoint

```
POST /bulk-verifications/{id}/cancel
```

### Path parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | The verification's ID |

### Request example

```bash
curl -X POST "https://api.usercheck.com/bulk-verifications/bv_x7Kq2m/cancel" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Success response (200)

```json
{
  "data": {
    "id": "bv_x7Kq2m",
    "status": "cancelled",
    "items": {
      "total": 3,
      "pending": 0,
      "success": 1,
      "invalid": 0,
      "rate_limited": 0,
      "error": 0,
      "cancelled": 2
    },
    "created_at": "2026-11-03T10:15:42.000000Z",
    "updated_at": "2026-11-03T10:15:43.000000Z"
  }
}
```

Cancelling a verification that is already cancelled answers `200` again, so a retried cancel is harmless. A verification that has already completed cannot be cancelled and returns `409`.

## Rate limits

The bulk verification endpoints have their own rate limit: 120 requests per minute per account, shared by all its keys. Polling and paging never use up the per-second [rate limit](/docs/api/rate-limits) your lookups and Gates decisions rely on, and the items a verification checks don't count against it either.

Exceeding the limit returns `429` with a `Retry-After` header:

```json
{
  "error": "rate_limit_exceeded",
  "message": "Too many requests"
}
```

Requests with a missing, mistyped, or revoked key are limited to 5 per hour per IP address. Past that, they return `429` instead of `401`.

## Error responses

### Unauthorized (401)

```json
{
  "message": "API key is required"
}
```

`message` is `"API key is required"` when the request carried no API key, and `"Invalid API key"` when the key is not recognized.

### Paid plan required (403)

```json
{
  "message": "This feature requires a Pro account"
}
```

### Verification not found (404)

```json
{
  "message": "Bulk verification not found."
}
```

Returned for an ID that does not exist, and for one that belongs to another account or to another environment than the key's.

### Already completed (409)

Returned when cancelling a verification that has already completed.

```json
{
  "message": "This bulk verification has already completed."
}
```

### Misdirected request (421)

Rare, and only on submissions and cancellations. Nothing was created or cancelled, and the request can be retried as is.

```json
{
  "message": "Bulk verification submissions and cancellations are processed in eu-west-1, and this request reached another region. Retry it."
}
```

### Validation failed (422)

```json
{
  "message": "You can submit up to 25,000 items per bulk verification. This request contains 25001.",
  "errors": {
    "items": ["You can submit up to 25,000 items per bulk verification. This request contains 25001."]
  }
}
```

`message` carries the first error from `errors`, with `(and N more errors)` appended when there is more than one. Errors on single items are keyed by position:

```json
{
  "message": "Each item must be 255 characters or fewer.",
  "errors": {
    "items.2": ["Each item must be 255 characters or fewer."]
  }
}
```

An invalid `Idempotency-Key` header is reported under the `Idempotency-Key` key.

### Too many verifications queued or running (429)

Returned when your account already has 10 verifications queued or running, counting every environment and the ones started from the dashboard. Your key may not be able to list or cancel all of them: it only sees its own environment's. It carries no `Retry-After` header: a new submission is accepted as soon as one of them finishes.

```json
{
  "message": "Your account has 10 bulk verifications queued or running, across all its environments and the dashboard. Submit more once one finishes."
}
```

A `429` from the [rate limit](#rate-limits) has a different body, shown above.
