# Bulk Validation

Validate multiple email addresses and domains in a single request. Entries can be mixed in any order; DISIFY infers each entry's type. Per-request limits depend on your tier (10,000 anonymous → 100,000 on Pro — see [Authentication](https://docs.disify.com/guide/authentication.md)).

## Endpoints

`GET /api/email/{emails}/mass`
`POST /api/email`

## GET request

Pass comma-separated emails, domains, or both directly in the URL with the `/mass` suffix:

```bash
curl "https://disify.com/api/email/user@gmail.com,example.org,test@disposable-inbox.test/mass"
```

## POST request

Send the `bulk` parameter alongside comma-, space-, or newline-separated entries:


**cURL**

```bash
curl -X POST https://disify.com/api/email \
     -d "bulk=true" \
     --data-urlencode "email=user@gmail.com,example.org,test@disposable-inbox.test"
```

**Python**

```python
import requests

response = requests.post("https://disify.com/api/email", data={
    "bulk": True,
    "email": "user@gmail.com,example.org,test@disposable-inbox.test"
})
print(response.json())
```

**PHP**

```php
$ch = curl_init("https://disify.com/api/email");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    "bulk"  => true,
    "email" => "user@gmail.com,example.org,test@disposable-inbox.test"
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
```


These snippets show request encoding. Add the timeout and HTTP checks shown in the [Quick Start](https://docs.disify.com/guide/quickstart.md), and handle both response shapes below.

### POST parameters

| Parameter | Type | Description |
| --- | --- | --- |
| `bulk` | boolean | **Required.** Set to `true` to enable bulk mode. |
| `email` | string | Comma-, space-, or newline-separated emails and domains. Entries containing `@` are treated as emails; all others are treated as domains. |
| `block_privacy_relays` | boolean | Paid [privacy-relay policy](https://docs.disify.com/api/forwarding.md) override. Omit to inherit the key/account setting. |
| `domain` | string | Alternative parameter for domain-only or mixed input. Type inference still happens per entry. |
| `json` | boolean | Optional. When set, a synchronous response is wrapped as `{ "session", "stats", "results" }` — where `stats` is the summary object below and `results` maps each submitted entry to its validation result — so you don't need a second call to `/api/view/{session}`. Queued responses do not include this map. |

Send `bulk=1` to enable bulk mode and `json=1` for the optional result map; omit flags you do not need. The `features` parameter is ignored for bulk requests. Core checks run for every entry; `spam_trap` and privacy forwarding also run when included in your tier. Use single checks for explicitly requested premium features.

## Bulk response

Illustrative core summary:

```json
{
  "total": 3,
  "unique": 3,
  "invalid_format": 0,
  "invalid_dns": 0,
  "disposable": 1,
  "valid": 2,
  "session": "371ef6badf1e11510ced33107ab44bf5"
}
```

### Bulk response fields

| Field | Type | Description |
| --- | --- | --- |
| `total` | int | Number of parsed input entries before de-duplication and filtering, including empty entries between repeated commas. |
| `unique` | int | Distinct entries processed after removing duplicates, empty entries, and entries longer than 254 bytes. Counts against the per-request limit and includes invalid and disposable entries. |
| `invalid_format` | int | Count of entries with invalid format |
| `invalid_dns` | int | Count of non-disposable, valid-format entries with `dns: false`, including inconclusive DNS results |
| `disposable` | int | Count of entries detected as disposable |
| `valid` | int | Count of valid-format, usable-DNS, non-disposable entries allowed by the effective policy — these are what `/api/view/{session}` returns |
| `session` | string | 32-character hexadecimal session ID to retrieve valid results |

Paid forwarding checks additionally return `forwarding` (all known relay matches), `blocked_forwarding` (otherwise valid entries rejected as privacy relays), and `policy_indeterminate` (otherwise valid entries with an inconclusive rejection policy). The categories are evaluated in order: invalid format, disposable, invalid DNS, relay rejection, inconclusive policy, valid. Those exclusive categories sum to `unique`; `forwarding` overlaps them and must not be added to that total. Queued jobs retain the submitted policy even if settings change before processing.

## Retrieving results

`GET /api/view/{session}`

After a bulk validation, use the `session` hash to retrieve the valid emails and domains. The response body is **plain text** — one valid entry per line by default. For a batch submitted with an API key, authenticate retrieval with an active key belonging to the same account. Anonymous batches must be retrieved from the originating IP. Knowing a session ID alone does not grant access.

```bash
curl https://disify.com/api/view/d117271ce938bf91bc718f6cfb7954de
```

### Optional suffixes

| Suffix | Effect |
| --- | --- |
| `/download` | Returns the list as `application/octet-stream` with filename `valid_list.txt`. |
| `/separator` (also `/separate`, `/comma`) | Returns results comma-separated on a single line instead of newline-separated. |

Suffixes can be combined in any order:

```bash
# Comma-separated + download
curl https://disify.com/api/view/d117271ce938bf91bc718f6cfb7954de/separator/download
```

**Want per-entry results?**

Pass `json=1` on the **initial** bulk submit. The `results` map is included only if that request finishes synchronously. Queued jobs do not provide per-entry verdict maps.

An empty `results` value, or results for sequential numeric input keys, serializes as an array.

**Sessions expire after 15 minutes**

Sessions last 15 minutes. For queued jobs, this includes queue and processing time; completion does not restart the clock. Retrieve and store your results promptly.

## Synchronous and asynchronous jobs

Bulk requests of **100 or fewer** unique entries attempt synchronous processing. If the processing time budget is exhausted, the complete batch is queued instead. Larger batches are queued immediately. Always handle both response shapes, even when requesting `json=1` or submitting a small batch. A queued request returns HTTP `200` with a `pending` response:

```json
{
  "session": "d117271ce938bf91bc718f6cfb7954de",
  "status": "pending",
  "email_count": 8000,
  "estimated_seconds": 32
}
```

Poll `GET /api/view/{session}` for progress. While still running, the same endpoint returns JSON with a `status` of `pending`, `processing`, or `failed`:

```json
{
  "session": "d117271ce938bf91bc718f6cfb7954de",
  "status": "processing",
  "email_count": 8000,
  "processed_count": 3200
}
```

`email_count` counts unique email and domain entries in the job.

When complete, the endpoint normally returns the plain-text valid list (with the suffix options above), without a JSON `status` field. A completed job without a result file can return JSON summary statistics with `status: "completed"` instead. Inspect the response content type before parsing.

Configured [webhooks](https://docs.disify.com/api/webhooks.md) can send a `bulk.completed` event with the final stats. Delivery is best-effort; retain polling as a fallback.

**One async job at a time**

Only one async job per client account (or IP, for anonymous users) may run at a time. Anonymous users must also let a cooldown elapse between jobs — the number of seconds to wait is returned in the `429` error message. Retry after that interval.

## Errors

Bulk requests can fail before validation starts. Each returns a JSON `{ "error": "..." }` body with the matching status code:

| Code | When | Example message |
| --- | --- | --- |
| `413` | Bulk input exceeds 2,000,000 bytes | `Bulk input is too large.` |
| `422` | More unique addresses than your tier's per-request limit | `Maximum 10,000 unique emails allowed. Tried to load 12,500.` |
| `429` | An async job is already running, or you're inside the anonymous cooldown | `You already have a bulk job in progress. Wait for it to complete before submitting another.` / `Please wait {n} seconds before submitting another bulk job.` |
| `503` | The async queue, shared storage, or bounded processing buffer is temporarily unavailable | `Service temporarily unavailable` / `Service is busy. Please try again in a few minutes.` |

See [Error Handling](https://docs.disify.com/api/errors.md) for the full list of API status codes.

Source: https://docs.disify.com/api/bulk.html
