# Migrate from Lusha

This guide maps the phone lookups of Lusha's Person API (`GET /v2/person`, `POST /v2/person`) and the v3 Search and Enrich Contacts endpoints to Tomba. Lusha API behavior checked on 2026-09-30 against the [Lusha API reference](https://docs.lusha.com/api-reference/enrichment/search-single-contact) and [Lusha API error codes](https://docs.lusha.com/v2/error-codes).

## 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).
- Set your HTTP client timeout to at least 180 seconds. Tomba returns the finished result in the response.

## Endpoint mapping

Replace the base URL `https://api.lusha.com` with `https://api.tomba.io`. Tomba picks the endpoint by the identifier you have:

| Lusha                                                        | Tomba                                                                        | Notes                                                                            |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `GET /v2/person` with `email` or `linkedinUrl`               | [`GET /v1/phone-finder`](/api/phone#phone-finder)                            | Returns phone numbers only.                                                      |
| `GET /v2/person` with `firstName`, `lastName`, and a company | [`GET /v1/email-finder`](/api/finder#email-finder) with `enrich_mobile=true` | Numbers are in `data.phone_data`, next to the email address.                     |
| `GET /v2/person` with `personId`                             | No equivalent                                                                | Look the person up by email address, LinkedIn URL, or name and company.          |
| `POST /v2/person`                                            | Single requests, or a [bulk job](#bulk-and-async-jobs)                       |                                                                                  |
| `POST /v3/contacts/search-and-enrich`                        | Single requests, or a [bulk job](#bulk-and-async-jobs)                       | Map each entry in `contacts` as a `GET /v2/person` call.                         |
| `POST /v3/contacts/search`, then `POST /v3/contacts/enrich`  | One request per person                                                       | Tomba has no separate reveal step: the lookup returns the numbers.               |
| No equivalent                                                | [`GET /v1/phone-validator`](/api/phone#phone-validator)                      | Checks a number you already have and returns its line type, carrier, and region. |

## Authentication changes

Lusha reads the key from the `api_key` header. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:

```bash
# Lusha
curl -G "https://api.lusha.com/v2/person" \
  --data-urlencode "email=jane.doe@stripe.com" \
  -H "api_key: $LUSHA_API_KEY"

# Tomba
curl -G "https://api.tomba.io/v1/phone-finder" \
  --data-urlencode "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

| Lusha                                                                                         | Tomba                     | Notes                                                                                                            |
| --------------------------------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `email`                                                                                       | `email`                   | On `/v1/phone-finder`.                                                                                           |
| `linkedinUrl`                                                                                 | `linkedin`                | On `/v1/phone-finder`. Send the full profile URL, such as `https://www.linkedin.com/in/jane-doe`.                |
| `firstName`, `lastName`                                                                       | `first_name`, `last_name` | On `/v1/email-finder` with `enrich_mobile=true`. `/v1/email-finder` also accepts `full_name`.                    |
| `companyDomain`                                                                               | `domain`                  | On `/v1/email-finder`. On `/v1/phone-finder`, `domain` returns the company's number, not the person's.           |
| `companyName`                                                                                 | `company`                 | On `/v1/email-finder`.                                                                                           |
| `revealPhones`                                                                                | Not needed                | `/v1/phone-finder` returns only phone numbers. On the finder endpoints, `enrich_mobile=true` adds them.          |
| `filterBy=phoneNumbers`                                                                       | Not needed                | A phone finder request with no number returns an empty `data` array.                                             |
| No equivalent                                                                                 | `full`                    | `full=true` returns every number found as an array; without it, `data` holds one number, preferring a valid one. |
| `personId`, `refreshJobInfo`, `partialProfile`, `revealEmails`, `signals`, `signalsStartDate` | No equivalent             |                                                                                                                  |

## Response field mapping

Lusha returns the person in `contact.data`. The Tomba phone finder returns the number itself in `data`, or an array of numbers with `full=true`:

| Lusha                                                            | Tomba                             | Notes                                                                                                                                           |
| ---------------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `contact.data.phoneNumbers[].number`, v3 `phones[].number`       | `data.e164_format`                | Also in `intl_format`, `local_format`, and `rfc3966_format`.                                                                                    |
| `contact.data.phones` (deprecated)                               | `data.e164_format`                |                                                                                                                                                 |
| `contact.data.phoneNumbers[].phoneType`, v3 `phones[].type`      | `data.line_type`                  | Different values; see [Status mapping](#status-mapping).                                                                                        |
| `contact.data.phoneNumbers[].doNotCall`, v3 `phones[].doNotCall` | No equivalent                     | Check Do Not Call registries yourself before you dial.                                                                                          |
| `contact.data.phoneNumbers[].updateDate`                         | No equivalent                     |                                                                                                                                                 |
| `contact.isCreditCharged`                                        | No equivalent                     | When a request is charged is listed in [Credit costs](/usage-and-quotas#credit-costs).                                                          |
| `contact.data.location.countryIso2`                              | No equivalent on the phone finder | `data.country_code` is the country of the number, not of the person.                                                                            |
| Name, job title, company, and social fields in `contact.data`    | No equivalent on the phone finder | `/v1/email-finder` returns the name, position, company, and LinkedIn URL. For a full profile, see the [Person API](/api/enrichment#person-api). |
| Email fields in `contact.data`                                   | No equivalent on the phone finder | Use `/v1/email-finder` or [`GET /v1/linkedin`](/api/finder#linkedin-finder).                                                                    |

Tomba also returns `valid`, `carrier`, `region`, and `timezones` for each number, and the `email`, `domain`, or `linkedin` it was found for. Every field is described in [Phone response](/attributes/phone).

## Status mapping

| Lusha                                                  | Tomba                                                                                                          |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `phoneType` `Mobile`, v3 `type` `mobile`               | `line_type` `MOBILE`                                                                                           |
| `phoneType` `Direct` or `Phone`, v3 `direct` or `work` | No direct equivalent. Tomba reports the kind of line, such as `FIXED_LINE`, `VOIP`, or `FIXED_LINE_OR_MOBILE`. |
| `contact.error.name` `EMPTY_DATA`, or `404`            | `200` with `data` as an empty array                                                                            |
| No equivalent                                          | `valid` is `false`: the number isn't valid in its country's numbering plan                                     |

Every `line_type` value is defined in [Line types](/attributes/phone#line-types). With `full=true`, filter on `valid` if you only want valid numbers.

## Error mapping

Lusha returns errors as `{statusCode, message, errors}`. Tomba returns an `errors` object with `type`, `message`, and `code`:

| Lusha                                         | Tomba                                                                                                                        | What to do                                                                                                                          |
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `400` validation failed, `412` invalid syntax | `422 params_invalid`                                                                                                         | Fix the parameter named in the message. Sending none of `email`, `domain`, and `linkedin` to `/v1/phone-finder` also returns `422`. |
| `401` missing or invalid key                  | `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.                                                                                 |
| `403` account inactive                        | `401 authentication_failed`                                                                                                  | [Contact support](https://app.tomba.io/contact).                                                                                    |
| `404` record not in the database              | `200` with `data` as an empty array                                                                                          | Treat as no match.                                                                                                                  |
| `429` rate limit or daily quota               | `429 rate_limit`                                                                                                             | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses).                                 |
| `451` GDPR restriction                        | `451 claimed_email` or `451 claimed_linkedin`                                                                                | The person asked Tomba to stop processing their data. Remove the record from your data.                                             |
| `499` client closed request                   | `504` after 180 seconds                                                                                                      | Raise your client timeout, or use a bulk job.                                                                                       |
| `500`                                         | `500 api_error`                                                                                                              | Retry with backoff.                                                                                                                 |

Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types), and Tomba's rate limit headers are in [Rate limit headers](/rate-limits#rate-limit-headers).

## Bulk and async jobs

Lusha's `POST /v2/person` and v3 contact endpoints answer up to 100 contacts in one synchronous response, keyed by your `contactId` or `clientReferenceId`. A Tomba bulk job runs in the background: create it, launch it, poll its progress, and download the results as CSV. For a few contacts, send single requests instead.

| Lusha input                            | Tomba bulk type                                                                                                               |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Contacts with `email` or `linkedinUrl` | `phone-finder`: a JSON `list` or CSV file of email addresses or LinkedIn URLs. See [Phone finder](/bulks#phone-finder).       |
| Contacts with a name and company       | `finder` with `find_phones` set to `true`: a CSV file with name and company columns. See [Email finder](/bulks#email-finder). |

A JSON `list` is de-duplicated and reordered, so join the results back to your records on the email address or LinkedIn URL, not on position. The requests for each step are in [Lifecycle](/bulks#lifecycle), and `webhook_url` sends an event when a job ends; see [Bulk job events](/webhook#bulk-job-events). A job is charged when you first download its results; see [Billing](/bulks#billing).

## Code example

This function replaces a `GET /v2/person` phone lookup and returns Lusha's `phoneNumbers` shape, or `null` when no number is found. It sends an email address or LinkedIn URL to the phone finder, and a name and company to the email finder with `enrich_mobile=true`. It needs Node.js 18 or later.

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

async function tombaGet(path, params) {
    const url = new URL(path, "https://api.tomba.io");
    for (const [key, value] of Object.entries(params)) {
        if (value) url.searchParams.set(key, value);
    }
    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}`,
        );
    }
    return body.data;
}

// Accepts Lusha's parameters: email, linkedinUrl, or firstName + lastName
// with companyDomain or companyName.
async function findPhones(params) {
    let numbers;
    if (params.email || params.linkedinUrl) {
        numbers = await tombaGet("/v1/phone-finder", {
            email: params.email,
            linkedin: params.linkedinUrl,
            full: "true",
        });
    } else {
        const person = await tombaGet("/v1/email-finder", {
            first_name: params.firstName,
            last_name: params.lastName,
            domain: params.companyDomain,
            company: params.companyName,
            enrich_mobile: "true",
        });
        numbers = person.phone_data ?? [];
    }

    if (numbers.length === 0) return null;
    return {
        phoneNumbers: numbers.map((phone) => ({
            number: phone.e164_format,
            lineType: phone.line_type,
            valid: phone.valid,
        })),
    };
}

console.log(await findPhones({ email: "jane.doe@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 `api_key`.
2. Route each lookup by its identifier: email address or LinkedIn URL to `/v1/phone-finder`, name and company to `/v1/email-finder` with `enrich_mobile=true`. Replace `personId` lookups with one of these.
3. Read numbers from `data` (with `full=true`) or `data.phone_data` instead of `contact.data.phoneNumbers`, and map `phoneType` to `line_type`.
4. Treat an empty `data` array as no match, in place of `EMPTY_DATA` and `404`.
5. Swap error handling: `422` replaces `400` and `412`, `451` carries a `claimed_*` type, and `errors` is an object.
6. Move batches of up to 100 contacts to Tomba bulk jobs, or loop over single requests.
7. Run the same sample of contacts through both services and compare the numbers before you switch production traffic.
