# Migrate from ZeroBounce

This guide maps the ZeroBounce API v2 (single validation, batch validation, the bulk file API, and credit balance) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). ZeroBounce API behavior checked on 2026-09-30 against the [ZeroBounce API reference](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails).

## Before you begin

- Get your API key (`ta_…`) and secret (`ts_…`); see [Authentication](/authentication).
- Check what a verification costs in [Credit costs](/usage-and-quotas#credit-costs) and your plan's throttling in [Limits by plan](/rate-limits#limits-by-plan).
- Set your HTTP client timeout to at least 180 seconds. Tomba returns the finished result in the response.

## Endpoint mapping

ZeroBounce uses the hosts `api.zerobounce.net` (or `api-us`, `api-eu`) and `bulkapi.zerobounce.net`. Tomba serves everything from `api.tomba.io`.

| ZeroBounce                                        | Tomba                                                                 |
| ------------------------------------------------- | --------------------------------------------------------------------- |
| `GET /v2/validate` (or `POST`)                    | [`GET /v1/email-verifier`](/api/verifier#email-verifier)              |
| `POST /v2/validatebatch`                          | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `POST https://bulkapi.zerobounce.net/v2/sendfile` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET /v2/filestatus`                              | `GET /v1/bulk/verifier/{id}/progress`                                 |
| `GET /v2/getfile`                                 | `GET /v1/bulk/verifier/{id}/download`                                 |
| `GET /v2/deletefile`                              | `DELETE /v1/bulk/verifier/{id}/delete`                                |
| `GET /v2/getcredits`                              | [`GET /v1/me`](/api/account#get-account)                              |

## Authentication changes

ZeroBounce reads the key from the `api_key` parameter, in the query string or the request body. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string or body:

```bash
# ZeroBounce
curl "https://api.zerobounce.net/v2/validate?api_key=$ZEROBOUNCE_API_KEY&email=jane.doe@stripe.com"

# Tomba
curl "https://api.tomba.io/v1/email-verifier?email=jane.doe@stripe.com" \
  -H "X-Tomba-Key: $TOMBA_API_KEY" \
  -H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```

Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).

## Parameter mapping

| ZeroBounce                                               | Tomba                                      | Notes                                                                                       |
| -------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `api_key`                                                | `X-Tomba-Key` and `X-Tomba-Secret` headers |                                                                                             |
| `email`                                                  | `email`                                    | Required. A malformed address returns `422 params_invalid` instead of a result.             |
| `ip_address`                                             | No equivalent                              |                                                                                             |
| `timeout`                                                | No equivalent                              | Set the timeout in your HTTP client.                                                        |
| `activity_data`, `verify_plus`                           | No equivalent                              |                                                                                             |
| Batch `email_batch[].email_address`                      | `list`                                     | A newline-separated string of addresses.                                                    |
| Send file `file`                                         | `file`                                     | Must be sent with the content type `text/csv`. Also send `name`.                            |
| `email_address_column`                                   | `email_field_index`                        | Zero-based in Tomba: subtract 1.                                                            |
| `has_header_row`                                         | No equivalent                              | Tomba always reads the first row as a header.                                               |
| `return_url`                                             | `webhook_url`                              | Receives a signed event when the job ends. See [Bulk job events](/webhook#bulk-job-events). |
| `first_name_column`, `last_name_column`, `gender_column` | No equivalent                              |                                                                                             |
| `ip_address_column`, `remove_duplicate`, `allow_phase_2` | No equivalent                              |                                                                                             |
| `file_id`                                                | `{id}` in the path                         |                                                                                             |
| Get file `download_type`, `activity_data`                | No equivalent                              | Tomba's download `type` selects `full` or `valid` rows; see [Lifecycle](/bulks#lifecycle).  |

## Response field mapping

ZeroBounce returns a flat object. Tomba nests the checks in `data.email` and lists the public pages where the address appears in `data.sources`. Field definitions are in [Email verifier response](/attributes/verifier).

| ZeroBounce                             | Tomba                                    | Notes                                                                        |
| -------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------- |
| `address`                              | `data.email.email`                       | An empty string when the domain is disposable.                               |
| `status`                               | `data.email.status`, `data.email.result` | See [Status mapping](#status-mapping).                                       |
| `sub_status`                           | No equivalent                            | Tomba reports `greylisted`, `block`, `disposable`, and `mx_check` as fields. |
| `free_email`                           | `data.email.webmail`                     |                                                                              |
| `catchall_domain`                      | `data.email.accept_all`                  |                                                                              |
| `mx_found`                             | `data.email.mx_check`                    |                                                                              |
| `mx_record`                            | `data.email.mx.records`                  | An array of MX host names instead of one string.                             |
| `smtp_provider`                        | `data.email.smtp_provider`               |                                                                              |
| `domain_age_days`                      | No equivalent                            | `data.email.whois.created_date` gives the domain's registration date.        |
| `account`, `domain`                    | No equivalent                            | Split the address at `@`.                                                    |
| `did_you_mean`                         | No equivalent                            |                                                                              |
| `active_in_days`, `active_first_seen`  | No equivalent                            |                                                                              |
| `firstname`, `lastname`, `gender`      | No equivalent                            |                                                                              |
| `city`, `region`, `zipcode`, `country` | No equivalent                            |                                                                              |
| `processed_at`                         | No equivalent                            |                                                                              |
| `error`                                | `errors.message`                         | Returned with an HTTP error status; see [Error mapping](#error-mapping).     |

Tomba also returns `score`, `block`, `greylisted`, `gibberish`, `regex`, and `whois`, which have no ZeroBounce counterpart. Read your remaining balance from the `X-Verify-Remaining` header or `GET /v1/me`; see [Check usage](/usage-and-quotas#check-usage).

## Status mapping

| ZeroBounce `status` | `sub_status`          | Tomba `status`                                          | Tomba `result`  |
| ------------------- | --------------------- | ------------------------------------------------------- | --------------- |
| `valid`             | Any                   | `valid`                                                 | `deliverable`   |
| `invalid`           | Any                   | `invalid`                                               | `undeliverable` |
| `invalid`           | `failed_syntax_check` | No status. The request fails with `422 params_invalid`. |                 |
| `catch-all`         | Any                   | `accept_all`                                            | `risky`         |
| `unknown`           | Any                   | `unknown`                                               | `risky`         |
| `do_not_mail`       | `disposable`          | `disposable`                                            | Empty string    |
| `do_not_mail`       | Other sub-statuses    | No equivalent. Tomba returns the mailbox result.        |                 |
| `spamtrap`, `abuse` |                       | No equivalent. Tomba returns the mailbox result.        |                 |

ZeroBounce returns some catch-all domains as `valid` with the `accept_all` sub-status. Tomba sets `status` to `accept_all` whenever the domain's mail server accepts mail for any address. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).

## Error mapping

ZeroBounce reports failures in the body: an `error` string on single validation and credits, an `errors` array on batch validation, and `success: false` with `error_message` on the file endpoints. Tomba returns an HTTP error status with an `errors` object, so check the status code instead:

| ZeroBounce                                           | Tomba                                                                                                                        | What to do                                                                                          |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `Invalid API Key or your account ran out of credits` | `400 authentication_failed` (missing or malformed), `400 api_key_expired`, `401 authentication_failed` (wrong key or secret) | Check both headers; [rotate](/authentication#rotate-keys) an expired key.                           |
| `Invalid API Key or your account ran out of credits` | `402 quota_exceeded`                                                                                                         | Tomba separates the two cases. Wait for your usage window to renew or add credits.                  |
| Temporary block after exceeding the rate limit       | `429 rate_limit`                                                                                                             | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| No equivalent                                        | `408 proxy_error`                                                                                                            | The mailbox check failed. Retry later.                                                              |
| No equivalent                                        | `451 claimed_email`                                                                                                          | The owner of the address asked Tomba to stop processing it. Remove it from your list.               |

Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).

## Bulk and async jobs

ZeroBounce's `validatebatch` returns results in the response. Tomba runs every list as an asynchronous `verifier` bulk job, so move batch calls to the same flow as your file uploads, or send one `GET /v1/email-verifier` request per address.

| ZeroBounce                                                    | Tomba                                                                                                |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `validatebatch` with `email_batch`                            | `POST /v1/bulk/verifier` with `name` and a newline-separated `list` of addresses                     |
| `sendfile` with `file` and `email_address_column`             | `POST /v1/bulk/verifier` with `name`, a CSV `file`, and `email_field_index`                          |
| Validation starts on upload                                   | Launch the job: `PUT /v1/bulk/verifier/{id}`                                                         |
| Poll `filestatus` for `file_status` and `complete_percentage` | Poll `GET /v1/bulk/verifier/{id}/progress` for `status` and `progress` until `status` is `completed` |
| The `return_url` callback                                     | `webhook_url`. See [Bulk job events](/webhook#bulk-job-events).                                      |
| `getfile`                                                     | `GET /v1/bulk/verifier/{id}/download?file=full`                                                      |

```bash
curl -X POST "https://api.tomba.io/v1/bulk/verifier" \
  -H "X-Tomba-Key: $TOMBA_API_KEY" \
  -H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
  -F "name=Newsletter list" \
  -F "file=@emails.csv;type=text/csv" \
  -F "email_field_index=0"
```

Tomba treats the first row of the file as a header, so add one if your ZeroBounce files have none. The launch, progress, and download requests are shown in [Lifecycle](/bulks#lifecycle). Row limits and daily job limits are in [Limits](/bulks#limits), and a job is charged when you first download its results; see [Billing](/bulks#billing).

## Code example

This function replaces a call to `/v2/validate` and returns ZeroBounce's field names for the values Tomba supplies. It needs Node.js 18 or later.

```js title="validate.mjs"
const TOMBA_HEADERS = {
    "X-Tomba-Key": process.env.TOMBA_API_KEY,
    "X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};

// Tomba status -> ZeroBounce [status, sub_status]
const STATUSES = {
    valid: ["valid", ""],
    invalid: ["invalid", ""],
    accept_all: ["catch-all", ""],
    unknown: ["unknown", ""],
    disposable: ["do_not_mail", "disposable"],
};

async function validate(email) {
    const url = new URL("https://api.tomba.io/v1/email-verifier");
    url.searchParams.set("email", email);
    const res = await fetch(url, { headers: TOMBA_HEADERS });
    const body = await res.json();
    if (body.errors) {
        // Unlike ZeroBounce, errors use HTTP status codes, such as 402 or 429.
        throw new Error(
            `${res.status} ${body.errors.type}: ${body.errors.message}`,
        );
    }

    const check = body.data.email;
    const [status, sub_status] =
        STATUSES[check.status.toLowerCase()] ?? STATUSES.unknown;
    return {
        address: check.email || email,
        status,
        sub_status,
        free_email: check.webmail,
        catchall_domain: check.accept_all,
        mx_found: check.mx_check,
        mx_record: check.mx?.records?.[0] ?? null,
        smtp_provider: check.smtp_provider,
    };
}

console.log(await validate("jane.doe@stripe.com"));
```

## Cutover checklist

1. Store the Tomba key and secret as `TOMBA_API_KEY` and `TOMBA_SECRET_KEY`, send them as `X-Tomba-Key` and `X-Tomba-Secret`, and remove `api_key` from your URLs, request bodies, and logs.
2. Point single validations at `GET https://api.tomba.io/v1/email-verifier`, drop `timeout`, and set a client timeout of at least 180 seconds.
3. Replace checks of the `error` field with HTTP status handling: `402`, `422`, `429` with `Retry-After`, `408`, and `451`.
4. Translate `status` with the [status mapping](#status-mapping), comparing it case-insensitively, and stop branching on `sub_status`.
5. Replace `validatebatch` and `sendfile` with `verifier` bulk jobs: create, launch, receive the `webhook_url` event instead of `return_url`, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
