# Migrate from Voila Norbert

This guide maps the Voila Norbert API (`/search/name`, `/search/domain`, `/contacts`, `/massives`, `/verifier`, `/enrich`, and `/account`) to Tomba. Voila Norbert API behavior checked on 2026-09-30 against the [Voila Norbert API reference](https://api.voilanorbert.com/).

## Before you begin

- Get your API key (`ta_…`) and secret (`ts_…`); see [Authentication](/authentication).
- Check what each endpoint costs in [Credit costs](/usage-and-quotas#credit-costs) and your plan's throttling in [Limits by plan](/rate-limits#limits-by-plan).
- Voila Norbert can return a name search while it's still running, with `searching: true`, and you poll the contact until it finishes. Tomba returns the finished result in the response. Set your HTTP client timeout to at least 180 seconds.

## Endpoint mapping

Replace the base URL `https://api.voilanorbert.com/2018-01-08` with `https://api.tomba.io/v1`. Voila Norbert searches are `POST` requests; Tomba lookups are `GET` requests with query parameters.

| Voila Norbert                                 | Tomba                                                                                                                      | Notes                                                                                                       |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `POST /search/name`                           | [`GET /v1/email-finder`](/api/finder#email-finder)                                                                         |                                                                                                             |
| `GET /contacts/{id}` (polling a search)       | No equivalent                                                                                                              | Remove the polling loop.                                                                                    |
| `POST /search/domain`                         | [`GET /v1/domain-search`](/api/finder#domain-search)                                                                       |                                                                                                             |
| `POST /verifier/upload` for one address       | [`GET /v1/email-verifier`](/api/verifier#email-verifier)                                                                   | One address per request.                                                                                    |
| `POST /verifier/upload` for a list            | `POST /v1/bulk/verifier`                                                                                                   | See [Email verifier](/bulks#email-verifier).                                                                |
| `GET /verifier/{token}`, `.../download`       | `GET /v1/bulk/verifier/{id}/progress`, `.../download`                                                                      | See [Lifecycle](/bulks#lifecycle).                                                                          |
| `POST /enrich/upload` for one address         | [`GET /v1/enrich`](/api/finder#email-enrichment)                                                                           |                                                                                                             |
| `POST /enrich/upload` for a list              | `POST /v1/bulk/enrich`                                                                                                     | See [Email enrichment](/bulks#email-enrichment).                                                            |
| `GET /enrich/{token}`, `.../download`         | `GET /v1/bulk/enrich/{id}/progress`, `.../download`                                                                        | See [Lifecycle](/bulks#lifecycle).                                                                          |
| `POST /massives/`                             | `POST /v1/bulk/finder`                                                                                                     | See [Bulk and async jobs](#bulk-and-async-jobs).                                                            |
| `GET /massives/{id}`                          | `GET /v1/bulk/finder/{id}/progress`                                                                                        |                                                                                                             |
| `DELETE /massives/{id}`                       | `DELETE /v1/bulk/finder/{id}/delete`                                                                                       |                                                                                                             |
| `.../pause`, `.../resume`, `.../rerun`        | No equivalent                                                                                                              |                                                                                                             |
| `GET /lists/`, `POST /lists/`                 | [`GET /v1/leads_lists`](/api/lead-lists#retrieve-leads-lists), [`POST /v1/leads_lists`](/api/lead-lists#create-leads-list) | Tomba doesn't add search results to a list; create leads with [`POST /v1/leads`](/api/leads#create-a-lead). |
| `GET /account/`, `GET /organization/credits/` | [`GET /v1/me`](/api/account#get-account)                                                                                   | See [Check usage](/usage-and-quotas#check-usage).                                                           |

## Authentication changes

Voila Norbert uses HTTP Basic authentication with any user name and the API token as the password. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:

```bash
# Voila Norbert
curl -X POST "https://api.voilanorbert.com/2018-01-08/search/name" \
  -u "any:$NORBERT_API_TOKEN" \
  -d "name=Jane Doe" \
  -d "domain=stripe.com"

# Tomba
curl -G "https://api.tomba.io/v1/email-finder" \
  --data-urlencode "full_name=Jane Doe" \
  --data-urlencode "domain=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

| Voila Norbert                      | Tomba          | Notes                                                                                                                                                                                                                                                                                   |
| ---------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Name search `name`                 | `full_name`    | Or send `first_name` and `last_name`.                                                                                                                                                                                                                                                   |
| Name and domain search `domain`    | `domain`       | The email finder returns `400 unknown_record` for a disposable or webmail domain.                                                                                                                                                                                                       |
| Name and domain search `company`   | `company`      | A domain gives better results.                                                                                                                                                                                                                                                          |
| Domain search `page`               | `page`         | Tomba also takes `limit`; the cost depends on it. See [Credit costs](/usage-and-quotas#credit-costs).                                                                                                                                                                                   |
| `webhook`                          | `webhook_url`  | Optional in Tomba. The result is still returned in the response, and also posted to your URL when it qualifies. See [Per-request callbacks](/webhook#per-request-callbacks). Bulk jobs send a signed event to their own `webhook_url`; see [Bulk job events](/webhook#bulk-job-events). |
| `list_id`                          | No equivalent  |                                                                                                                                                                                                                                                                                         |
| Verifier and enrich `data`, `file` | `list`, `file` | Bulk jobs. A JSON `list` has one address per line, like `data`.                                                                                                                                                                                                                         |

## Response field mapping

Tomba wraps every result in `data`.

| Voila Norbert                                      | Tomba                                    | Notes                                                       |
| -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------- |
| `id`                                               | No equivalent                            | There's no contact record to fetch later.                   |
| `searching`                                        | No equivalent                            | Every Tomba response is final.                              |
| `name`                                             | `data.full_name`                         |                                                             |
| `email.email`                                      | `data.email`                             | `null` when no address is found; the status is still `200`. |
| `email.score`                                      | `data.score`, `data.verification.status` | See [Status mapping](#status-mapping).                      |
| `company.name`                                     | `data.company`                           |                                                             |
| `company.url`                                      | `data.website_url`                       | A domain in Tomba, not a URL.                               |
| Domain search `result[].email.email`               | `data.emails[].email`                    |                                                             |
| Domain search `result[].name`                      | `data.emails[].full_name`                |                                                             |
| Domain search `result[].is_new`                    | No equivalent                            |                                                             |
| Domain search `total`                              | `meta.total`                             |                                                             |
| Domain search `has_next`                           | `meta.current` < `meta.total_pages`      |                                                             |
| Verifier `email`                                   | `data.email.email`                       | An empty string when the domain is disposable.              |
| Verifier `is_deliverable`, `is_risky`, `is_bounce` | `data.email.result`                      | See [Status mapping](#status-mapping).                      |
| Verifier `error_msg`                               | No equivalent                            |                                                             |

Tomba also returns `position`, `linkedin`, `country`, and the `sources` where the address was found; see [Person](/attributes/person). The verifier fields are in [Email verifier response](/attributes/verifier).

## Status mapping

Voila Norbert scores a found address 100 when it was verified against the mail server, 80 when it wasn't, and 5 when it comes only from external sources. Tomba's `data.score` is computed differently, so read the mail server check from `data.verification.status` instead:

| Voila Norbert `email.score` | Tomba `verification.status` |
| --------------------------- | --------------------------- |
| `100`                       | `valid`                     |

Tomba can also return `accept_all`, `invalid`, or `unknown`, or `null` when the address hasn't been verified.

For the verifier, map the webhook result flags to Tomba's `result`:

| Voila Norbert verifier flag | Tomba `result`  |
| --------------------------- | --------------- |
| `is_deliverable`            | `deliverable`   |
| `is_risky`                  | `risky`         |
| `is_bounce`                 | `undeliverable` |

Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values) and [Result values](/attributes/verifier#result-values).

## Error mapping

Tomba returns an `errors` object with `type`, `message`, and `code`:

| Voila Norbert               | Tomba                                                                                                                        | What to do                                                                                                                         |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `400` bad request           | `422 params_invalid`                                                                                                         | Fix the parameter named in the message.                                                                                            |
| `401` authentication failed | `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.                                                          |
| `402` insufficient credits  | `402 quota_exceeded`                                                                                                         | Wait for your usage window to renew or add credits.                                                                                |
| `402` missing webhook       | Not returned                                                                                                                 | Tomba doesn't need a webhook.                                                                                                      |
| `429` rate limit exceeded   | `429 rate_limit`                                                                                                             | Retry after the `Retry-After` delay instead of `X-RateLimit-Reset`; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `5xx`                       | `500 api_error`                                                                                                              | Retry with backoff.                                                                                                                |

Also handle `451 claimed_email` when the owner of an address asked Tomba to stop processing it. Error responses don't consume credits; every type is listed in [Errors](/error-handling#error-types).

## Bulk and async jobs

Voila Norbert's `/massives/` endpoint takes CSV content or a file URL, and sends the result file to a required webhook. Tomba's `finder` bulk job takes an uploaded CSV file and sends no webhook: you poll its progress and download the results.

| Voila Norbert `/massives/` field | Tomba `finder` field                                                             | Notes                                                                            |
| -------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `content`, `file_url`            | `file`                                                                           | Upload the CSV as `multipart/form-data`.                                         |
| `col_name`                       | `first_name_field_index` and `last_name_field_index`, or `full_name_field_index` | Tomba's indexes start at 0. Without them, the columns are found by their header. |
| `col_domain`, `col_company`      | `domain_field_index`, `company_name_field_index`                                 |                                                                                  |
| `webhook`                        | No equivalent                                                                    | Poll the job's progress.                                                         |
| `rerun_frequency`                | No equivalent                                                                    | Create a new job; running the same list again is charged again.                  |
| `webhook_file_retention_time`    | No equivalent                                                                    | Results can be downloaded for 180 days.                                          |

Verification and enrichment lists become `verifier` and `enrich` jobs; see [Email verifier](/bulks#email-verifier) and [Email enrichment](/bulks#email-enrichment). Each job is created, launched, polled, and downloaded as CSV; see [Lifecycle](/bulks#lifecycle). A job is charged when you first download its results; see [Billing](/bulks#billing).

## Code example

This function replaces a `POST /search/name` call and its polling. It returns Voila Norbert's contact field names, with `email` set to `null` when Tomba finds no address. It needs Node.js 18 or later.

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

// Accepts Voila Norbert's parameters: name, and domain or company.
async function searchName({ name, domain, company }) {
    const url = new URL("https://api.tomba.io/v1/email-finder");
    url.searchParams.set("full_name", name);
    if (domain) url.searchParams.set("domain", domain);
    if (company) url.searchParams.set("company", company);

    const res = await fetch(url, { headers: TOMBA_HEADERS });
    const body = await res.json();
    if (body.errors) {
        throw new Error(
            `${res.status} ${body.errors.type}: ${body.errors.message}`,
        );
    }

    const found = body.data;
    return {
        name,
        searching: false,
        email: found.email
            ? {
                  email: found.email,
                  score: found.score,
                  status: found.verification?.status?.toLowerCase() ?? null,
              }
            : null,
        company: { name: found.company, url: found.website_url },
    };
}

console.log(await searchName({ name: "Jane Doe", domain: "stripe.com" }));
```

## Cutover checklist

1. Store the Tomba key and secret as `TOMBA_API_KEY` and `TOMBA_SECRET_KEY`, and send them as `X-Tomba-Key` and `X-Tomba-Secret` instead of Basic authentication.
2. Change the base URL, send lookups as `GET` requests, and send `name` as `full_name`.
3. Remove the `searching` check and the `/contacts/{id}` polling loop.
4. Replace score thresholds with `data.verification.status`, comparing it case-insensitively.
5. Move `/massives/`, `/verifier/upload`, and `/enrich/upload` lists to `finder`, `verifier`, and `enrich` bulk jobs, setting zero-based field indexes where the headers don't name the columns, and replace their webhooks with `webhook_url`.
6. Handle `402` for exhausted credits, retry `429` after `Retry-After`, and drop addresses that return `451`.
7. Run the same sample of people and addresses through both services and compare the results before you switch production traffic.
