# Migrate from Emailable

This guide maps the Emailable API v1 (single verification, batch verification, and account info) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). Emailable API behavior checked on 2026-09-30 against the [Emailable API reference](https://emailable.com/docs/api/).

## 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, so there is no `249` response to retry.

## Endpoint mapping

Replace the base URL `https://api.emailable.com/v1` with `https://api.tomba.io/v1`.

| Emailable                    | Tomba                                                                             |
| ---------------------------- | --------------------------------------------------------------------------------- |
| `GET /v1/verify` (or `POST`) | [`GET /v1/email-verifier`](/api/verifier#email-verifier)                          |
| `POST /v1/batch`             | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch             |
| `GET /v1/batch?id=`          | `GET /v1/bulk/verifier/{id}/progress`, then `GET /v1/bulk/verifier/{id}/download` |
| `GET /v1/account`            | [`GET /v1/me`](/api/account#get-account)                                          |

## Authentication changes

Emailable reads the key from the `api_key` parameter (or an OAuth `access_token`), or from an `Authorization: Bearer` header. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string or body:

```bash
# Emailable
curl "https://api.emailable.com/v1/verify?email=jane.doe@stripe.com" \
  -H "Authorization: Bearer $EMAILABLE_API_KEY"

# 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"
```

If you call Emailable from browser code with a public API key, move the call to your server and keep the Tomba key and secret there ([Security](/security#api-credentials)). Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).

## Parameter mapping

| Emailable                                      | Tomba                                      | Notes                                                                                            |
| ---------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `api_key`, `access_token`                      | `X-Tomba-Key` and `X-Tomba-Secret` headers |                                                                                                  |
| `email`                                        | `email`                                    | Required. A malformed address returns `422 params_invalid` instead of a result.                  |
| `smtp`                                         | No equivalent                              |                                                                                                  |
| `accept_all`                                   | No equivalent                              | Every Tomba response includes `data.email.accept_all`.                                           |
| `timeout`                                      | No equivalent                              | Set the timeout in your HTTP client.                                                             |
| `captcha_response`                             | No equivalent                              |                                                                                                  |
| Batch `emails`                                 | `list`                                     | A newline-separated string, instead of a comma-separated string or JSON array. Also send `name`. |
| Batch `url`                                    | `webhook_url`                              | Receives a signed event when the job ends. See [Bulk job events](/webhook#bulk-job-events).      |
| Batch `response_fields`, `retries`, `simulate` | No equivalent                              |                                                                                                  |
| Batch status `id`                              | `{id}` in the path                         |                                                                                                  |
| Batch status `partial`                         | No equivalent                              | Results are available only as a CSV download once the job is `completed`.                        |

## Response field mapping

Emailable 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).

| Emailable                                                      | Tomba                                    | Notes                                                                    |
| -------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ |
| `email`                                                        | `data.email.email`                       | An empty string when the domain is disposable.                           |
| `state`                                                        | `data.email.status`, `data.email.result` | See [Status mapping](#status-mapping).                                   |
| `reason`                                                       | No equivalent                            | Tomba reports `greylisted`, `block`, and `mx_check` as separate fields.  |
| `score`                                                        | `data.email.score`                       | Computed differently.                                                    |
| `accept_all`                                                   | `data.email.accept_all`                  |                                                                          |
| `disposable`                                                   | `data.email.disposable`                  |                                                                          |
| `free`                                                         | `data.email.webmail`                     |                                                                          |
| `mx_record`                                                    | `data.email.mx.records`                  | An array of MX host names instead of one string.                         |
| `smtp_provider`                                                | `data.email.smtp_provider`               |                                                                          |
| `role`, `no_reply`, `mailbox_full`                             | No equivalent                            |                                                                          |
| `did_you_mean`                                                 | No equivalent                            |                                                                          |
| `user`, `domain`, `tag`                                        | No equivalent                            | Split the address yourself.                                              |
| `first_name`, `last_name`, `full_name`, `gender`, `birth_year` | No equivalent                            |                                                                          |
| `duration`                                                     | No equivalent                            |                                                                          |
| `message`                                                      | `errors.message`                         | Returned with an HTTP error status; see [Error mapping](#error-mapping). |

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

## Status mapping

| Emailable `state` | Tomba `status`                                                            | Tomba `result`                            |
| ----------------- | ------------------------------------------------------------------------- | ----------------------------------------- |
| `deliverable`     | `valid`                                                                   | `deliverable`                             |
| `undeliverable`   | `invalid`                                                                 | `undeliverable`                           |
| `risky`           | `accept_all` for a catch-all domain; `disposable` for a disposable domain | `risky`; an empty string for `disposable` |
| `unknown`         | `unknown`                                                                 | `risky`                                   |

An address that Emailable returns as `undeliverable` with the reason `invalid_email` fails syntax checks; Tomba rejects it with `422 params_invalid` instead of returning a status.

Tomba's `result` uses the same words as Emailable's `state` for `deliverable`, `undeliverable`, and `risky`, but an inconclusive check is `risky`, not `unknown`. Branch on `status` to tell them apart, and compare it case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).

## Error mapping

Both APIs signal failures with HTTP status codes. Emailable's error body is `{"message": "…"}`; Tomba's is an `errors` object with `type`, `message`, and `code`.

| Emailable               | Tomba                                                                    | What to do                                                                                          |
| ----------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `249` Try Again         | Not returned                                                             | Remove the retry loop.                                                                              |
| `400` Bad Request       | `422 params_invalid`                                                     | Fix the parameter named in the message.                                                             |
| `401` no API key        | `400 authentication_failed`                                              | Send both headers.                                                                                  |
| `403` invalid API key   | `401 authentication_failed` (wrong key or secret), `400 api_key_expired` | Check both headers; [rotate](/authentication#rotate-keys) an expired key.                           |
| `402` Payment Required  | `402 quota_exceeded`                                                     | Wait for your usage window to renew or add credits.                                                 |
| `404` batch not found   | `GET /v1/bulk/verifier/{id}` returns `404 unknown_record`                | Check the job ID.                                                                                   |
| `429` Too Many Requests | `429 rate_limit`                                                         | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `500`, `503`            | `500 api_error`                                                          | Retry with backoff.                                                                                 |
| 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

| Emailable                                                       | Tomba                                                                                                |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `POST /v1/batch` with `emails`                                  | `POST /v1/bulk/verifier` with `name` and a newline-separated `list`, or a CSV `file`                 |
| Verification starts on creation                                 | Launch the job: `PUT /v1/bulk/verifier/{id}`                                                         |
| Poll `GET /v1/batch` for `processed` and `total`                | Poll `GET /v1/bulk/verifier/{id}/progress` for `status` and `progress` until `status` is `completed` |
| The `url` callback                                              | `webhook_url`. See [Bulk job events](/webhook#bulk-job-events).                                      |
| `emails` array (up to 1,000 addresses) or a `download_file` ZIP | `GET /v1/bulk/verifier/{id}/download?file=full` returns a CSV for every job size                     |
| `total_counts`, `reason_counts`                                 | No equivalent. Count the rows of the CSV.                                                            |

```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" \
  -H "Content-Type: application/json" \
  -d '{"name": "Newsletter list", "list": "jane.doe@stripe.com\njohn.smith@shopify.com"}'
```

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 `/v1/verify` and returns Emailable's field names for the values Tomba supplies. It needs Node.js 18 or later.

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

// Tomba status -> Emailable state
const STATES = {
    valid: "deliverable",
    invalid: "undeliverable",
    accept_all: "risky",
    disposable: "risky",
    unknown: "unknown",
};

async function verify(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) {
        // Tomba never returns 249; a response is always final.
        throw new Error(
            `${res.status} ${body.errors.type}: ${body.errors.message}`,
        );
    }

    const check = body.data.email;
    return {
        email: check.email || email,
        state: STATES[check.status.toLowerCase()] ?? "unknown",
        score: check.score,
        accept_all: check.accept_all,
        disposable: check.disposable,
        free: check.webmail,
        mx_record: check.mx?.records?.[0] ?? null,
        smtp_provider: check.smtp_provider,
    };
}

console.log(await verify("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` and bearer tokens from your requests and logs.
2. Point single verifications at `GET https://api.tomba.io/v1/email-verifier`, drop `smtp`, `accept_all`, and `timeout`, and set a client timeout of at least 180 seconds.
3. Remove the `249` retry loop and swap error handling: `400` for a missing key, `401` for a wrong one, `422` for bad parameters, and `408` and `451` as new cases.
4. Translate `state` with the [status mapping](#status-mapping), branching on `status` rather than `result`, and compare it case-insensitively.
5. Replace batches with `verifier` bulk jobs: create with `list` or `file`, launch, receive the `webhook_url` event instead of the `url` callback, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
