# Migrate from Apollo.io

This guide maps the Apollo.io People Enrichment (`POST /api/v1/people/match`) and Bulk People Enrichment (`POST /api/v1/people/bulk_match`) endpoints to Tomba. Apollo.io API behavior checked on 2026-09-30 against the [Apollo.io API reference](https://docs.apollo.io/reference/people-enrichment).

## 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).
- Apollo.io matches a person from whichever identifiers you send. Tomba has one endpoint per input, so pick the endpoint from the identifiers you have: a name and company, a LinkedIn URL, or an email address.
- Set your HTTP client timeout to at least 180 seconds. Tomba returns the finished result in the response, including phone numbers.

## Endpoint mapping

Replace the base URL `https://api.apollo.io/api/v1` with `https://api.tomba.io/v1`. Apollo.io takes `POST` requests; Tomba lookups are `GET` requests with query parameters.

| Apollo.io                                                            | Tomba                                               | Notes                                                                                      |
| -------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `POST /people/match` with a name and `domain` or `organization_name` | [`GET /v1/email-finder`](/api/finder#email-finder)  | Returns the person's work address.                                                         |
| `POST /people/match` with `linkedin_url`                             | [`GET /v1/linkedin`](/api/finder#linkedin-finder)   | Returns the same fields as the email finder.                                               |
| `POST /people/match` with `email`                                    | [`GET /v1/people/find`](/api/enrichment#person-api) | Returns the person in the Clearbit-style format; see [Enrichment](/attributes/enrichment). |
| `POST /people/match` with `reveal_phone_number`                      | [`GET /v1/phone-finder`](/api/phone#phone-finder)   | Or add `enrich_mobile=true` to the email finder or LinkedIn finder request.                |
| `POST /people/bulk_match`                                            | Bulk jobs                                           | See [Bulk and async jobs](#bulk-and-async-jobs).                                           |

## Authentication changes

Apollo.io reads the key from the `x-api-key` header, or an OAuth 2.0 bearer token. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`. Tomba also supports OAuth 2.0; see [OAuth 2.0](/authentication#oauth-20).

```bash
# Apollo.io
curl -X POST "https://api.apollo.io/api/v1/people/match?first_name=Jane&last_name=Doe&domain=stripe.com" \
  -H "x-api-key: $APOLLO_API_KEY"

# Tomba
curl "https://api.tomba.io/v1/email-finder?first_name=Jane&last_name=Doe&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

| Apollo.io                                    | Tomba                                                          | Notes                                                                                                                                                                        |
| -------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `first_name`, `last_name`                    | `first_name`, `last_name`                                      | Email finder.                                                                                                                                                                |
| `name`                                       | `full_name`                                                    | Email finder. Used when `first_name` and `last_name` aren't both sent.                                                                                                       |
| `domain`                                     | `domain`                                                       | Email finder. A disposable or webmail domain returns `400 unknown_record`.                                                                                                   |
| `organization_name`                          | `company`                                                      | Email finder. Apollo.io also matches a previous employer; Tomba searches the company you send. A domain gives better results.                                                |
| `linkedin_url`                               | `url` on [`GET /v1/linkedin`](/api/finder#linkedin-finder)     |                                                                                                                                                                              |
| `email`                                      | `email` on [`GET /v1/people/find`](/api/enrichment#person-api) |                                                                                                                                                                              |
| `reveal_phone_number`                        | `enrich_mobile=true`                                           | Numbers are returned in `data.phone_data` in the same response, at an extra cost; no webhook is needed.                                                                      |
| `webhook_url`                                | `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). |
| `id`, `hashed_email`                         | No equivalent                                                  |                                                                                                                                                                              |
| `reveal_personal_emails`                     | No equivalent                                                  | Tomba returns work addresses.                                                                                                                                                |
| `run_waterfall_email`, `run_waterfall_phone` | No equivalent                                                  |                                                                                                                                                                              |
| `poll_only`                                  | No equivalent                                                  | Every Tomba response is final.                                                                                                                                               |

## Response field mapping

Apollo.io returns the person in `person`. Tomba's email finder and LinkedIn finder return it in `data`; the fields are described in [Person](/attributes/person).

| Apollo.io                                    | Tomba                               | Notes                                                                                                                  |
| -------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `person.email`                               | `data.email`                        | `null` when no address is found; the status is still `200`.                                                            |
| `person.email_status`                        | `data.verification.status`          | See [Status mapping](#status-mapping).                                                                                 |
| `person.first_name`, `last_name`             | `data.first_name`, `data.last_name` |                                                                                                                        |
| `person.name`                                | `data.full_name`                    |                                                                                                                        |
| `person.title`                               | `data.position`                     |                                                                                                                        |
| `person.linkedin_url`                        | `data.linkedin`                     |                                                                                                                        |
| `person.country`                             | `data.country`                      | A two-letter country code in Tomba.                                                                                    |
| `person.departments[]`                       | `data.department`                   | One department. The department names differ.                                                                           |
| `person.organization`                        | `data.company`, `data.website_url`  | The company name and domain. For a full company profile, call [`GET /v1/companies/find`](/api/enrichment#company-api). |
| `person.phone_numbers[].sanitized_number`    | `data.phone_data[].e164_format`     | With `enrich_mobile=true`. `line_type` gives the number type; see [Phone response](/attributes/phone).                 |
| `person.match_confidence`                    | `data.score`                        | Computed differently. Recalibrate any threshold you apply to it.                                                       |
| `person.city`, `state`                       | No equivalent                       | [`GET /v1/people/find`](/api/enrichment#person-api) returns `data.geo.city` and `data.geo.state` when Tomba has them.  |
| `person.seniority`                           | No equivalent                       | [`GET /v1/people/find`](/api/enrichment#person-api) returns `data.employment.seniority` when Tomba has it.             |
| `person.id`, `contact_id`, `organization_id` | No equivalent                       |                                                                                                                        |
| `person.headline`, `employment_history`      | No equivalent                       |                                                                                                                        |
| `request_id`, `waterfall`                    | No equivalent                       | Tomba returns a request ID in the `X-Request-ID` header; see [Going to production](/going-to-production).              |

Tomba also returns `pattern`, `type`, `gender`, `accept_all`, and the `sources` where the address was found. For the email lookup, `GET /v1/people/find` returns `data.name`, `data.employment`, `data.geo`, and `data.linkedin.handle`; see [Enrichment](/attributes/enrichment).

## Status mapping

| Apollo.io `email_status` | Tomba `verification.status` |
| ------------------------ | --------------------------- |
| `verified`               | `valid`                     |

Tomba also returns `accept_all`, `invalid`, and `unknown`, or `null` when the address hasn't been verified. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).

## Error mapping

Apollo.io returns an `error` string. Tomba returns an `errors` object with `type`, `message`, and `code`:

| Apollo.io                                              | Tomba                                                                                                                        | What to do                                                                                          |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `400` validation error                                 | `422 params_invalid`                                                                                                         | Fix the parameter named in the message.                                                             |
| `400` missing `webhook_url` with `reveal_phone_number` | Not returned                                                                                                                 | Tomba doesn't need a webhook for phone numbers.                                                     |
| `401` invalid API 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.                           |
| `429` rate limit exceeded                              | `429 rate_limit`                                                                                                             | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |

Also handle `402 quota_exceeded` when you run out of credits, `400 unknown_record` when the email finder receives a disposable or webmail domain, and `451 claimed_email` when the owner of an address asked Tomba to stop processing it. `GET /v1/people/find` returns `200` with an `errors` object of type `not_found` when Tomba has no data for the address, so check the body for `errors`. Error responses don't consume credits; every type is listed in [Errors](/error-handling#error-types).

Apollo.io reports its limits in `x-rate-limit-*` and `x-*-requests-left` headers. Tomba's headers are described in [Rate limit headers](/rate-limits#rate-limit-headers).

## Bulk and async jobs

Apollo.io's bulk endpoint takes up to 10 people in `details[]` and returns `matches[]` in the response. Tomba runs lists as bulk jobs; row limits per type are in [Bulk types](/bulks#bulk-types).

| Apollo.io `details[]` entries | Tomba bulk type                                                                                         |
| ----------------------------- | ------------------------------------------------------------------------------------------------------- |
| Name and `domain` or company  | `finder`: a CSV file with name and company columns. See [Email finder](/bulks#email-finder).            |
| `linkedin_url`                | `linkedin`: a JSON `list` of profile URLs or a CSV file. See [LinkedIn finder](/bulks#linkedin-finder). |
| `email`                       | `enrich`: a JSON `list` of addresses or a CSV file. See [Email enrichment](/bulks#email-enrichment).    |
| `reveal_phone_number`         | Set `phone` to `true` on the job, at an extra cost.                                                     |

Each job is created, launched, polled, and downloaded as CSV; see [Lifecycle](/bulks#lifecycle). To be notified when a job ends, set `webhook_url`; see [Bulk job events](/webhook#bulk-job-events). A job is charged when you first download its results; see [Billing](/bulks#billing).

The bulk response fields `total_requested_enrichments`, `unique_enriched_records`, `missing_records`, and `credits_consumed` have no equivalent. Poll the job's progress instead.

## Code example

This function replaces a `people/match` call made with a name and company or with a LinkedIn URL. It returns Apollo.io's `person` field names, or `null` when Tomba finds no address. It needs Node.js 18 or later.

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

// Accepts Apollo.io's parameters: linkedin_url, or first_name + last_name
// (or name) with domain or organization_name.
async function peopleMatch(params) {
    let url;
    if (params.linkedin_url) {
        url = new URL("https://api.tomba.io/v1/linkedin");
        url.searchParams.set("url", params.linkedin_url);
    } else {
        url = new URL("https://api.tomba.io/v1/email-finder");
        const map = {
            first_name: "first_name",
            last_name: "last_name",
            name: "full_name",
            domain: "domain",
            organization_name: "company",
        };
        for (const [apollo, tomba] of Object.entries(map)) {
            if (params[apollo]) url.searchParams.set(tomba, params[apollo]);
        }
    }
    if (params.reveal_phone_number)
        url.searchParams.set("enrich_mobile", "true");

    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;
    if (!found.email) return null;
    const status = found.verification?.status?.toLowerCase() ?? null;
    return {
        first_name: found.first_name,
        last_name: found.last_name,
        name: found.full_name,
        email: found.email,
        // Apollo.io's "verified" is Tomba's "valid"; other statuses pass through.
        email_status: status === "valid" ? "verified" : status,
        title: found.position,
        linkedin_url: found.linkedin,
        country: found.country,
        phone_numbers: (found.phone_data ?? []).map((phone) => ({
            sanitized_number: phone.e164_format,
        })),
    };
}

console.log(
    await peopleMatch({
        first_name: "Jane",
        last_name: "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 `x-api-key`.
2. Route each `people/match` call by its input: a name and company to `/v1/email-finder`, a LinkedIn URL to `/v1/linkedin`, and an email address to `/v1/people/find`.
3. Replace `reveal_phone_number` and its webhook with `enrich_mobile=true`, and read the numbers from `data.phone_data`.
4. Update response parsing: read `data` instead of `person`, `position` instead of `title`, `linkedin` instead of `linkedin_url`, and translate `email_status` with the [status mapping](#status-mapping).
5. Handle `402` for exhausted credits, retry `429` after `Retry-After`, and drop addresses that return `451`.
6. Move `bulk_match` batches to `finder`, `linkedin`, or `enrich` bulk jobs.
7. Run the same sample of people through both services and compare the results before you switch production traffic.
