# Migrate from Kaspr

This guide maps Kaspr's Get LinkedIn Profile endpoint and its key endpoints to Tomba: phone numbers to Phone Finder, work emails to LinkedIn Finder. Kaspr API behavior checked on 2026-09-30 against the [Kaspr API reference](https://kaspr.stoplight.io/docs/kaspr-api/branches/main/2ptd62aajjv62-introduction).

## 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.developers.kaspr.io` with `https://api.tomba.io/v1`. Kaspr returns phones and emails from one `POST` request; Tomba splits them across two `GET` endpoints that both take the LinkedIn profile URL.

| Kaspr                                                 | Tomba                                                      | Notes                                                                                            |
| ----------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `POST /profile/linkedin` with `dataToGet` `phone`     | [`GET /v1/phone-finder`](/api/phone#phone-finder)          | Send the profile URL as `linkedin`. Set `full=true` to get every number, as in Kaspr's `phones`. |
| `POST /profile/linkedin` with `dataToGet` `workEmail` | [`GET /v1/linkedin`](/api/finder#linkedin-finder)          | Send the profile URL as `url`.                                                                   |
| `POST /profile/linkedin` for phones and a work email  | [`GET /v1/linkedin`](/api/finder#linkedin-finder)          | Add `enrich_mobile=true` to get the person's numbers in `data.phone_data` in the same response.  |
| Numbers you already stored from Kaspr                 | [`GET /v1/phone-validator`](/api/phone#phone-validator)    | Checks validity and returns line type, carrier, and region for a number you have.                |
| `GET /keys/verifyKey`                                 | [`GET /v1/me`](/api/account#get-account)                   | Wrong credentials return an authentication error.                                                |
| `GET /keys/remainingCredits`                          | [`GET /v1/me`](/api/account#get-account)                   | See [Check usage](/usage-and-quotas#check-usage).                                                |
| `GET /keys/rateLimits`                                | [`GET /v1/rate-limits`](/api/account#retrieve-rate-limits) | See [Check your limits](/rate-limits#check-your-limits).                                         |

## Authentication changes

Kaspr takes the key in an `authorization: Bearer` header and needs `accept-version: v2.0` on the profile endpoint. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and has no version header:

```bash
# Kaspr
curl -X POST "https://api.developers.kaspr.io/profile/linkedin" \
  -H "authorization: Bearer $KASPR_API_KEY" \
  -H "accept-version: v2.0" \
  -H "Content-Type: application/json" \
  -d '{"id": "jane-doe", "name": "Jane Doe", "dataToGet": ["phone"]}'

# Tomba
curl "https://api.tomba.io/v1/phone-finder?linkedin=https://www.linkedin.com/in/jane-doe&full=true" \
  -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

Kaspr's body fields become query parameters:

| Kaspr          | Tomba                                                | Notes                                                                                                                                                              |
| -------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`           | `linkedin` on Phone Finder, `url` on LinkedIn Finder | Send a full profile URL, such as `https://www.linkedin.com/in/jane-doe`. When you stored only Kaspr's profile ID, build the URL from it.                           |
| `name`         | No equivalent                                        | Tomba doesn't need the person's name for a LinkedIn lookup.                                                                                                        |
| `dataToGet`    | Choice of endpoint                                   | Call Phone Finder for `phone` and LinkedIn Finder for `workEmail`, or LinkedIn Finder with `enrich_mobile=true` for both.                                          |
| `requiredData` | No equivalent                                        | Both endpoints return what they find. Check the response in your code and discard results that lack the data you need.                                             |
| No equivalent  | `full` on Phone Finder                               | `true` returns every number found as an array. Without it, `data` is one number, preferring a valid one.                                                           |
| No equivalent  | `email`, `domain` on Phone Finder                    | Send them with `linkedin` to also look up numbers by email address or company domain; the results are merged without duplicates. `domain` returns company numbers. |
| No equivalent  | `webhook_url`                                        | Also sends the result to your URL; see [Webhooks](/webhook#per-request-callbacks). Phone Finder sends no callback with `full=true`.                                |

## Response field mapping

Kaspr returns the person under `profile`. Tomba returns `data`: a phone number object on Phone Finder, and a person object on LinkedIn Finder.

| Kaspr                                             | Tomba                                                                                | Notes                                                                                                                                              |
| ------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `profile.phones[]`                                | Phone Finder with `full=true`: `data[].intl_format`, `data[].e164_format`            | Kaspr returns strings such as `+1 415 555 0100`. Tomba returns each number in several formats; see [Phone response](/attributes/phone#fields).     |
| `profile.starryPhone`                             | Phone Finder without `full`: `data.intl_format`                                      | Tomba's single number prefers a valid one.                                                                                                         |
| `profile.phones[]` alongside a work email         | LinkedIn Finder with `enrich_mobile=true`: `data.phone_data[]`                       | `data.phone_number` is `true` when Tomba has a number for the person. `phone_data` isn't returned with `full=true`.                                |
| `profile.professionalEmails[]`                    | LinkedIn Finder with `full=true`: `data[].email`                                     | Up to two stored addresses.                                                                                                                        |
| `profile.starryProfessionalEmail`                 | LinkedIn Finder: `data.email`                                                        | The single most likely address. `null` when none is found.                                                                                         |
| `profile.personalEmails[]`, `starryPersonalEmail` | No equivalent                                                                        | Tomba's `type` field is `personal` for an address that belongs to one person and `generic` for a role address; it doesn't mark personal mailboxes. |
| `profile.name`, `firstName`, `lastName`           | LinkedIn Finder: `data.full_name`, `first_name`, `last_name`                         |                                                                                                                                                    |
| `profile.id`                                      | LinkedIn Finder: `data.linkedin`                                                     | Tomba returns the full profile URL.                                                                                                                |
| `profile.title`                                   | LinkedIn Finder: `data.position`                                                     |                                                                                                                                                    |
| `profile.location`                                | LinkedIn Finder: `data.country`                                                      | A two-letter country code only.                                                                                                                    |
| `profile.company.name`, `company.domains[]`       | LinkedIn Finder: `data.company`, `data.website_url`                                  | For the other `company` fields, call [`GET /v1/companies/find`](/api/enrichment#company-api) with the domain.                                      |
| `fetchedAt`, `processingTime`                     | No equivalent                                                                        |                                                                                                                                                    |
| No equivalent                                     | Phone Finder: `valid`, `line_type`, `carrier`, `country_code`, `region`, `timezones` | See [Phone response](/attributes/phone#fields).                                                                                                    |
| No equivalent                                     | LinkedIn Finder: `score`, `verification`, `sources`                                  | See [Person response](/attributes/person).                                                                                                         |

When Phone Finder finds nothing, the status is `200`, `data` is an empty array, and the response repeats the `linkedin` you sent, reduced to its permalink such as `jane-doe`.

## Status mapping

Kaspr returns phones and emails without a status. Tomba adds one to each result:

| Result       | Tomba field                | Values                                                                                                                                                                  |
| ------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Phone number | `valid`                    | `true` when the number is valid in its country's numbering plan.                                                                                                        |
| Phone number | `line_type`                | `MOBILE`, `FIXED_LINE`, `VOIP`, and others; see [Line types](/attributes/phone#line-types).                                                                             |
| Work email   | `data.verification.status` | `valid`, `accept_all`, `invalid`, `unknown`, or `disposable`, or `null` when the address hasn't been verified; see [Status values](/attributes/verifier#status-values). |

To keep only dialable numbers, filter Phone Finder results on `valid: true`, and on `line_type` `MOBILE` if you call mobiles only.

## Error mapping

Kaspr returns `{message, reason}`. Tomba returns an `errors` object with `type`, `message`, and `code`:

| Kaspr                                  | Tomba                | What to do                                                                                          |
| -------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------- |
| `402` `No credits left`                | `402 quota_exceeded` | Wait for your usage window to renew or add credits.                                                 |
| `429` `Too many requests`, key blocked | `429 rate_limit`     | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |

Also handle Tomba's `400` and `401 authentication_failed` for missing or wrong credentials, `400 api_key_expired` for an expired key, `422 params_invalid` for a malformed LinkedIn URL or a Phone Finder request without `email`, `domain`, or `linkedin`, and `451 claimed_linkedin` or `451 claimed_email` when the person asked Tomba to stop processing their data. Error responses don't consume credits; every type is listed in [Errors](/error-handling#error-types).

Kaspr's rate-limit headers (`X-Daily-RateLimit-*`, `X-Hourly-RateLimit-*`, `X-Minutely-*`) have Tomba counterparts listed in [Rate limit headers](/rate-limits#rate-limit-headers).

## Bulk and async jobs

Kaspr's API takes one profile per request. Tomba also runs lists of profiles as bulk jobs:

| Task                              | Tomba bulk type                                                                                                                  |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Phone numbers for many profiles   | `phone-finder`: a JSON `list` of LinkedIn URLs, or a CSV file with a `linkedin` column. See [Phone finder](/bulks#phone-finder). |
| Work emails for many profiles     | `linkedin`: a JSON `list` of profile URLs. See [LinkedIn finder](/bulks#linkedin-finder).                                        |
| Checking numbers you already have | `phone-validator`: a JSON `list` of numbers. See [Phone validator](/bulks#phone-validator).                                      |

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

## Code example

This function replaces a call to Kaspr's Get LinkedIn Profile endpoint and returns Kaspr's `profile` field names, or `null` when Tomba finds neither a number nor an address. It needs Node.js 18 or later.

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

async function tomba(path, params) {
    const url = new URL(`https://api.tomba.io/v1/${path}`);
    for (const [key, value] of Object.entries(params)) {
        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 Kaspr's body: id (profile ID or URL) and dataToGet.
async function getLinkedinProfile({ id, dataToGet = ["phone", "workEmail"] }) {
    const linkedin = id.startsWith("http")
        ? id
        : `https://www.linkedin.com/in/${id}`;

    const [phones, person] = await Promise.all([
        dataToGet.includes("phone")
            ? tomba("phone-finder", { linkedin, full: "true" })
            : [],
        dataToGet.includes("workEmail")
            ? tomba("linkedin", { url: linkedin })
            : null,
    ]);

    const numbers = phones.map((phone) => phone.intl_format);
    const starry = phones.find((phone) => phone.valid) ?? phones[0];
    if (numbers.length === 0 && !person?.email) return null;

    return {
        id,
        name: person?.full_name ?? null,
        firstName: person?.first_name ?? null,
        lastName: person?.last_name ?? null,
        professionalEmails: person?.email ? [person.email] : [],
        starryProfessionalEmail: person?.email ?? null,
        phones: numbers,
        starryPhone: starry?.intl_format ?? null,
        title: person?.position ?? null,
        companyName: person?.company ?? null,
    };
}

console.log(
    await getLinkedinProfile({
        id: "https://www.linkedin.com/in/jane-doe",
        dataToGet: ["phone", "workEmail"],
    }),
);
```

## 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 the `authorization: Bearer` and `accept-version` headers.
2. Replace each `POST /profile/linkedin` with `GET` requests: Phone Finder for phones, LinkedIn Finder for work emails, and send full profile URLs instead of profile IDs.
3. Update response parsing: read numbers from `data` or `data[]` instead of `profile.phones`, handle an empty `data` array as no result, and drop code that reads personal emails.
4. Replace `requiredData` with checks in your code, and filter numbers on `valid` and `line_type`.
5. Swap error handling to read the `errors` object, and move to Tomba's rate-limit headers and `Retry-After`.
6. Move list processing to `phone-finder` and `linkedin` bulk jobs.
7. Run the same sample of LinkedIn profiles through both services and compare the results before you switch production traffic.
