# Tomba API documentation
> Complete documentation for Large Language Models
---
## Document: Webhooks
Receive Tomba results at your own URL: per-request callbacks with webhook_url, signed bulk job events, and the lead webhook set in the dashboard.
URL: /webhook
# Webhooks
Tomba can send data to your server in three ways: a callback for an individual API request when you add `webhook_url`, a signed event when a bulk job created with `webhook_url` ends, and a lead webhook that fires whenever a lead is created in your account.
## Per-request callbacks
Add `webhook_url` to a supported request. The API still returns the result in the response as usual. When the result qualifies, Tomba also sends it to your URL.
```bash
curl -G "https://api.tomba.io/v1/email-verifier" \
--data-urlencode "email=jane.doe@stripe.com" \
--data-urlencode "webhook_url=https://hooks.example.com/tomba/7f3c9a" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
### Supported endpoints
| Endpoint | Callback sent when |
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
| [`GET /domain-search`](/api/finder#domain-search) | At least one email address is returned |
| [`GET /email-finder`](/api/finder#email-finder) | An address is returned with a verification status other than `unknown` |
| [`GET /author-finder`](/api/finder#author-finder) | An address is returned |
| [`GET /enrich`](/api/finder#email-enrichment) | An address is returned with a verification status other than `unknown` |
| [`GET /linkedin`](/api/finder#linkedin-finder) | An address is returned |
| [`GET /email-verifier`](/api/verifier#email-verifier) | The status is not `disposable` or `unknown` |
| [`GET /phone-finder`](/api/phone#phone-finder) | A single valid phone number is returned. Not sent with `full=true`. |
| [`GET /people/find`](/api/enrichment#person-api) | A person is returned |
| [`GET /combined/find`](/api/enrichment#combined-api) | A person is returned |
Other endpoints ignore `webhook_url`.
### Delivery
Tomba sends a `POST` request whose body is the same JSON as the API response. These headers are included:
| Header | Value |
| ------------------- | ---------------------------------------------- |
| `Content-Type` | `application/json` |
| `Accept` | `application/json` |
| `User-Agent` | `Tomba-Webhook/1.0` |
| `X-Tomba-Timestamp` | Unix time, in seconds, of the delivery attempt |
- Each attempt times out after 30 seconds. A network error or a non-2xx response is retried, up to 4 attempts, 2 seconds apart. Respond with a 2xx status as soon as you've stored the payload.
- Callbacks aren't signed. Put a hard-to-guess token in the URL, as in the example above, and reject requests that don't carry it.
- The URL must use a public domain name; IP addresses and `localhost` fail with `422`. Plain `http://` URLs are accepted, but use `https://`.
- A callback is also sent for a [duplicate request](/usage-and-quotas#duplicate-requests), which isn't charged. Changing `webhook_url` makes the request a new one for the duplicate rule.
## Bulk job events
Set `webhook_url` when you [create a bulk job](/bulks#create-a-job), and Tomba sends a signed `POST` request to that URL when the job ends:
| `type` | Sent when |
| ---------------- | ---------------------------------------------------------------------------------------- |
| `bulk.completed` | The job completed. `data.metrics` holds its results, and `data.links.download` its file. |
| `bulk.failed` | The job failed. `data.error_message` says why. |
| `bulk.cancelled` | The job was cancelled while running. Cancelling a `pending` job sends no event. |
A retried job sends a new event when it ends, with `data.attempt` increased by one.
```json
{
"type": "bulk.completed",
"created_at": "2026-09-29T09:16:19Z",
"data": {
"bulk_id": 4521,
"bulk_type": "search",
"name": "Q3 target accounts",
"status": "completed",
"attempt": 1,
"started_at": "2026-09-29T09:14:05Z",
"completed_at": "2026-09-29T09:16:18Z",
"metrics": {
"v": 1,
"rows": {
"input": 3,
"valid_input": 3,
"invalid_input": 0,
"duplicates": 0,
"skipped": 0,
"processed": 3,
"errors": 0
},
"results": {
"found": 3,
"not_found": 0,
"hit_rate": 100,
"records": 24
},
"distribution": {},
"emails": {
"total": 24,
"per_found_row": 8,
"with_sources": 21,
"webmail": 0,
"deliverable_rate": 75
},
"errors": { "client": 0, "server": 0, "rate_limited": 0 },
"credits": { "search": 3, "verify": 24, "phone": 0, "total": 27 },
"timing": {
"queued_ms": 1840,
"duration_ms": 133520,
"rows_per_minute": 1.3
}
},
"billed_on_download": true,
"links": {
"api": "https://api.tomba.io/v1/bulk/search/4521",
"download": "https://api.tomba.io/v1/bulk/search/4521/download",
"app": "https://app.tomba.io/bulk/searches/4521"
}
}
}
```
| Field | Description |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `type`, `created_at` | The event, and when it was created |
| `data.bulk_id`, `data.bulk_type`, `data.name`, `data.status` | The job. Phone finder jobs are `phone`. |
| `data.attempt` | `1` for the first run, then one more for each retry |
| `data.error_message` | Why the job failed. `bulk.failed` only. |
| `data.started_at`, `data.completed_at` | When the job started and completed |
| `data.metrics` | The job's results, as in [Job fields](/bulks#job-fields). `bulk.completed` only. |
| `data.billed_on_download` | Always `true`: the job is charged when you first download its results, not when the event is sent. See [Billing](/bulks#billing). |
| `data.links` | `api`: the job. `download`: its results file, `bulk.completed` only. `app`: the job in the dashboard. |
### Headers
The events follow the [Standard Webhooks](https://www.standardwebhooks.com) specification:
| Header | Value |
| ------------------- | --------------------------------------------------------------------------------------------------------------- |
| `Content-Type` | `application/json` |
| `User-Agent` | `Tomba-Webhooks/1.0` |
| `webhook-id` | The event's ID, such as `blk_4521_1_bulk.completed`. It's the same on every attempt; use it to skip duplicates. |
| `webhook-timestamp` | Unix time, in seconds, of the attempt |
| `webhook-signature` | `v1,` followed by the base64-encoded HMAC-SHA256 signature |
### Verify the signature
Get your secret from `GET /v1/bulk/webhook-secret`. It's created with your first bulk job that has a `webhook_url`, or by this call, and it signs the events of all your jobs. To verify an event:
1. Remove the `whsec_` prefix from the secret and base64-decode the rest: that's the key.
2. Compute the HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}` with the key, where `body` is the raw request body, and base64-encode it.
3. Compare the result with the part of `webhook-signature` after `v1,`. Reject events whose timestamp is more than 5 minutes old.
```js title="Node.js"
import crypto from "node:crypto";
// secret: from GET /v1/bulk/webhook-secret. body: the raw request body, as a string.
export function verifyTombaEvent(secret, headers, body) {
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const signed = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${body}`;
const expected = crypto
.createHmac("sha256", key)
.update(signed)
.digest("base64");
const received = headers["webhook-signature"].replace(/^v1,/, "");
const age = Math.abs(
Date.now() / 1000 - Number(headers["webhook-timestamp"]),
);
return (
age < 300 &&
expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
);
}
```
```python title="Python"
import base64
import hashlib
import hmac
import time
# secret: from GET /v1/bulk/webhook-secret. body: the raw request body, as bytes.
def verify_tomba_event(secret: str, headers: dict, body: bytes) -> bool:
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
received = headers["webhook-signature"].removeprefix("v1,")
fresh = abs(time.time() - int(headers["webhook-timestamp"])) < 300
return fresh and hmac.compare_digest(expected, received)
```
`POST /v1/bulk/webhook-secret/rotate` replaces the secret. The old one stops working at once, including for the events of jobs that are already running.
### Delivery and retries
- Each attempt times out after 10 seconds. A network error or a non-2xx response is retried, up to 5 attempts: the first right away, then 10 seconds, 30 seconds, 2 minutes, and 5 minutes after the previous one. A `410 Gone` response stops the retries.
- `webhook_url` must be an `https://` URL of at most 2,048 characters, without a username or password, whose host resolves to a public IP address. Otherwise, creating the job fails with `422`. Deliveries connect only to public IP addresses.
- Each attempt is recorded in the job's history as `webhook_delivered` or `webhook_failed`, with its HTTP status; see [`GET /v1/bulk/{type}/{id}/history`](/api/bulks#get-bulk-job-history).
## Lead events
The lead webhook calls your URL every time a lead is created in your account. It isn't sent when a lead is updated or deleted, and it requires a paid plan.
Set it up under [Webhooks](https://app.tomba.io/settings/webhooks) in the dashboard. An account has one lead webhook:
| Setting | Description |
| ------- | -------------------------------------------------------------------------------------------------------------------- |
| Name | 2 to 50 characters. |
| URL | Where Tomba sends the lead. Tomba stores the URL in lowercase, including the path and query, so use a lowercase URL. |
| Method | `POST`, `PUT`, `PATCH`, `GET`, or `HEAD`. Use `POST`. |
| Header | One custom header name and value, such as `Authorization: Bearer `. Use it to authenticate Tomba's requests. |
| Status | Whether the webhook is enabled. New webhooks start disabled. |
Tomba sends the lead with `Content-Type: application/json`, the `User-Agent` `Mozilla/5.0 (compatible; TombaHooks/1.0; +https://tomba.io)`, and your custom header:
```json
{
"data": {
"email": "jane.doe@stripe.com",
"company": "Stripe",
"first_name": "Jane",
"last_name": "Doe",
"phone": null,
"website": "stripe.com",
"position": "Marketing Manager",
"twitter": null,
"linkedin": null,
"notes": null,
"score": 90
}
}
```
| Field | Type | Description |
| ------------------------- | -------------- | ------------------------- |
| `email` | string | Lead's email address |
| `website` | string | Company website |
| `company` | string or null | Company name |
| `first_name`, `last_name` | string or null | Lead's name |
| `phone` | string or null | Phone number |
| `position` | string or null | Job title |
| `twitter`, `linkedin` | string or null | Social profiles |
| `notes` | string or null | Notes saved with the lead |
| `score` | integer | Lead score |
The body has no event name, ID, or timestamp, and Tomba doesn't record whether delivery succeeded. If you need a complete record, reconcile with [`GET /v1/leads`](/api/leads#retrieve-leads).
---
## Document: API versioning
How the Tomba API is versioned, which changes ship within v1, what counts as a breaking change, and which legacy paths still work.
URL: /versioning
# API versioning
This page explains how the Tomba API is versioned, which changes you should expect within a version, and where changes are announced.
## Current version
The current and only version of the API is v1. The major version is part of every endpoint path:
```text
https://api.tomba.io/v1/email-finder
```
## Backward-compatible changes
These changes ship within v1, without a new version:
- New endpoints.
- New optional parameters on existing endpoints.
- New fields in response objects, including in `meta` and in nested objects.
- New response headers.
- New values in enumerated fields, including new `errors.type` values.
Write your client so that these changes don't break it:
- Ignore response fields and headers your client doesn't recognize. Don't use strict schema validation that rejects unknown fields.
- Don't fail on an unrecognized enum value. Treat it like the closest value you handle, or log it.
- When `errors.type` has a value you don't handle, fall back to the HTTP status code. [Errors](/error-handling#status-codes) lists what each status means and whether to retry.
## Breaking changes
A breaking change is one that can make a working integration fail or misread a response:
- Removing or renaming an endpoint, parameter, response field, or header.
- Changing a field's type, format, or meaning.
- Making an optional parameter required, or rejecting a value that was accepted before.
- Changing the status code or `errors.type` returned for an existing condition.
- Changing the default page size or the shape of a paginated response.
Breaking changes have shipped within v1. The [API changelog](/changelog) lists each one with its date, marked Breaking, and says what to change in your client.
## Legacy paths
These older paths still work. Each one runs the same handler as its current path, with the same parameters, credit costs, and rate limit counters. The [API reference](/api) documents only the current paths; use them in new code.
| Legacy path | Current path |
| -------------------------------- | --------------------------------------------------------------- |
| `GET /v1/email-verifier/{email}` | [`GET /v1/email-verifier?email=`](/api/verifier#email-verifier) |
| `GET /v1/phone/{email}` | [`GET /v1/phone-finder?email=`](/api/phone#phone-finder) |
| `GET /v1/phone` | [`GET /v1/phone-finder`](/api/phone#phone-finder) |
## Tracking changes
- [API changelog](/changelog): dated changes to endpoints, parameters, headers, and status codes.
- [SDKs](/libraries): each SDK's GitHub repository publishes its own releases.
- [Data updates](/data/updates): refreshes of Tomba's email and company data.
---
## Document: Credits and usage
How Tomba meters API usage: plan allowances, what each endpoint costs, when requests are free, refunds, and how to check your balance.
URL: /usage-and-quotas
# Credits and usage
Most Tomba endpoints consume credits from a monthly allowance. This page is the reference for what each request costs, when it's free, and how to check what you have left. Throttling is separate; see [Rate limits](/rate-limits).
## Quotas
Your account has four credit balances:
| Balance | Used by |
| -------------------- | ------------------------------------------------------------------- |
| Search credits | Finders, enrichment, phone lookups, company search, similar domains |
| Verification credits | Email verifier |
| Email count credits | Email count, email format, location |
| Sources credits | Email sources |
New accounts start on the Free plan, with 25 search credits per usage window. Paid plans include equal search and verification allowances:
| Plan | Search credits | Verification credits | Availability |
| ---------- | -------------: | -------------------: | --------------------------------------------- |
| Basic | 2,500 | 2,500 | Self-serve |
| Growth | 5,000 | 5,000 | Self-serve |
| Pro | 20,000 | 20,000 | Self-serve |
| Pro Plus | 50,000 | 50,000 | Self-serve |
| Scale 100k | 100,000 | 100,000 | On request |
| Scale 200k | 200,000 | 200,000 | On request |
| Scale 350k | 350,000 | 350,000 | On request |
| Enterprise | By contract | By contract | [Contact sales](https://app.tomba.io/contact) |
Prices are on the [pricing page](https://tomba.io/pricing); [Tomba Enterprise](/enterprise) covers the Enterprise plan. Email count and sources allowances depend on the plan and appear in `GET /v1/me`.
**Usage window.** Monthly subscriptions reset at each billing date. Free, yearly, and Enterprise plans reset every 30 days; yearly plans receive a monthly allowance, not an annual pool. Unused plan credits don't carry over.
**Credit packs.** Pay-as-you-go packs add credits on top of your plan. Pack credits last 365 days from purchase, carry over between windows, and are used only after your plan credits run out, oldest pack first.
**Workspaces.** Members of a workspace use the owner's credits. The owner can set a search or verification limit for each member; a member who reaches it gets `402 quota_exceeded`.
**Auto-upgrade.** If you turn on auto-upgrade in the dashboard, your subscription moves to the next plan in the table above when 10% of your search credits remain. It never moves to Enterprise.
## Credit costs
A request is charged only when it succeeds (HTTP 200) and returns a result that meets the "Charged when" condition. Errors, `429` responses, and `451` responses cost nothing.
| Endpoint | Cost | Charged when |
| ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [`GET /domain-search`](/api/finder#domain-search) | 1 search credit per 10 results requested with `limit`, rounded up (the default `limit` of 10 costs 1), plus 5 per returned address with phone data | At least one email address is returned |
| [`GET /email-finder`](/api/finder#email-finder) | 1 search credit, or 6 with phone data | An address is returned with a verification status other than `unknown` |
| [`GET /author-finder`](/api/finder#author-finder) | 1 search credit | An address is returned |
| [`GET /enrich`](/api/finder#email-enrichment) | 1 search credit, or 6 with phone data | An address is returned with a verification status other than `unknown` |
| [`GET /linkedin`](/api/finder#linkedin-finder) | 1 search credit, or 6 with phone data. With `full=true`: 1 per returned address plus 5 per phone number | An address is returned |
| [`GET /email-verifier`](/api/verifier#email-verifier) | 1 verification credit, plus 5 per phone number returned with `enrich_mobile=true` | The status is not `disposable` or `unknown` |
| [`GET /phone-finder`](/api/phone#phone-finder) | 5 search credits, or 1 when the request includes `domain` | A valid phone number is returned |
| [`GET /phone-validator`](/api/phone#phone-validator) | 1 search credit | Every successful response, including one with no data |
| [`POST /reveal/search`](/api/reveal#search-companies) | 1 search credit per request | At least one company is returned |
| [`GET /similar`](/api/domain#similar) | 1 search credit per 10 results requested with `limit`, rounded up | Results are returned |
| [`GET /people/find`](/api/enrichment#person-api) | 1 search credit | A person is returned |
| [`GET /companies/find`](/api/enrichment#company-api) | 1 search credit | A company is returned |
| [`GET /combined/find`](/api/enrichment#combined-api) | 2 search credits | A person is returned |
| [`GET /email-count`](/api/finder#email-count) | 1 email count credit | Every call |
| [`GET /email-format`](/api/finder#email-format) | 1 email count credit | Every successful call, including an empty result |
| [`GET /location`](/api/finder#location) | 1 email count credit | Every successful call, including an empty result |
| [`GET /email-sources`](/api/email#email-sources) | 1 sources credit | Sources are found |
| [`GET /technology`](/api/domain#technology) | Not charged. Requires a positive search balance. | — |
| [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions), [`GET /domain-status`](/api/domain#domain-status), [Logo API](/logo-api) | Free | — |
| Account and management endpoints (`/me`, `/usage`, `/logs`, `/rate-limits`, `/keys`, `/leads`, `/flag`) | Free | — |
| [Bulk jobs](/bulks#billing) | Per row, depending on the bulk type | When you first download the results |
"Phone data" means phone numbers returned because you set `enrich_mobile=true`.
If a request would cost more credits than you have left, it fails with `402 quota_exceeded` before any work is done. When the email count or sources balance is exhausted, those endpoints return `429 rate_limit` instead.
## Duplicate requests
Sending the same request again within the same usage window costs nothing. The response still returns the data.
- **Same request** means the same user, endpoint, and parameters. Changing any parameter, including `page`, `limit`, `enrich_mobile`, or `webhook_url`, makes a new request.
- Some endpoints compare only their main input: the URL for author finder and LinkedIn finder, the email for person and combined enrichment, and the domain for company enrichment.
- The rule is per user. Two members of a workspace sending the same request are each charged.
- It doesn't apply to email count, email sources, or bulk jobs.
- It resets with your usage window, so a repeat just after renewal is charged.
## Refunds for incorrect data
When an email address returned by Tomba hard-bounces, report it with [`POST /v1/flag`](/api/flag#flag-incorrect-data), `flag_type` `email` and `reason` `hard_bounce`. After Tomba reviews and confirms the report, one search credit is returned to your current usage window.
Other flag types and reasons correct the data but don't refund credits. A second report for an item that already has a pending report returns `409 duplicate_record`. You can also review your reports under [Flags](https://app.tomba.io/settings/flags) in the dashboard.
## Free plan limits
On the Free plan, outside a workspace:
- Domain search returns at most 10 addresses. A `limit` above 10 fails with `400 params_invalid`, and `page` is always 1.
- Company search returns only the first page. A request for a later page returns the first page again, without the keyword, founded, revenue, type, technology, similar, SIC, and NAICS filters.
- Similar domains and technology return only data Tomba already has, without a live lookup.
## Check usage
| Where | What it shows |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Usage](https://app.tomba.io/usage) in the dashboard | Balances and usage over time, for you or your whole workspace |
| [`GET /v1/me`](/api/account#get-account) | `pricing.available_searches` and `pricing.available_verifications` (plan plus pack credits); `requests.domains` and `requests.verifications` with `available` (the allowance) and `used`; the current window in `issued` and `expired` |
| [`GET /v1/usage`](/api/account#retrieve-api-usage) | Daily usage per balance |
| [`GET /v1/logs`](/api/account#retrieve-api-logs) | Charged requests with their cost; repeats appear with `cost` 0 |
| Response headers | `X-Search-Limit` / `X-Search-Remaining`, `X-Verify-Limit` / `X-Verify-Remaining`, `X-Count-Limit` / `X-Count-Remaining`, `X-Sources-Limit` / `X-Sources-Remaining` on charged endpoints. Values are from before the current request. |
In `requests.domains` and `requests.verifications`, the remaining balance is `available` minus `used`.
---
## Document: Support
How to contact Tomba support, what to include in a request, which support each plan gets, and where to check service status.
URL: /support
# Support
This page lists the ways to reach Tomba and the details that let support resolve an API issue on the first reply.
## Get help
| Channel | Use it for |
| --------------------------------------------- | ----------------------------------------------------------------------------- |
| [Help center](https://help.tomba.io/en/) | Guides and answers to common account questions |
| [Contact form](https://app.tomba.io/contact) | Account, billing, and API questions; sales and Enterprise |
| [support@tomba.io](mailto:support@tomba.io) | API and integration issues, and reports of misuse |
| [security@tomba.io](mailto:security@tomba.io) | Security vulnerabilities. See [Security](/security#reporting-a-vulnerability) |
| [privacy@tomba.io](mailto:privacy@tomba.io) | Data removal and privacy rights requests |
| [Status page](https://status.tomba.io/) | Incidents and outages |
## What to include
- The `X-Request-ID` header from the response. Every API response carries one.
- The time of the request, in UTC.
- The endpoint and its parameters. Leave out personal data such as email addresses where the issue reproduces without it.
- The name or ID of the API key you used. Never send the secret or the full key.
- The error `type` and `message` from the response body. See [Errors](/error-handling#error-format).
## Support by plan
| Plan | Support |
| ------------- | -------------------------------------------------------------- |
| All plans | Email support |
| Growth and up | Priority support with faster response times |
| Enterprise | A named account manager and a custom SLA agreed under contract |
A 99.9% uptime SLA is available only as part of an Enterprise contract. Without a signed SLA, the API has no uptime commitment. [Tomba Enterprise](/enterprise) lists what the plan includes; to discuss it, use the [contact form](https://app.tomba.io/contact).
## Service status
Check [status.tomba.io](https://status.tomba.io/) for open incidents before you report an outage.
## Before you contact us
- [Errors](/error-handling): what each error type means and how to handle it.
- [Rate limits](/rate-limits): what a `429` response means and how to retry.
- [Credits and usage](/usage-and-quotas): quotas, credit costs, and duplicate requests.
- [FAQs](/faqs)
- [Changelog](/changelog): recent changes to the API.
---
## Document: Security and data handling
How Tomba protects API traffic and credentials, how long it keeps account data, and how to report a vulnerability.
URL: /security
# Security and data handling
This page covers how to connect to the Tomba API securely, how Tomba retains account data, and where to report security issues.
## Transport
The API is served over HTTPS only. Send every request to `https://api.tomba.io/v1`.
API responses carry this `Strict-Transport-Security` header, so browsers and HSTS-aware clients refuse plain HTTP to the domain for a year:
```http
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
```
## API credentials
- Keep your API key and secret on your server. Don't put them in browser code, mobile apps, or public repositories, where anyone can read them and spend your credits.
- Load them from environment variables or a secrets manager, as in `$TOMBA_API_KEY` and `$TOMBA_SECRET_KEY` in the examples on this site.
- Every key expires. See [Key expiry](/authentication#key-expiry).
- Use a separate key per integration, and rotate a key as soon as you suspect it leaked. See [Rotate keys](/authentication#rotate-keys).
If Tomba's firewall blocks the IP address your requests come from, the API returns error type `ip_blocked`. See [Errors](/error-handling#error-types).
## Webhooks
Callbacks sent to a `webhook_url` aren't signed. Put a hard-to-guess token in the URL and reject requests that don't carry it. See [Per-request callbacks](/webhook#delivery).
## Data retention
The [Privacy Policy](https://tomba.io/legal/privacy-policy) sets these retention periods for account data:
| Data | Retention |
| ------------------------ | ------------------------------------------------------------------ |
| Active account data | While the account is active, plus 3 years of inactivity |
| Deleted accounts | Most data removed within days; remaining artefacts within 3 months |
| Billing records | As required by tax and accounting law, typically 7 years |
| Security and access logs | Up to 12 months |
| Support correspondence | 3 years from last contact |
If this table and the Privacy Policy differ, the Privacy Policy applies.
## Removing personal data
Anyone can remove their email address, phone number, or LinkedIn profile from Tomba. Requests for removed data return status `451`. See [Data removal](/data#data-removal).
## Reporting a vulnerability
Email security issues to [security@tomba.io](mailto:security@tomba.io). To encrypt your report, use Tomba's [PGP key](https://tomba.io/tomba-security.asc). Tomba publishes its disclosure contacts in [security.txt](https://tomba.io/.well-known/security.txt).
## Compliance
- [Security Policy](https://tomba.io/legal/security-policy)
- [GDPR](https://tomba.io/legal/gdpr)
- [Privacy Notice for California Residents](https://tomba.io/legal/privacy-notice-for-california-residents)
- [Data Processing Addendum](https://tomba.io/legal/data-processing-addendum)
- [Privacy Policy](https://tomba.io/legal/privacy-policy)
- [Terms of Service](https://tomba.io/legal/terms-of-service)
---
## Document: Rate limits
How Tomba throttles API requests: per-plan limits for each endpoint, the headers that report them, and how to handle 429 responses.
URL: /rate-limits
# Rate limits
Tomba limits how many requests you can send to each endpoint per second, per minute, and per day. The limits depend on your plan and apply to your whole account. Monthly allowances are separate; see [Credits and usage](/usage-and-quotas).
## Limits by plan
Each cell is requests per second / per minute / per day.
| Endpoint | Free | Basic | Growth | Pro |
| -------------------------------------------------------- | ------------ | --------------- | --------------- | ----------------- |
| Domain search, email finder, similar domains, technology | 1 / 2 / 5 | 3 / 50 / 500 | 5 / 80 / 1,000 | 8 / 150 / 4,000 |
| Email verifier | 1 / 2 / 10 | 2 / 40 / 500 | 3 / 60 / 1,000 | 5 / 100 / 4,000 |
| Author finder | 1 / 2 / 5 | 3 / 50 / 500 | 5 / 80 / 1,000 | 5 / 100 / 4,000 |
| Email enrichment (`/enrich`) | 1 / 2 / 5 | 3 / 50 / 500 | 5 / 80 / 1,000 | 5 / 150 / 4,000 |
| Person, company, and combined enrichment | 1 / 2 / 5 | 3 / 50 / 500 | 5 / 80 / 1,000 | 5 / 100 / 4,000 |
| LinkedIn finder, phone finder, phone validator | 1 / 2 / 5 | 2 / 20 / 200 | 3 / 40 / 500 | 5 / 80 / 2,000 |
| Location | 1 / 5 / 20 | 2 / 20 / 200 | 3 / 40 / 500 | 5 / 80 / 2,000 |
| Email count | 1 / 5 / 20 | 3 / 50 / 1,000 | 5 / 80 / 2,000 | 8 / 150 / 5,000 |
| Email format | 1 / 5 / 20 | 2 / 50 / 1,000 | 3 / 80 / 2,000 | 5 / 150 / 5,000 |
| Email sources | 1 / 5 / 20 | 2 / 20 / 100 | 3 / 10 / 100 | 5 / 10 / 100 |
| Domain suggestions | 1 / 5 / 20 | 3 / 50 / 500 | 5 / 80 / 1,000 | 8 / 150 / 4,000 |
| Account (`/me`) | 2 / 30 / 500 | 5 / 100 / 3,000 | 5 / 150 / 5,000 | 10 / 200 / 10,000 |
Other plans:
- **Pro Plus and Scale plans:** no limits on these endpoints.
- **Enterprise:** no per-second or per-minute limit, and 100,000 requests per endpoint per day.
- **Credit packs:** while a pack has credits left, each limit is the higher of your plan's and the pack's. A pack of 50,000 credits or more removes the limits.
The table shows each plan's standard limits. `GET /v1/rate-limits` returns the limits that apply to your account; see [Check your limits](#check-your-limits).
### Endpoints with fixed limits
These endpoints have the same limits on every plan:
| Endpoint | Limit |
| -------------------------------------- | ------------------------------------- |
| `POST /reveal/search` | 5 requests per second |
| Bulk jobs | See [Bulk operations](/bulks#limits) |
| `GET /keys`, `GET /keys/{id}` | 100 requests per minute |
| `POST`, `PATCH`, `PUT`, `DELETE /keys` | 100 requests per hour |
| `GET /flag`, `POST /flag` | 10 requests per minute per IP address |
Leads, lead lists, lead attributes, `/logs`, `/usage`, `/rate-limits`, and `/domain-status` aren't rate-limited.
## How limits are counted
- **One set of counters per account.** Every API key, OAuth app, and workspace member of an account shares the same counters. Limits aren't per key.
- **Per endpoint.** Each row in the table has its own counters.
- **Every accepted request counts,** including requests that return no result or fail after the limit check. Requests rejected with `429` don't count.
- **Daily limits** reset at 00:00 UTC.
- **Per-second and per-minute limits** reset once 1 second or 60 seconds pass without an accepted request. A steady stream of requests keeps the counter from resetting, so continuous traffic can reach the limit even when its average rate is lower. After a `429` for `rps` or `rpm`, pause for the whole window, 1 or 60 seconds, before you retry.
## Rate limit headers
Every response from an endpoint in the [Limits by plan](#limits-by-plan) table carries these headers, on successful and `429` responses alike:
| Header | Value |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `x-second-rate-limit` | Per-second limit. `0` means no limit. |
| `x-minute-rate-limit` | Per-minute limit. `0` means no limit. |
| `x-daily-rate-limit` | Daily limit. `0` means no limit. |
| `x-minute-request-left` | Requests left in the per-minute counter |
| `x-daily-request-left` | Requests left today |
| `x-minute-reset-seconds` | Seconds until the next full minute. The per-minute counter can take longer to reset; see [How limits are counted](#how-limits-are-counted). |
| `x-daily-reset-seconds` | Seconds until 00:00 UTC |
| `RateLimit-Policy` | The per-second and daily policies, for example `"rps";q=8;w=1, "daily";q=4000;w=86400` |
| `RateLimit` | Remaining requests and seconds to reset for the same policies, for example `"rps";r=7;t=1, "daily";r=3842;t=64802` |
On a `429`, the headers show the values from before the rejected request.
Endpoints with fixed limits return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` on successful responses and only `Retry-After` on a `429`.
## Handle 429 responses
A request over a limit fails with status `429`, error type `rate_limit`, and a `Retry-After` header in seconds:
```json
{
"errors": {
"type": "rate_limit",
"message": "Rate limit exceeded (rpm). Limit is 150 requests. Retry after 42 seconds.",
"code": 429
}
}
```
The message names the limit that was hit: `rps`, `rpm`, or `daily`. For `daily`, `Retry-After` is the time until 00:00 UTC. A `429` without `Retry-After` from email count, email format, location, or email sources means that balance is used up, not that you're sending too fast; see [Credits and usage](/usage-and-quotas#quotas).
Wait at least `Retry-After` seconds, and for `rps` and `rpm` at least the whole window, then retry with exponential backoff:
```javascript title="Node.js"
async function tombaGet(url, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, {
headers: {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
},
});
if (res.status !== 429) return res.json();
const body = await res.json();
const retryAfter = Number(res.headers.get("Retry-After") ?? 1);
const windowSeconds = /\(rpm\)/.test(body.errors?.message ?? "")
? 60
: 1;
const backoff = Math.min(2 ** attempt, 60);
const waitSeconds = Math.max(retryAfter, windowSeconds, backoff);
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
}
throw new Error("Rate limited: retries exhausted");
}
const data = await tombaGet(
"https://api.tomba.io/v1/domain-search?domain=stripe.com",
);
console.log(data);
```
```python title="Python"
import os
import re
import time
import requests
HEADERS = {
"X-Tomba-Key": os.environ["TOMBA_API_KEY"],
"X-Tomba-Secret": os.environ["TOMBA_SECRET_KEY"],
}
def tomba_get(url, max_retries=5):
for attempt in range(max_retries + 1):
response = requests.get(url, headers=HEADERS, timeout=180)
if response.status_code != 429:
return response.json()
message = response.json().get("errors", {}).get("message", "")
retry_after = int(response.headers.get("Retry-After", 1))
window = 60 if re.search(r"\(rpm\)", message) else 1
backoff = min(2**attempt, 60)
time.sleep(max(retry_after, window, backoff))
raise RuntimeError("Rate limited: retries exhausted")
print(tomba_get("https://api.tomba.io/v1/domain-search?domain=stripe.com"))
```
## Check your limits
[`GET /v1/rate-limits`](/api/account#retrieve-rate-limits) returns the limits that apply to your account and your current usage, for each endpoint group. It isn't rate-limited itself.
```bash
curl "https://api.tomba.io/v1/rate-limits" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
```json
{
"data": {
"domain-search": {
"limits": { "rps": 8, "rpm": 150, "daily": 4000 },
"usage": { "rpm_used": 12, "daily_used": 847 }
},
"email-verifier": {
"limits": { "rps": 5, "rpm": 100, "daily": 4000 },
"usage": { "rpm_used": 0, "daily_used": 230 }
}
}
}
```
Most keys match the endpoint path. The others:
| Key | Endpoint |
| ----------------- | ----------------------------------------------------------- |
| `enrich` | `/enrich` |
| `linkedin` | `/linkedin` |
| `phone` | `/phone-finder` |
| `enrich-email` | `/people/find` |
| `enrich-company` | `/companies/find` |
| `enrich-combined` | `/combined/find` |
| `me` | `/me` |
| `searches` | Searches from the Tomba dashboard |
| `reveal` | Listed but not enforced; `/reveal/search` has a fixed limit |
In a workspace, members see the owner's limits and the workspace's combined usage. The dashboard shows the same information under [Rate limits](https://app.tomba.io/api/rate-limits).
---
## Document: Quickstart
Create a Tomba account, get your API key and secret, and make your first API request.
URL: /quickstart
# Quickstart
This page takes you from a new account to a working API request.
## Create an account
Sign up at [app.tomba.io/auth/register](https://app.tomba.io/auth/register). New accounts start on the free plan; its monthly allowance is listed in [Credits and usage](/usage-and-quotas#quotas).
## Get your API key and secret
Every request needs two credentials: an API key and your account secret.
1. Open [API keys](https://app.tomba.io/api) in the dashboard.
2. Create a key, or copy an existing one. Keys start with `ta_`.
3. Copy your secret from the same page. The secret starts with `ts_`.
4. Store both as environment variables so they stay out of your code:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
Keys expire. See [Authentication](/authentication#key-expiry) for expiry and rotation.
## Make your first request
Find the email addresses Tomba knows for a company domain:
```bash
curl "https://api.tomba.io/v1/domain-search?domain=stripe.com" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
A successful response returns the company in `data.organization`, the email addresses in `data.emails`, and paging information in `meta`. Trimmed, it looks like this:
```json
{
"data": {
"organization": {
"website_url": "stripe.com",
"organization": "Stripe",
"location": {
"country": "US",
"city": "San Francisco",
"state": "California"
},
"industries": "Financial Services",
"pattern": "{first}",
"accept_all": false
},
"emails": [
{
"email": "john@stripe.com",
"first_name": "John",
"last_name": "Doe",
"position": "Software Engineer",
"department": "engineering",
"type": "personal",
"score": 90,
"verification": {
"date": "2025-01-15T00:00:00Z",
"status": "valid"
}
}
]
},
"meta": { "total": 1250, "pageSize": 10, "current": 1, "total_pages": 125 }
}
```
[Company response](/attributes/company) and [Person response](/attributes/person) describe every field.
If the request fails, the response contains an `errors` object instead. [Errors](/error-handling) lists every error type and what to do about it. Every endpoint, with its parameters and response schema, is in the [API reference](/api).
## Dashboard
| Task | Where |
| ---------------------------------------------- | -------------------------------------------------------------- |
| Create, rename, and revoke API keys | [API keys](https://app.tomba.io/api) |
| Check remaining credits | [Usage](https://app.tomba.io/usage) |
| Review individual API requests | [Request history](https://app.tomba.io/usage/requests) |
| See your current rate limits | [Rate limits](https://app.tomba.io/api/rate-limits) |
| Change plan or billing details | [Subscription](https://app.tomba.io/settings/subscription) |
| Configure lead webhooks | [Webhooks](https://app.tomba.io/settings/webhooks) |
| Invite team members | [Members](https://app.tomba.io/settings/members) |
| Review OAuth apps that can access your account | [Connected apps](https://app.tomba.io/settings/connected-apps) |
| Report incorrect data | [Flags](https://app.tomba.io/settings/flags) |
Before you send volume, read [Rate limits](/rate-limits), [Credits and usage](/usage-and-quotas), and [Going to production](/going-to-production).
---
## Document: Logo API
Get a company's logo from its domain name. The Logo API needs no authentication and returns the image itself, scaled to the size you ask for.
URL: /logo-api
# Logo API
The Logo API returns a company's logo image for a domain. It needs no API key and uses no credits ([Credits and usage](/usage-and-quotas#credit-costs)), so you can put its URL straight into an `` tag.
## Request
```text
GET https://logo.tomba.io/{domain}
```
| Parameter | In | Description |
| ---------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `domain` | Path | The company's domain, such as `stripe.com`. A subdomain returns the logo of its registrable domain, so `www.stripe.com` returns the logo for `stripe.com`. A trailing image extension, as in `stripe.com.png`, is ignored. |
| `s` or `size` | Query | Width and height in pixels, from 1 to 512. The logo is scaled to a square PNG of that size; a logo that isn't square is stretched to fit. Without `s`, or with `s=80`, the logo is returned as stored, in its original format and dimensions. |
| `greyscale` | Query | `true` returns the logo as a greyscale PNG. |
| `d` or `default` | Query | URL-encoded `http` or `https` URL of an image to return when Tomba has no logo for the domain. It must point to a public host. |
## Response
A successful request returns status `200` with the image in the body:
- `Content-Type` is `image/png` when you set `s` or `greyscale`. Otherwise, it's the format the logo is stored in: `image/png`, `image/jpeg`, `image/webp`, `image/avif`, or `image/svg+xml`.
- `Content-Disposition` is `attachment; filename="."`. Opening the URL in a browser tab therefore downloads the image; an `` tag displays it.
When Tomba has no logo for the domain, the API still returns status `200`:
- Without `d`, it returns a placeholder PNG of `s` × `s` pixels, 80 × 80 by default, with `Content-Disposition: attachment; filename=default.png`. Check that header to tell a placeholder from a logo.
- With `d`, it fetches your image and returns it unchanged, with `Content-Disposition: attachment; filename="default"`.
## Errors
Errors return a JSON body with an `error` message, such as `{"error": "size must be between 1 and 512"}`.
| Status | Cause |
| ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `400` | The domain isn't a valid domain name, `s` isn't a number from 1 to 512, or `d` isn't an `http` or `https` URL on a public host. |
| `502` | Tomba couldn't fetch the image at `d`, or that URL didn't return status `200`. |
## Examples
```html
```
```bash
curl -o stripe.png "https://logo.tomba.io/stripe.com?s=200&greyscale=true"
```
---
## Document: All limits
Every Tomba API limit in one table, from request size and timeouts to rate limits, credits, bulk jobs, and keys, with a link to the page that defines each.
URL: /limits
# All limits
Every limit that applies to the Tomba API, in one place. Each row links to the page that defines the limit and its current value.
## Limits
| Limit | Value or where to find it | Defined on |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Request body size | 4 MB. Larger bodies fail with `413`. | This page |
| Request duration | 180 seconds. Longer requests end with `504` from the gateway. | This page |
| Requests per second, minute, and day | Per plan and per endpoint. `GET /v1/rate-limits` returns the values for your account. | [Limits by plan](/rate-limits#limits-by-plan) |
| Endpoints with a fixed rate limit | Company search, API keys, and flags have the same limit on every plan. | [Endpoints with fixed limits](/rate-limits#endpoints-with-fixed-limits) |
| Credit allowances | Search, verification, email count, and sources credits per usage window, by plan. `GET /v1/me` returns yours. | [Quotas](/usage-and-quotas#quotas) |
| Credits per request | Per endpoint | [Credit costs](/usage-and-quotas#credit-costs) |
| Free repeat requests | Same request within the usage window | [Duplicate requests](/usage-and-quotas#duplicate-requests) |
| Free plan results | Domain search result count and pages; company search pages | [Free plan limits](/usage-and-quotas#free-plan-limits) |
| Workspace member credits | Optional per-member search and verification limits set by the owner | [Quotas](/usage-and-quotas#quotas) |
| Workspace members | `pricing.available_teams` in [`GET /v1/me`](/api/account#get-account) | [API reference](/api/account#get-account) |
| Rows per bulk job | Per bulk type | [Bulk types](/bulks#bulk-types) |
| Bulk jobs created, running at once, downloads | Per bulk type and per job | [Bulk limits](/bulks#limits) |
| Page size of paginated endpoints | Per endpoint | [Pagination](/going-to-production#pagination) |
| Active API keys | `pricing.available_keys` in [`GET /v1/me`](/api/account#get-account) | [Authentication](/authentication#rotate-keys) |
| API key lifetime | Set per key, up to a maximum | [Key expiry](/authentication#key-expiry) |
| OAuth token lifetime | Access and refresh tokens | [OAuth 2.0](/authentication#oauth-20) |
| Leads and lead lists | `pricing.available_leads` and `pricing.available_list` in [`GET /v1/me`](/api/account#get-account) | [API reference](/api/account#get-account) |
| Lead attributes | Per account | [Create a lead attribute](/api/lead-attributes#create-a-lead-attribute) |
| Webhook callback timeout and retries | Per delivery attempt | [Delivery](/webhook#delivery) |
| Remote MCP server | Plan rate limits and credits, plus the server's own retries | [Limits and billing](/llm/remote-mcp/introduction#limits-and-billing) |
| Pay per request (agents.tomba.io) | Shared across all agents | [Rate limits](/mpp/introduction#rate-limits) |
A request over a rate limit or a daily bulk limit fails with `429`. A request over a credit balance fails with `402`, or with `429` for the email count and sources balances. A request over an account cap on keys, leads, lead lists, or lead attributes fails with `422`. See [Errors](/error-handling#status-codes).
---
## Document: SDK overview
Tomba's official API client libraries for 13 languages, with each package name, install command, and source repository.
URL: /libraries
# SDK overview
Tomba maintains client libraries for 13 languages. Each one wraps the REST API described in the [API reference](/api), and each page below covers installation, errors, rate limit data, and the methods the library provides.
| Language | Package | Install | Source |
| ---------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| [C#](/libraries/csharp-email-finder) | [Tomba](https://www.nuget.org/packages/Tomba) on NuGet | `dotnet add package Tomba` | [tomba-io/csharp](https://github.com/tomba-io/csharp) |
| [Dart](/libraries/dart-email-finder) | [tomba](https://pub.dev/packages/tomba) on pub.dev | `dart pub add tomba` | [tomba-io/dart](https://github.com/tomba-io/dart) |
| [Deno](/libraries/deno-email-finder) | [@tomba/sdk](https://jsr.io/@tomba/sdk) on JSR | `deno add jsr:@tomba/sdk` | [tomba-io/deno](https://github.com/tomba-io/deno) |
| [Elixir](/libraries/elixir-email-finder) | [tomba](https://hex.pm/packages/tomba) on Hex | `{:tomba, "~> 1.0"}` in `mix.exs` | [tomba-io/elixir](https://github.com/tomba-io/elixir) |
| [Go](/libraries/go-email-finder) | [github.com/tomba-io/go](https://pkg.go.dev/github.com/tomba-io/go/tomba) | `go get github.com/tomba-io/go` | [tomba-io/go](https://github.com/tomba-io/go) |
| [Lua](/libraries/lua-email-finder) | [tomba](https://luarocks.org/modules/benemohamed/tomba) on LuaRocks | `luarocks install tomba`, after the [`xml` step](/libraries/lua-email-finder#install) | [tomba-io/lua](https://github.com/tomba-io/lua) |
| [Node.js](/libraries/node-email-finder) | [tomba](https://www.npmjs.com/package/tomba) on npm | `npm install tomba` | [tomba-io/node](https://github.com/tomba-io/node) |
| [Perl](/libraries/perl-email-finder) | [Tomba::Finder](https://metacpan.org/pod/Tomba::Finder) on CPAN | `cpanm Tomba::Finder` | [tomba-io/perl](https://github.com/tomba-io/perl) |
| [PHP](/libraries/php-email-finder) | [tomba-io/php](https://packagist.org/packages/tomba-io/php) on Packagist | `composer require tomba-io/php` | [tomba-io/php](https://github.com/tomba-io/php) |
| [Python](/libraries/python-email-finder) | [tomba-io](https://pypi.org/project/tomba-io/) on PyPI | `pip install tomba-io` | [tomba-io/python](https://github.com/tomba-io/python) |
| [R](/libraries/r-email-finder) | tomba, from GitHub | `remotes::install_github("tomba-io/r")` | [tomba-io/r](https://github.com/tomba-io/r) |
| [Ruby](/libraries/ruby-email-finder) | [tomba](https://rubygems.org/gems/tomba) on RubyGems | `gem install tomba` | [tomba-io/ruby](https://github.com/tomba-io/ruby) |
| [Rust](/libraries/rust-email-finder) | [tomba](https://crates.io/crates/tomba) on crates.io | `cargo add tomba` | [tomba-io/rust](https://github.com/tomba-io/rust) |
The libraries don't all cover the same endpoints. Each page's method reference lists what its library supports, and a known issues section lists any methods that don't work against the current API.
Every library authenticates with an API key and your account secret, sent as the `X-Tomba-Key` and `X-Tomba-Secret` headers. None of them reads environment variables on its own: load the values in your code and pass them to the client. Create keys and copy your secret on the [API keys](https://app.tomba.io/api) page; expiry and rotation are covered in [Authentication](/authentication).
---
## Document: Introduction
The Tomba REST API: base URL, authentication, response format, and an index of every endpoint by task.
URL: /introduction
# Introduction
Tomba finds, verifies, and enriches professional email addresses, phone numbers, and company data. This page covers the conventions every endpoint shares and indexes the endpoints by task. To make your first request, follow the [Quickstart](/quickstart).
## Base URL
All endpoints are served over HTTPS from:
```text
https://api.tomba.io/v1
```
The major version is part of the path. Backward-compatible changes, such as new response fields or new optional parameters, ship without a new version, so ignore response fields your client doesn't recognize. See [API versioning](/versioning) and the [API changelog](/changelog).
## Authentication
Send your API key and secret on every request in the `X-Tomba-Key` and `X-Tomba-Secret` headers:
```bash
curl "https://api.tomba.io/v1/domain-search?domain=stripe.com" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
[Authentication](/authentication) covers key expiry, rotation, and the other supported methods.
## Response format
Responses are JSON. A successful response carries the result in `data`; list endpoints add paging information in `meta`. A failed request returns an `errors` object instead:
```json
{
"errors": {
"type": "authentication_failed",
"message": "Invalid credentials.",
"code": 401
}
}
```
Branch on `errors.type` rather than the HTTP status alone. [Errors](/error-handling) lists every type.
Set your client timeout to at least 180 seconds. Searches and verifications run in real time against remote mail servers and websites, so response times vary.
## Endpoints
Paths are relative to the base URL. Each link opens the full reference, with parameters, response schema, and a playground.
### Find email addresses
| Task | Endpoint |
| ------------------------------------------------------------------------- | ------------------------------------------------- |
| List the email addresses found for a company domain | [`GET /domain-search`](/api/finder#domain-search) |
| Find one person's email address from their name and company | [`GET /email-finder`](/api/finder#email-finder) |
| Find the email address of an article's author | [`GET /author-finder`](/api/finder#author-finder) |
| Find the email address for a LinkedIn profile URL | [`GET /linkedin`](/api/finder#linkedin-finder) |
| Count the email addresses found for a domain, by department and seniority | [`GET /email-count`](/api/finder#email-count) |
| Get the email formats a domain uses | [`GET /email-format`](/api/finder#email-format) |
| List the web pages where an email address was found | [`GET /email-sources`](/api/email#email-sources) |
### Verify email addresses
| Task | Endpoint |
| ----------------------------------------------- | ----------------------------------------------------- |
| Check whether an email address can receive mail | [`GET /email-verifier`](/api/verifier#email-verifier) |
| Check whether a domain is webmail or disposable | [`GET /domain-status`](/api/domain#domain-status) |
### Enrich people and companies
| Task | Endpoint |
| ------------------------------------------------------- | ---------------------------------------------------- |
| Get the person and company behind an email address | [`GET /enrich`](/api/finder#email-enrichment) |
| Get a person profile in the Clearbit-compatible format | [`GET /people/find`](/api/enrichment#person-api) |
| Get a company profile in the Clearbit-compatible format | [`GET /companies/find`](/api/enrichment#company-api) |
| Get the person and company profiles together | [`GET /combined/find`](/api/enrichment#combined-api) |
### Phone numbers
| Task | Endpoint |
| ---------------------------------------------------------------- | ---------------------------------------------------- |
| Find phone numbers for an email address, domain, or LinkedIn URL | [`GET /phone-finder`](/api/phone#phone-finder) |
| Validate a phone number and get its line type and carrier | [`GET /phone-validator`](/api/phone#phone-validator) |
### Companies and domains
| Task | Endpoint |
| --------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Search companies by industry, size, location, and other filters | [`POST /reveal/search`](/api/reveal#search-companies) |
| Find websites similar to a domain | [`GET /similar`](/api/domain#similar) |
| List the technologies a website uses | [`GET /technology`](/api/domain#technology) |
| Get the domain for a company name | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| Count a domain's email addresses by country or city | [`GET /location`](/api/finder#location) |
| Get a company logo (no authentication) | [Logo API](/logo-api) |
### Lists and jobs
| Task | Endpoint |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Run any finder, verifier, or enrichment over a list | [`POST /bulk/{type}`](/bulks) |
| Store and organize leads | [`/leads`](/api/leads), [`/leads_lists`](/api/lead-lists), [`/attributes`](/api/lead-attributes) |
| Receive results at your own URL | [`webhook_url`](/webhook#per-request-callbacks) |
### Account
| Task | Endpoint |
| ----------------------------------------- | ----------------------------------------------- |
| Get account details and remaining credits | [`GET /me`](/api/account#get-account) |
| Get usage over time | [`GET /usage`](/api/account#retrieve-api-usage) |
| List recent API requests | [`GET /logs`](/api/account#retrieve-api-logs) |
| Manage API keys | [`/keys`](/api/keys) |
| Report an incorrect email or a bounce | [`POST /flag`](/api/flag#flag-incorrect-data) |
## Limits and credits
Requests are throttled per plan and per endpoint; see [Rate limits](/rate-limits). Most endpoints consume credits from your monthly allowance; [Credits and usage](/usage-and-quotas) lists the cost of each. [All limits](/limits) lists every limit and where it's defined.
## SDKs and tools
The API is also available through [SDKs for 13 languages](/libraries), the [Tomba CLI](/cli/installation), [MCP servers](/llm/remote-mcp/introduction) for AI assistants, and [integrations](/integrations/overview) with automation platforms.
---
## Document: Going to production
A checklist for running a Tomba API integration in production: timeouts, retries, request IDs, pagination, key expiry, webhooks, and monitoring.
URL: /going-to-production
# Going to production
Work through each section before you send production traffic to the Tomba API. Each item links to the page that defines the behavior.
## Timeouts
- Set your HTTP client timeout to 180 seconds. Searches and verifications run in real time against the target's mail servers and websites, so a single request can take that long.
- A request that runs longer than 180 seconds ends with `504` from the gateway. The body isn't JSON and has no `errors` object.
- The request body can be at most 4 MB. A larger body fails with `413` and `{"error": "Request Entity Too Large"}`.
- For lists of inputs, use [bulk jobs](/bulks) instead of long-running loops of single requests.
## Retries
Retry only the statuses that [Errors](/error-handling#status-codes) marks as retryable:
| Status | Retry |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `408` | Later, with backoff. |
| `429` | After `Retry-After`, following [Handle 429 responses](/rate-limits#handle-429-responses). A `429` without `Retry-After` from email count, email format, location, or email sources means the balance is used up; don't retry it. |
| `500` | With exponential backoff. |
| `504` | With backoff, or move the work to a [bulk job](/bulks). |
Don't retry `400`, `401`, `402`, `403`, `404`, `409`, `413`, `422`, or `451`: the same request fails the same way until you change it or your account. The exception is a `422` with type `openai_error` or `search_failed` from company search, which you can retry.
Use exponential backoff with a cap and random jitter, and stop after a fixed number of attempts. Failed requests aren't charged; see [Credits and usage](/usage-and-quotas#credit-costs).
The API has no idempotency keys. Retrying a `GET` is safe, but retrying a request that creates something can create it twice. Before you retry one of these after a timeout or `5xx`, list the resource to check whether the first attempt succeeded:
| Request | A retry can |
| ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `POST /v1/bulk/{type}` | Create a second job, which counts toward the [bulk limits](/bulks#limits) |
| `POST /v1/leads` | Create a second lead with the same address |
| `POST /v1/leads_lists` | Fail with `422 params_invalid` when the first attempt created the list, because list names are unique |
| `POST /v1/keys` | Create a second API key |
| `PUT /v1/keys/{id}` | Rotate the key again, invalidating the value from the first attempt |
| `POST /v1/flag` | Return `409 duplicate_record` when the first report was stored; treat it as stored |
## Repeated requests
Some repeats of a successful request are free. The [duplicate-request rule](/usage-and-quotas#duplicate-requests) defines which ones, and for how long. Read it before you add your own cache: a cache keyed on different parameters than the rule can cost credits the rule would have waived.
## Request IDs
Responses from the API carry an `X-Request-ID` header. Log it with each request; quote it when you [contact support](/support#what-to-include) about a failed request.
You can also set the ID yourself. Send an `X-Request-ID` header of printable ASCII characters and the API returns the same value. An empty value, or one with other characters, is replaced by a generated ID.
```bash
curl -i "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" \
-H "X-Request-ID: order-sync-7f3c9a"
```
A `504` from the gateway doesn't carry the header. Sending your own ID gives you a value to quote in that case too.
## Pagination
Paginated endpoints take a 1-based `page`. None accepts an offset. The page size and the names of the pagination fields differ between endpoints:
| Endpoint | Parameters | Pagination fields |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| [`GET /v1/domain-search`](/api/finder#domain-search) | `page`; `limit` from a fixed set of values, default 10. The [API reference](/api/finder#domain-search) lists the values. | `meta.total`, `meta.pageSize`, `meta.current`, `meta.total_pages` |
| [`GET /v1/similar`](/api/domain#similar) | `page`. Pages always hold 25 companies; `limit` only sets the credit cost. | `meta.total`, `meta.pageSize`, `meta.current`, `meta.total_pages` |
| [`POST /v1/reveal/search`](/api/reveal#search-companies) | `page` in the JSON body, 1 to 1,000. Pages always hold 50 companies. | `meta.total`, `meta.page`, `meta.limit`, `meta.pages` |
| [`GET /v1/leads`](/api/leads#retrieve-leads) | `page`; `limit` default 10. Values above 200 are treated as 200. | `data.meta.total`, `data.meta.pageSize`, `data.meta.current`, `data.meta.total_pages` |
| [`GET /v1/bulk/{type}`](/api/bulks#list-bulk-jobs) | `page`; `limit` default 10. Values above 50 are treated as 50. | `meta.total`, `meta.pageSize`, `meta.current`, `meta.total_pages` |
| [`GET /v1/logs`](/api/account#retrieve-api-logs) | `page`; `limit` 1 to 100, default 20. Larger values fail with `422`. | `meta.total`, `meta.page`, `meta.limit`, `meta.total_pages` |
- On `GET /v1/leads` and `GET /v1/bulk/{type}`, `total_pages` is rounded down, so it can miss the last, partial page. Keep requesting pages until one returns fewer items than `limit`.
- On domain search and similar domains, a larger `limit` costs more credits; see [Credit costs](/usage-and-quotas#credit-costs).
- On the Free plan, domain search and company search return only the first page; see [Free plan limits](/usage-and-quotas#free-plan-limits).
## Keys and secrets
- Keep the key and secret on your server, loaded from environment variables or a secrets manager. See [Security](/security#api-credentials).
- Use one key per integration, so you can rotate or delete one without touching the others.
- Every key expires. Track `expires_at` from [`GET /v1/keys`](/api/keys#retrieve-api-keys), and alert on the `X-Tomba-Key-Expired` header and the `api_key_expired` error. See [Key expiry](/authentication#key-expiry).
- Rotate keys before they expire, following [Rotate keys](/authentication#rotate-keys).
## Webhooks
Callbacks sent to a `webhook_url` aren't signed. Put a hard-to-guess token in the URL, reject requests that don't carry it, and respond with a `2xx` as soon as you've stored the payload. [Webhooks](/webhook#delivery) describes the delivery and retry behavior.
## Monitoring
- Subscribe to [status.tomba.io](https://status.tomba.io/) for incidents and outages.
- Track your throttling headroom from the rate limit headers, or poll [`GET /v1/rate-limits`](/rate-limits#check-your-limits), which isn't rate-limited itself.
- Track your credit balances with `GET /v1/me`, `GET /v1/usage`, or the quota headers on each response. See [Check usage](/usage-and-quotas#check-usage).
- Alert on rising `429`, `5xx`, and `504` rates, and on `402 quota_exceeded`, which means a balance is too low for the request.
---
## Document: Glossary
Definitions of the terms used across the Tomba documentation, from credits and usage windows to verification results and bulk jobs.
URL: /glossary
# Glossary
Short definitions of the terms used in these docs. Each links to the page that describes the behavior in full.
## Billing
### Search credits
The balance used by the finders, enrichment, phone lookups, company search, and similar domains. See [Quotas](/usage-and-quotas#quotas).
### Verification credits
The balance used by the email verifier. See [Quotas](/usage-and-quotas#quotas).
### Email count credits
The balance used by email count, email format, and location. When it runs out, those endpoints return `429` instead of `402`. See [Quotas](/usage-and-quotas#quotas).
### Sources credits
The balance used by email sources. When it runs out, the endpoint returns `429` instead of `402`. See [Quotas](/usage-and-quotas#quotas).
### Usage window
The period after which your plan's credit allowances reset. Unused plan credits don't carry over to the next window. See [Quotas](/usage-and-quotas#quotas).
### Duplicate request
A request with the same user, endpoint, and parameters as an earlier one in the same usage window. It returns the data without charging credits again. See [Duplicate requests](/usage-and-quotas#duplicate-requests).
### Plan
Your subscription. It sets your credit allowances, your [rate limits](/rate-limits#limits-by-plan), and account caps such as the number of API keys and leads. See [Quotas](/usage-and-quotas#quotas).
## Accounts and access
### Workspace
A team of Tomba users under one owner. Members use the owner's credits and rate limits and share bulk jobs, and the owner can set a credit limit for each member. See [Quotas](/usage-and-quotas#quotas).
### API key
A credential that starts with `ta_`, sent in the `X-Tomba-Key` header. An account can hold several keys, each with its own name and expiry date. See [Authentication](/authentication#request-headers).
### Secret
Your account's secret, which starts with `ts_` and is sent in the `X-Tomba-Secret` header. An account has one secret, shared by all of its keys. See [Authentication](/authentication#request-headers).
### OAuth scope
A permission granted to an OAuth 2.0 access token, such as `search` or `verify`. A token without the scope an endpoint needs gets `403 insufficient_scope`. See [OAuth 2.0](/authentication#oauth-20).
### Request ID
The value of the `X-Request-ID` response header, which identifies one API request. Quote it when you contact support. See [Request IDs](/going-to-production#request-ids).
## Data and results
### Verification status
The detailed outcome of an email verification, returned in `status`. See [Status values](/attributes/verifier#status-values).
### Verification result
The deliverability verdict of an email verification, returned in `result`. See [Result values](/attributes/verifier#result-values).
### Accept-all
A domain whose mail server accepts mail for any address, also called catch-all, so Tomba can't confirm that a specific mailbox exists. See [Status values](/attributes/verifier#status-values).
### Disposable
A domain that belongs to a disposable email provider. Domain search returns no addresses for it, the email finder rejects it, and a verification that returns this status isn't charged. See [Status values](/attributes/verifier#status-values).
### Webmail
A domain that belongs to a webmail provider, such as Gmail or Outlook. Domain search returns no addresses for it and the email finder rejects it; the email verifier reports it in the `webmail` field. See [What Tomba excludes](/data#what-tomba-excludes).
### Score
A confidence value on a result. A person result's `score` is based on the address's verification status and the number of public sources that list it; the email verifier's `score` is higher when delivery is more likely. See [Checks and scores](/data#checks-and-scores).
### Claim
A request from a person to remove their email address, phone number, or LinkedIn profile from Tomba. Requests that name claimed data return `451`. See [Data removal](/data#data-removal).
## Features
### Bulk job
One Tomba operation run over a list of inputs. You create and launch it, poll its progress, and download the results as CSV. See [Bulk operations](/bulks).
### Webhook callback
A `POST` of a request's result to the `webhook_url` you add to a supported request. Callbacks aren't signed. See [Per-request callbacks](/webhook#per-request-callbacks).
### Lead webhook
A URL, set in the dashboard, that Tomba calls each time a lead is created in your account. See [Lead events](/webhook#lead-events).
### Flag
A report of incorrect data, sent with `POST /v1/flag`. When Tomba confirms a report that an email address hard-bounced, it returns a search credit. See [Refunds for incorrect data](/usage-and-quotas#refunds-for-incorrect-data).
### MCP server
A Model Context Protocol server that lets AI assistants call Tomba as tools. Tomba hosts a [remote server](/llm/remote-mcp/introduction) and publishes a [local server](/llm/local-mcp/introduction) that runs on your machine.
### Pay per request (MPP)
Access to Tomba endpoints at `agents.tomba.io` without a Tomba account or API key. The caller pays for each request through InFlow with the Machine Payments Protocol. See [Tomba for AI agents](/mpp/introduction).
---
## Document: Frequently asked questions
Short answers to common questions about Tomba API keys, credits, rate limits, refunds, verification results, and data, with links to the details.
URL: /faqs
# Frequently asked questions
Each answer links to the page that has the full details.
## How do I get an API key and secret?
Create an account, then copy a key and your secret from the [API keys](https://app.tomba.io/api) page of the dashboard. [Quickstart](/quickstart#get-your-api-key-and-secret) walks through it, and [Authentication](/authentication) shows how to send them.
## Do API keys expire?
Yes. Every key has an expiry date, and a request with an expired key fails with the error type `api_key_expired`. [Key expiry](/authentication#key-expiry) gives the lifetime, and [Rotate keys](/authentication#rotate-keys) shows how to replace a key without downtime.
## When does a request use credits?
Most endpoints charge only when a request succeeds and returns a result, and most repeats of the same request within your usage window are free. [Credit costs](/usage-and-quotas#credit-costs) lists what each endpoint costs and when it's charged.
## Do unused credits carry over?
Plan credits don't: they reset at the start of each usage window. Credits from a credit pack stay available until the pack expires. See [Quotas](/usage-and-quotas#quotas).
## How do I check how many credits I have left?
Open [Usage](https://app.tomba.io/usage) in the dashboard, or call `GET /v1/me`. [Check usage](/usage-and-quotas#check-usage) lists the response fields and headers that report your balance.
## What does a `429` response mean?
You hit a rate limit, or used up your email count or sources credits. [Handle 429 responses](/rate-limits#handle-429-responses) explains how to tell the two apart and when to retry.
## Can I get a credit back for an address that bounced?
Yes, for a hard bounce. Report the address with `POST /v1/flag` and the reason `hard_bounce`, and Tomba refunds a credit after it confirms the report. See [Refunds for incorrect data](/usage-and-quotas#refunds-for-incorrect-data) and the [Data quality guarantee](/data-quality-guarantee).
## Can I get a refund on my subscription?
Subscription refunds follow the Refund Policy in the [Terms of Service](https://tomba.io/legal/terms-of-service).
## What do `status` and `result` mean in a verification?
`result` is the deliverability verdict, and `status` is the detailed outcome of the check. [Status values](/attributes/verifier#status-values) and [Result values](/attributes/verifier#result-values) list every value and what to do with it.
## Where does Tomba's data come from?
From public web sources, such as company websites and published articles, and from addresses that Tomba generates from each company's email format and checks against its mail server. See [Data sources and coverage](/data).
## How can someone remove their data from Tomba?
With the [Claim](https://app.tomba.io/claim) tool, or by emailing [privacy@tomba.io](mailto:privacy@tomba.io). Once the request is confirmed, Tomba stops returning the data, and requests for it return status `451`. See [Data removal](/data#data-removal).
## Can I use Tomba without calling the REST API directly?
Yes. Use the [SDKs](/libraries), the [Tomba CLI](/cli/installation), the [MCP server](/llm/remote-mcp/introduction) for AI assistants, [integrations](/integrations/overview) such as Zapier and n8n, or the [Google Sheets](https://tomba.io/sheets) and [Excel](https://tomba.io/excel) add-ons.
---
## Document: Errors
The Tomba API error format, the HTTP status codes it returns, and every error type with its cause and whether to retry.
URL: /error-handling
# Errors
When a request fails, the response body contains an `errors` object. Branch on `errors.type`: one HTTP status can cover several causes.
## Error format
```json
{
"errors": {
"type": "params_invalid",
"message": "Either domain or company parameter must be provided.",
"code": 422
}
}
```
| Field | Description |
| --------- | -------------------------------------------------------------------------------- |
| `type` | Machine-readable cause. The values are listed under [Error types](#error-types). |
| `message` | Human-readable explanation. The wording can change; don't match on it. |
| `code` | The HTTP status code of the response. |
`errors` is always a single object, never an array. Responses from the API carry an `X-Request-ID` header, and you can send your own ([Request IDs](/going-to-production#request-ids)); include it when you [contact support](/support#what-to-include) about a failed request.
A few failures don't use this format:
- A request body larger than 4 MB returns `413` with `{"error": "Request Entity Too Large"}`.
- A request that runs longer than 180 seconds returns `504` from the gateway, with a non-JSON body.
- `GET /people/find`, `GET /companies/find`, and `GET /combined/find` return status `200` with an `errors` body, `code` 404 or 422, when nothing is found or the input is a disposable or webmail address. Check the body for `errors` on these endpoints.
## Status codes
| Status | Meaning | Retry? |
| ------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `200` | Success | — |
| `201` | Resource created. `GET /email-sources` also returns `201`, with no data, when it finds no sources. | — |
| `400` | Credentials are missing, malformed, or expired, or a parameter or plan rule rejected the request | No. Fix the request. |
| `401` | Credentials don't match an account, the account can't use the API, or the OAuth token is invalid | No |
| `402` | Not enough credits for the request | No. Wait for your usage window to renew or add credits. |
| `403` | The OAuth token lacks a scope, or the plan doesn't include the feature | No |
| `404` | The route or record doesn't exist | No |
| `408` | The email verifier couldn't complete the mailbox check | Yes, later |
| `409` | A duplicate record, or a bulk job whose status doesn't allow the action | No |
| `413` | The request body is larger than 4 MB | No |
| `422` | A parameter failed validation, or an account cap was reached | No. Fix the request. |
| `429` | A rate limit was hit, or the email count or sources balance is used up | Yes, after `Retry-After`, for rate limits |
| `451` | The address, profile, or phone number is withheld at its owner's request | No |
| `500` | Server error | Yes, with backoff |
| `504` | The request took longer than 180 seconds | Yes, or use [bulk jobs](/bulks) |
None of the error responses consume credits.
## Error types
| `errors.type` | Status | Cause | What to do |
| ---------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `params_invalid` | 422, 400 | A parameter failed validation (422), or a plan rule rejected it (400), such as the Free plan's result limit. | Correct the parameter named in the message. |
| `unknown_record` | 404, 400 | The record doesn't exist (404), or the email finder received a disposable or webmail domain (400). | Check the ID or the input. |
| `unknown_route` | 404 | The URL doesn't match an endpoint. | Check the method and path in the [API reference](/api). |
| `not_found` | 404 | The requested log entry doesn't exist. The find endpoints also return it inside a `200` response. | — |
| `authentication_failed` | 400, 401, 403 | The credentials are missing, malformed, or wrong, or the account can't use the API. | See [Authentication](/authentication#errors). |
| `api_key_expired` | 400 | The API key is past its expiry date. The `X-Tomba-Key-Expired` header gives the date. | [Rotate the key](/authentication#rotate-keys). |
| `invalid_token` | 401 | The OAuth access token is invalid, expired, or revoked. | Refresh the token or re-authorize. |
| `insufficient_scope` | 403 | The OAuth access token lacks the scope the endpoint needs. | Request the scope; see [OAuth 2.0](/authentication#oauth-20). |
| `forbidden` | 403 | A workspace member without admin rights requested team or member usage. | Use `scope=user`, or ask a workspace admin. |
| `quota_exceeded` | 402 | The search or verification balance, or the member's individual limit, is lower than the request's cost. | See [Credits and usage](/usage-and-quotas). |
| `rate_limit` | 429, 422 | 429: a rate limit was hit, or the email count or sources balance is used up. 422: the account reached its cap on API keys, leads, lead lists, or lead attributes. | For 429, see [Handle 429 responses](/rate-limits#handle-429-responses). For 422, remove items or change plan. |
| `proxy_error` | 408 | The email verifier's mailbox check failed. | Retry later. |
| `duplicate_record` | 409 | A flag for the same item is already pending review. | Wait for the review. |
| `invalid_state` | 409 | The bulk job's status doesn't allow the action, such as launching a job that is already running, or the job has expired. The message says which. | Check the job's `status`; see [Lifecycle](/bulks#lifecycle). |
| `claimed_email`, `claimed_linkedin`, `claimed_phone` | 451 | The person asked Tomba to stop processing their data, so the address, LinkedIn profile, or phone number is withheld permanently. | Remove the record from your data. |
| `ip_blocked` | 400 | Tomba's firewall blocks the request's IP address. | [Contact support](https://app.tomba.io/contact). |
| `openai_error`, `search_failed` | 422 | Company search couldn't process the query. | Retry, or simplify the query. |
| `internal_error` | 500 | A live technology lookup failed. | Retry with backoff. |
| `api_error` | 500, 400, 404, 422 | An unexpected error, or a bulk job without the requested result file (404). | Retry a 500 with backoff. If it persists, contact support with the `X-Request-ID`. |
## Handle errors
```javascript title="Node.js"
const res = await fetch(
"https://api.tomba.io/v1/email-finder?domain=stripe.com&full_name=Jane%20Doe",
{
headers: {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
},
},
);
const body = await res.json();
if (body.errors) {
const { type, message } = body.errors;
const requestId = res.headers.get("X-Request-ID");
if (type === "rate_limit" && res.status === 429) {
// Wait for Retry-After, then retry. See /rate-limits.
} else if (res.status === 408 || res.status >= 500) {
// Transient: retry later with backoff.
} else {
throw new Error(
`${res.status} ${type}: ${message} (request ${requestId})`,
);
}
} else {
console.log(body.data);
}
```
```python title="Python"
import os
import requests
response = requests.get(
"https://api.tomba.io/v1/email-finder",
params={"domain": "stripe.com", "full_name": "Jane Doe"},
headers={
"X-Tomba-Key": os.environ["TOMBA_API_KEY"],
"X-Tomba-Secret": os.environ["TOMBA_SECRET_KEY"],
},
timeout=180,
)
body = response.json()
if "errors" in body:
error = body["errors"]
request_id = response.headers.get("X-Request-ID")
if error["type"] == "rate_limit" and response.status_code == 429:
pass # Wait for Retry-After, then retry. See /rate-limits.
elif response.status_code == 408 or response.status_code >= 500:
pass # Transient: retry later with backoff.
else:
raise RuntimeError(
f"{response.status_code} {error['type']}: {error['message']} "
f"(request {request_id})"
)
else:
print(body["data"])
```
---
## Document: Tomba Enterprise
What the Tomba Enterprise plan adds to the API, and the enterprise apps: Tomba for Salesforce and Tomba for Snowflake.
URL: /enterprise
# Tomba Enterprise
This section is for Enterprise customers and teams evaluating the Enterprise plan. It covers what the plan changes for API users and documents the two enterprise apps, Tomba for Salesforce and Tomba for Snowflake.
## What the Enterprise plan includes
| Area | On Enterprise | Details |
| -------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| Credits | Search and verification allowances are set by contract. | [Quotas](/usage-and-quotas#quotas) |
| Rate limits | No per-second or per-minute limit; a daily limit per endpoint applies. | [Limits by plan](/rate-limits#limits-by-plan) |
| Single sign-on | SAML SSO and SCIM provisioning for your workspace. Both require an Enterprise plan. | [Workspace SSO settings](https://app.tomba.io/settings/workspace/sso) |
| Support | A named account manager, and an uptime SLA when your contract includes one. | [Support by plan](/support#support-by-plan) |
Enterprise accounts call the API the same way as every other plan; see the [Documentation](/introduction) and the [API reference](/api).
## Enterprise apps
| App | What it does | Requirements |
| ------------------------------------------------ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Tomba for Salesforce](/salesforce/introduction) | Finds, verifies, and enriches Leads, Contacts, and Accounts inside Salesforce. | A Tomba Enterprise plan and a subscription to the package; see [Pricing & Plans](/salesforce/pricing-and-licenses). |
| [Tomba for Snowflake](/snowflake/introduction) | A Snowflake Native App that finds, verifies, and enriches contacts in your tables. | A Snowflake account. Usage is billed on your Snowflake invoice, so it needs no Tomba plan; see [Pricing and limits](/snowflake/pricing-and-limits). |
## Talk to sales
To move to Enterprise or change an Enterprise contract, use the [contact form](https://app.tomba.io/contact). If you're already on Enterprise, your account manager handles credits, renewals, and plan changes.
---
## Document: Data sources and coverage
Where Tomba's contact and company data comes from, how addresses are found, checked, and scored, what's excluded, and how to remove data.
URL: /data
# Data sources and coverage
Tomba compiles business contact data from public web sources, generates likely addresses from each company's email format, and checks addresses against their mail servers. This page covers where the data comes from, how results are checked and scored, what Tomba leaves out, and how people remove their data. The [Privacy Policy](https://tomba.io/legal/privacy-policy) is the legal statement of these practices.
## Sources
- **Public web pages.** Tomba collects business contact details from company websites, public professional profiles, published articles, press releases, public directories, job postings, and similar public sources. It records each address with the page it was found on, which you get in [`sources`](/attributes/sources).
- **Generated addresses.** When Tomba has no stored address for a person, the [email finder](/api/finder#email-finder) builds candidates from the domain's most common email formats, checks them against the domain's mail server, and returns the most likely one with its verification status. If no candidate passes, Tomba doesn't search for the same name at the same domain again for 30 days.
- **Company records.** Company fields come from the company's website, public company profiles, and the domain's DNS and WHOIS records. Some fields, such as revenue, are estimates; see [Company response](/attributes/company).
## Checks and scores
- Each person result has a `score` from 0 to 100, based on the address's verification status and the number of public sources that list it.
- Domain search also returns addresses that Tomba's last check found invalid or couldn't confirm. Filter on `verification.status` if you only want valid addresses.
- Tomba derives each domain's email formats from the addresses it has found there, and reports them with their share in [`GET /email-format`](/api/finder#email-format).
## What Tomba excludes
- Personal email addresses, home addresses, personal phone numbers, and any data behind a login or paywall. Tomba doesn't collect them.
- Webmail and disposable domains. Domain search returns no addresses for them, and the email finder rejects them.
- Data whose owner asked for removal. See [Data removal](#data-removal).
## Freshness
Tomba keeps public contact data while its source page is live, re-verifies it regularly, and removes it when the source disappears. Each source's `last_seen_on` and `still_on_page` show when Tomba last found the address there. Tomba lists its data updates by month in [Data updates](/data/updates).
The [API Terms](https://tomba.io/legal/api-terms) require you to refresh or purge the contact data you store at least every 12 months.
## Data removal
Anyone can remove their email address, phone number, or LinkedIn profile from Tomba with the [Claim](https://app.tomba.io/claim) tool, or by emailing [privacy@tomba.io](mailto:privacy@tomba.io). Removal is free and doesn't need a Tomba account.
1. The person enters the address, phone number, or profile URL in the Claim tool. The tool can also correct their details instead of removing them.
2. Tomba emails a confirmation link.
3. When the person follows the link, Tomba stops returning the data: it's left out of results, and requests that name it directly return status `451`. Tomba also adds it to a suppression list so it isn't collected again.
Removal doesn't reach copies that customers exported before the request. A `451` response means the person asked Tomba to stop processing their data; don't process it yourself. See [Errors](/error-handling#error-types).
---
## Document: Data quality guarantee
What Tomba guarantees about Email Finder and Email Verifier results, and how to claim credits for incorrect data.
URL: /data-quality-guarantee
# Data quality guarantee
When an email address that Tomba returned hard-bounces, you can claim a credit refund. This page defines which Email Finder and Email Verifier results qualify and how to claim. It doesn't change the [Terms of Service](https://tomba.io/legal/terms-of-service), under which Tomba doesn't warrant that any address is deliverable. If you have a signed agreement with Tomba, its terms take precedence over this page.
## Email finder
Covers addresses returned by the [email finder](/api/finder#email-finder), [domain search](/api/finder#domain-search), [author finder](/api/finder#author-finder), and [LinkedIn finder](/api/finder#linkedin-finder), through the API or another Tomba tool.
An address qualifies when it meets the [conditions for both products](#conditions-for-both-products) and:
- Its `score` was 60 or higher when Tomba returned it.
## Email verifier
Covers addresses that the [email verifier](/api/verifier#email-verifier) returned with the `result` `deliverable` (see [Result values](/attributes/verifier#result-values)), through the API, bulk verification, or the Chrome extension.
An address qualifies when it meets the [conditions for both products](#conditions-for-both-products) and you sent to it within 30 days of its verification.
## Conditions for both products
- The message hard-bounced because the address doesn't exist: the receiving server rejected it permanently.
- You sent it from a domain authenticated with SPF, DKIM, and DMARC.
- You sent it in compliance with the [anti-spam policy](/anti-spam-policy).
These are not covered:
- Soft bounces and other temporary failures, such as a full mailbox or an unavailable server.
- Delivery to a spam or junk folder.
- Messages that the recipient's server or filters block or throttle, including rejections based on the message's content or the sender's reputation.
## How to claim
1. Report each address that hard-bounced. Use [`POST /v1/flag`](/api/flag#flag-incorrect-data) with `flag_type` `email` and `reason` `hard_bounce`, the `tomba flag create` command, or the report option on the result in the dashboard. Put the bounce message that your mail server received in `comment`.
2. Tomba reviews the report and confirms or rejects it. It can ask for the delivery log, the send date, and your domain's SPF, DKIM, and DMARC records to check the conditions above. Follow the report's `status` with [`GET /v1/flag`](/api/flag#list-my-flags), `tomba flag list`, or the [Flags](https://app.tomba.io/settings/flags) page of the dashboard.
3. For each confirmed report, Tomba refunds a credit as described in [Refunds for incorrect data](/usage-and-quotas#refunds-for-incorrect-data).
```bash title="curl"
curl -X POST "https://api.tomba.io/v1/flag" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"flag_type": "email", "value": "jane.doe@stripe.com", "reason": "hard_bounce", "comment": "550 5.1.1 The email account that you tried to reach does not exist."}'
```
```bash title="CLI"
tomba flag create --type email --value jane.doe@stripe.com --reason hard_bounce \
--comment "550 5.1.1 The email account that you tried to reach does not exist."
```
---
## Document: API changelog
Dated list of changes to the Tomba API v1: new endpoints, parameters, headers, status codes, and breaking changes.
URL: /changelog
# API changelog
This page lists changes to the Tomba API (v1), newest first. Refreshes of Tomba's data are listed in [Data updates](/data/updates), and SDK release notes are on each SDK's GitHub repository, linked from [SDKs](/libraries). [API versioning](/versioning) explains which changes count as breaking.
## September 2026
- Breaking September 30: Bulk jobs take new
parameter names, and the old ones are ignored. `maximum` is now
`max_emails_per_domain`; the `email_type` and `department` objects are
`email_type` with `email_type_mode`, and `departments` with
`departments_mode`; `verify` is `verify_emails`, `sources` is
`include_sources`, `phone` is `find_phones`, `full` is `find_all_phones`,
`valid` is `boost_score_from_sources`, `skip` is `skip_rows_with_email`, and
`notifie` is `notify`. The one-based `column` and `column_*` parameters are
replaced by zero-based `*_field_index` parameters, and columns are found from
the header row when you don't set them. Downloads take `file` instead of
`type`, and lists take `archived=true` instead of `filter=archived`. Update
your create requests; see [Create a job](/bulks#create-a-job).
- Breaking September 30: Bulk jobs return their options in `config` and their results in `metrics`. These replace `input_type`, `maximum`, `email_type`, `department`, `sources`, `verify`, `phone`, `full`, `notifie`, `upload`, `total`, `total_list`, `total_emails`, `chart`, `time_track`, `search_cost`, `verify_cost`, `phone_cost`, `launched`, and `processed_email`; `used` is now `billed`, and `table` is `preview`. `GET /v1/bulk/{type}/{id}` returns the job as an object in `data`, and `404 unknown_record` for a missing job instead of an empty array. The progress of a job that isn't launched has the status `pending` instead of `{"data": []}`, and an action that the job's status doesn't allow returns `409 invalid_state` instead of `404`. `DELETE /v1/bulk/linkedin/{id}` is removed; use `DELETE /v1/bulk/linkedin/{id}/delete`. A job past its `expired_at`, 180 days after it was created, can no longer be downloaded, launched, or retried: those requests return `409 invalid_state`. See [Job fields](/bulks#job-fields).
- September 30: Bulk jobs accept rows as JSON `data`, start when created with `launch`, and send a signed event to `webhook_url` when they end. Lists take `expired=true` or `false`. New routes cancel, retry, restore, and estimate a job and return its history, and `GET /v1/bulk/types`, `GET /v1/bulk/stats`, and `GET /v1/bulk/webhook-secret` return the bulk types, your bulk activity, and the key events are signed with. The limit of 2 jobs per type now counts running jobs only. See [Bulk operations](/bulks) and [Bulk job events](/webhook#bulk-job-events).
- September 21: People can now remove their phone number or LinkedIn profile from Tomba, as well as their email address. Phone Finder, Phone Validator, and LinkedIn Finder return `451` with error type `claimed_phone` or `claimed_linkedin` for removed data; delete that data from your records. See [Data removal](/data#data-removal).
- Breaking September 20: A key and secret that
don't match an account now return `401` instead of `400`, as do requests from
an account that is unconfirmed, has no password set, or has an expired
subscription. Missing, malformed, and expired credentials still return `400`;
if your client branches on the status code, handle both. See [Authentication
errors](/authentication#errors).
- Breaking September 20: API keys now expire,
and existing keys were given an expiry date. A request with an expired key
fails with `400` and error type `api_key_expired`, and the
`X-Tomba-Key-Expired` header gives the expiry time. Keys also gain a `name`,
`expires_at`, and `last_used_at`; `POST /v1/keys` accepts `name` and
`expires_in_days`, and `PATCH /v1/keys/{id}` renames a key. Check your keys'
`expires_at` and rotate them before they lapse. See [Key
expiry](/authentication#key-expiry).
- September 8: OAuth apps can register themselves with dynamic client registration at `POST /v1/oauth/register`. See [OAuth 2.0](/authentication#oauth-20).
## August 2026
- August 29: The API accepts OAuth 2.0 access tokens, sent as `Authorization: Bearer `, as an alternative to a key and secret. Apps get tokens through the authorization code flow with PKCE or the device flow, and discover the endpoints at `https://api.tomba.io/.well-known/oauth-authorization-server`. See [OAuth 2.0](/authentication#oauth-20).
- Breaking August 28: `GET /v1/logs` is now
paginated: it returns 20 entries per page by default, with a `meta` object,
instead of up to 3,000 entries in one response. Use `page` and `limit` to read
further pages. Entries gain `http_method`, `status_code`, `endpoint`,
`duration_ms`, and `api_key_id`, the endpoint accepts filters such as `type`,
`status_code`, `start`, and `end`, and `GET /v1/logs/{id}` returns one entry.
See [Retrieve API logs](/api/account#retrieve-api-logs).
- August 25: `POST /v1/flag` reports an incorrect result, and `GET /v1/flag` lists your reports. See [Flag incorrect data](/api/flag#flag-incorrect-data) and [Refunds for incorrect data](/usage-and-quotas#refunds-for-incorrect-data).
- August 12: A request can now run for up to 180 seconds before the gateway ends it with `504`. Set your client timeout to at least 180 seconds. See [Response format](/introduction#response-format).
- August 12: Higher-tier plans no longer have per-endpoint rate limits. See [Limits by plan](/rate-limits#limits-by-plan).
- August 3: Rate limits now depend on your plan and apply to each endpoint per second, per minute, and per day. Responses carry `RateLimit`, `RateLimit-Policy`, and `x-*-rate-limit` headers, a `429` carries `Retry-After`, and `GET /v1/rate-limits` returns the limits that apply to your account. See [Rate limits](/rate-limits) and [Retrieve rate limits](/api/account#retrieve-rate-limits).
## July 2026
- July 30: Email addresses whose owners asked Tomba to remove them are left out of Domain Search results. Email Finder, Email Verifier, Author Finder, Email Enrichment, and the person and combined enrichment endpoints return `451` with error type `claimed_email` for them; delete that address from your records. See [Data removal](/data#data-removal).
## April 2026
- Breaking April 21: When your search or
verification balance is lower than a request's cost, the request now fails
with `402` and error type `quota_exceeded`, instead of `429` and `rate_limit`.
Domain Search returns the request's estimated cost in the `X-Estimated-Cost`
header, and Email Verifier in `X-Estimated-Verify-Cost`. Handle `402`
separately from `429`, and don't retry it. See [Credit
costs](/usage-and-quotas#credit-costs).
## March 2026
- March 28: Finder, verifier, enrichment, and phone finder endpoints accept an optional `webhook_url` parameter. Tomba returns the result as usual and also posts it to that URL. See [Per-request callbacks](/webhook#per-request-callbacks).
## February 2026
- February 26: LinkedIn Finder accepts `full=true` to return every address Tomba has stored for the profile instead of the single most likely one. See [LinkedIn Finder](/api/finder#linkedin-finder).
- February 4: Email Finder, Email Enrichment, and LinkedIn Finder accept `enrich_mobile=true` to include the person's phone numbers in `data.phone_data`. Phone numbers cost extra credits; see [Credit costs](/usage-and-quotas#credit-costs).
## January 2026
- Breaking January 19: Email Format reports its
quota in the `X-Count-Limit` and `X-Count-Remaining` headers instead of
`X-Search-Limit` and `X-Search-Remaining`. Update any code that reads those
headers from Email Format responses. See [Check
usage](/usage-and-quotas#check-usage).
## December 2025
- December 29: Similar domains is paginated: it accepts `page` and `limit` and returns a `meta` object. See [Similar](/api/domain#similar).
## September 2025
- September 3: `POST /v1/reveal/search` accepts an API key and secret. See [Search companies](/api/reveal#search-companies).
## June 2025
- June 13: `GET /v1/location` returns, for each country, how many of a domain's email addresses belong to people there. See [Location](/api/finder#location).
## May 2025
- May 22: Phone Finder is available at `GET /v1/phone-finder`. The older `/v1/phone/{email}` path keeps working; see [Legacy paths](/versioning#legacy-paths).
- May 21: `GET /v1/phone-validator` checks whether a phone number is valid, and Phone Finder accepts a `domain` as well as an `email`. See [Phone validator](/api/phone#phone-validator) and [Phone finder](/api/phone#phone-finder).
- May 21: `GET /v1/domain-suggestions` returns companies whose name or domain matches a query, with their domains. See [Get domain suggestions](/api/domain-suggestions#get-domain-suggestions).
## April 2025
- April 17: `GET /v1/people/find`, `GET /v1/companies/find`, and `GET /v1/combined/find` enrich a person, a company, or both. See [Person API](/api/enrichment#person-api), [Company API](/api/enrichment#company-api), and [Combined API](/api/enrichment#combined-api).
---
## Document: Bulk operations
Run Tomba finder, verifier, and enrichment jobs over lists: create, launch, follow, and download bulk jobs, with their limits and billing.
URL: /bulks
# Bulk operations
A bulk job runs one Tomba operation over a list. You create the job with your input, launch it, follow its progress, and download the results as CSV. Credits are charged when you first download the results; see [Billing](#billing).
Every bulk endpoint takes the bulk type in its path, as in `POST /v1/bulk/{type}`, and accepts the same credentials as the rest of the API; OAuth tokens need the `leads` scope. Jobs are shared by all members of a workspace.
## Bulk types
| Type | `{type}` | Input | Maximum rows |
| ------------------------------------------------- | ----------------- | ------------------------------------------ | -----------: |
| [Domain search](#domain-search) | `search` | Domains or company names | 25,000 |
| [Similar domains](#similar-domains) | `similar` | Domains or company names | 5,000 |
| [Company enrichment](#company-enrichment) | `company` | Domains or company names | 25,000 |
| [Email finder](#email-finder) | `finder` | Names, with a domain or company name | 10,000 |
| [Email enrichment](#email-enrichment) | `enrich` | Email addresses | 20,000 |
| [LinkedIn finder](#linkedin-finder) | `linkedin` | LinkedIn profile URLs | 20,000 |
| [Author finder](#author-finder) | `author` | Article URLs | 5,000 |
| [Email verifier](#email-verifier) | `verifier` | Email addresses | 20,000 |
| [Phone finder](#phone-finder) | `phone-finder` | Email addresses, LinkedIn URLs, or domains | 10,000 |
| [Phone validator](#phone-validator) | `phone-validator` | Phone numbers, with an optional country | 10,000 |
| [Websites by technology](#websites-by-technology) | `technology` | Technology slugs, such as `react` | 10 |
| [Domain export](#domain-export) | `export` | One domain | 1 |
Rows beyond the maximum aren't processed. `GET /v1/bulk/types` returns every type with its fields, options, and limits.
### Domain search
Returns the email addresses of each domain, or of each company name with `input_type` set to `company`. It takes the options `input_type`, `max_emails_per_domain`, `email_type`, `departments`, `verify_emails`, `find_phones`, `include_sources`, and `boost_score_from_sources`; see [Options](#options).
`verify_emails` is turned off, without an error, when `max_emails_per_domain` is above 10 or the input has more than 2,000 rows.
```bash
curl -X POST "https://api.tomba.io/v1/bulk/search" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q3 target accounts",
"list": "stripe.com\nshopify.com\nnotion.so",
"max_emails_per_domain": 10,
"email_type": "personal",
"verify_emails": true,
"launch": true
}'
```
### Similar domains
Returns companies similar to each domain, with their email addresses. It takes the options `input_type` and `max_emails_per_domain`.
### Company enrichment
Returns a company profile for each domain, or for each company name with `input_type` set to `company`. It takes the options `input_type` and `find_phones`.
### Email finder
Returns one address per person. Send `data` or a CSV `file` with a first and last name, or a full name, and a domain or company name; a `list` isn't accepted. It takes the options `find_phones`, `include_sources`, and `skip_rows_with_email`.
```bash
curl -X POST "https://api.tomba.io/v1/bulk/finder" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
-F "name=Conference leads" \
-F "file=@leads.csv;type=text/csv" \
-F "launch=true"
```
With a header row such as `first_name,last_name,company_website`, the columns are found by name. For other headers, set `first_name_field_index`, `last_name_field_index`, and `domain_field_index`; see [Columns](#columns).
### Email enrichment
Returns person and company data for each email address. It takes the options `find_phones` and `include_sources`.
### LinkedIn finder
Returns the email address for each profile URL, in the form `https://www.linkedin.com/in/`. It takes the options `find_phones` and `include_sources`.
### Author finder
Returns the author's email address for each article URL. Use article URLs, not home pages. It takes the option `include_sources`.
### Email verifier
Returns the [verification result](/attributes/verifier) for each address. Verification is always on for this type. It takes the options `find_phones` and `include_sources`.
```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", "john.doe@shopify.com"],
"launch": true,
"webhook_url": "https://hooks.example.com/tomba/7f3c9a"
}'
```
### Phone finder
Returns a phone number for each email address, LinkedIn profile URL, or domain. A `list` can mix the three. In a file with several of these columns, they're found from the header, or set `email_field_index`, `linkedin_url_field_index`, or `domain_field_index`. With `find_all_phones` set to `true`, every number found is returned instead of one. Jobs of this type have the `bulk_type` `phone`.
### Phone validator
Returns validity, line type, and carrier for each phone number. In a file, the phone number column and an optional country column are found from the header, or set `phone_field_index` and `country_field_index`.
### Websites by technology
Returns the websites that use each technology, given as slugs such as `react` or `shopify`, up to 10 per job. Each technology returns at most as many websites as your plan's search credits when you create the job.
### Domain export
Exports the email addresses of one domain. Send `domain` instead of an input, and optionally `filter`: `all` (the default), `personal`, `generic`, or a department. It takes the options `find_phones` and `include_sources`, and `name` defaults to the domain.
A domain export needs a paid plan and starts when it's created. It has three routes: create, progress, and download, with the `full` file only.
## Create a job
`POST /v1/bulk/{type}` with a JSON body, or with a `multipart/form-data` form to upload a CSV file. Every parameter works in both; the [API reference](/api/bulks#create-a-bulk-job) describes each one.
| Parameter | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| `name` | 3 to 200 characters. Required, except for a domain export. |
| `list`, `data`, `file` | The input; see [Input](#input). |
| `has_header` | `false` when the first row of `data` or of the file isn't a header. Default `true`. |
| `delimiter` | Column separator of the file: `,` (the default), `;`, `\|`, or `tab`. |
| `*_field_index` | Zero-based column of a field; see [Columns](#columns). |
| Options | Depend on the type; see [Options](#options). |
| `notify` | `true` emails you when the job finishes with at least one result. |
| `launch` | `true` launches the job right after creating it; see [Lifecycle](#lifecycle). |
| `webhook_url` | An `https://` URL that receives an event when the job ends; see [Bulk job events](/webhook#bulk-job-events). |
A field index or an option that the type doesn't use returns `422 params_invalid`, except `verify_emails`, which is ignored. Other unknown parameters are ignored, including the names used before September 30, 2026, such as `maximum`, `verify`, `sources`, `notifie`, `column`, and `column_first`; see the [changelog](/changelog).
### Input
Send the input in one of three ways:
- **`list`:** one value per line, as a string or a JSON array. Tomba trims, lowercases, and de-duplicates the values, drops the invalid ones, and keeps the first values up to the row limit. The email finder doesn't accept a list.
- **`data`:** an array of rows, each an array of cells, for input with several columns. The first row is a header unless `has_header` is `false`. In a multipart form, send it as a JSON-encoded string.
- **`file`:** a CSV file of up to 4 MB, in a `multipart/form-data` request. The first row is a header unless `has_header` is `false`. Save spreadsheets as CSV first: `.xlsx`, `.xls`, `.numbers`, and `.ods` files are refused.
Domain search, similar domains, company enrichment, and websites by technology clean the values of `data` and `file` as they clean a list. Sending both `data` and `file` returns `422`. A list with no valid value returns `400 unknown_record`.
### Columns
With `data` or `file`, Tomba finds each column from the header row: a header such as `Company Website`, `domain`, or `url` is read as the domain. Matching ignores case, spaces, and punctuation. To choose a column yourself, set its field index to the column's zero-based position: the first column is `0`. Input with a single column needs neither.
| Field | Parameter | Types |
| --------------------- | ------------------------------------------------- | -------------------------------------------------------- |
| Domain | `domain_field_index` | `search`, `similar`, `company`, `finder`, `phone-finder` |
| Company name | `company_name_field_index` | `search`, `similar`, `company`, `finder` |
| First name, last name | `first_name_field_index`, `last_name_field_index` | `finder` |
| Full name | `full_name_field_index` | `finder` |
| Email address | `email_field_index` | `enrich`, `verifier`, `phone-finder` |
| LinkedIn profile URL | `linkedin_url_field_index` | `linkedin`, `phone-finder` |
| Article URL | `url_field_index` | `author` |
| Phone number, country | `phone_field_index`, `country_field_index` | `phone-validator` |
| Technology | `technology_field_index` | `technology` |
`GET /v1/bulk/types` lists the header names matched for each field in `fields[].header_synonyms`. When a required column isn't found, the request fails with `422 params_invalid`, and the message names the parameter to set.
### Options
| Option | Types | Effect |
| --------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input_type` | `search`, `similar`, `company` | `domain` or `company`. By default, `company` when the input has a company name column and no domain column, and `domain` otherwise. |
| `max_emails_per_domain` | `search`, `similar` | Addresses per domain: `1`, `10` (the default), `20`, `50`, or `100`. |
| `email_type`, `email_type_mode` | `search` | `email_type` is `all` (the default), `personal`, or `generic`. With `email_type_mode` `only` (the default), only that type is returned; with `priority`, every type is. |
| `departments`, `departments_mode` | `search` | Department names, such as `["sales", "marketing"]`. With `departments_mode` `only` (the default), only those departments are returned; with `priority`, every department is. The values are in the [API reference](/api/bulks#create-a-bulk-job). |
| `verify_emails` | `search` | `true` verifies the addresses found. |
| `find_phones` | `search`, `company`, `finder`, `enrich`, `linkedin`, `verifier`, `export` | `true` adds phone numbers, at an extra cost. |
| `include_sources` | `search`, `finder`, `enrich`, `linkedin`, `author`, `verifier`, `export` | `true` adds the web pages where each address was found. |
| `boost_score_from_sources` | `search` | `true` adds 10 points, up to 90, to the score of an address that couldn't be verified but was found on at least 5 web pages. |
| `skip_rows_with_email` | `finder` | `true` skips the rows that already have a value in a column named `email`. |
| `find_all_phones` | `phone-finder` | `true` returns every phone number found instead of one. |
| `filter` | `export` | `all` (the default), `personal`, `generic`, or a department name. |
`GET /v1/bulk/types` also lists options that some types accept without using
them, such as `email_type` for similar domains. They have no effect, except
`verify_emails`: on similar domains, company enrichment, email finder, email
enrichment, LinkedIn finder, and author finder jobs, it adds verification
credits without verifying anything. See [Billing](#billing).
## Lifecycle
A job is `pending` until it's launched, then `running`, and it ends `completed`, `failed`, or `cancelled`. The examples use the `search` type; replace it with the type of your job. Every operation is in the [API reference](/api/bulks).
1. **Create** the job. The response contains its ID:
```json
{ "data": { "id": 4521 } }
```
With `"launch": true`, the job starts right away. If it can't, it stays `pending`, and `data.message` says why:
```json
{
"data": {
"id": 4521,
"message": "Bulk created but not launched: You already have 2 Domain Search bulks running. Wait for one to finish, then launch this one."
}
}
```
2. **Launch** it, unless you set `launch`. Only a `pending` job can be launched; any other status returns `409 invalid_state`. The launch fails with `429 rate_limit` while 2 jobs of the type are running, or when a kind of credit the job needs is used up.
```bash
curl -X PUT "https://api.tomba.io/v1/bulk/search/4521" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
3. **Follow** it. Poll its progress until `status` is `completed`, `failed`, or `cancelled`, or set `webhook_url` to receive an event when it ends; see [Bulk job events](/webhook#bulk-job-events). The progress isn't wrapped in `data`, and it also includes `metrics`, the results so far:
```bash
curl "https://api.tomba.io/v1/bulk/search/4521/progress" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
```json
{
"status": "running",
"progress": 42,
"processed": 1260,
"error_message": null,
"rows_per_minute": 420,
"eta_seconds": 248
}
```
4. **Download** the results as CSV once the job is `completed`:
```bash
curl -o results.csv \
"https://api.tomba.io/v1/bulk/search/4521/download?file=full" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
| `file` | File |
| ----------- | -------------------------------------------------------------------------------------------------------------------------- |
| `full` | Every result. The default, and the only file of a domain export. |
| `valid` | Only rows with a valid result. Not available for company enrichment and similar domains. |
| `not_found` | Input rows that returned no result. Not available for email finder jobs, or for email verifier jobs created from a `list`. |
A job that isn't `completed` returns `409 invalid_state`, and a file that doesn't exist returns `404 api_error`.
A job expires 180 days after it's created, at `expired_at`. From then on, `expired` is `true`, and downloading, launching, or retrying the job returns `409 invalid_state`: download the results before that date.
To manage jobs:
| Operation | Request |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| List jobs | `GET /v1/bulk/{type}?page=1&limit=10`, newest first. `limit` is at most 50. Filter with `status`, with `q` for text in the name, with `expired=true` or `expired=false`, and with `archived=true` for archived jobs. |
| Get a job | `GET /v1/bulk/{type}/{id}`. A job that doesn't exist or was deleted returns `404 unknown_record`. |
| Estimate its cost | `GET /v1/bulk/{type}/{id}/estimate`; see [Billing](#billing). |
| Read its history | `GET /v1/bulk/{type}/{id}/history`: its events, such as the launch, downloads, and webhook deliveries, oldest first. |
| Cancel a job | `POST /v1/bulk/{type}/{id}/cancel`. Only a `pending` job can be cancelled. |
| Retry a job | `POST /v1/bulk/{type}/{id}/retry`. Runs a `failed` or `cancelled` job again from its input, and launches it. |
| Rename a job | `PUT /v1/bulk/{type}/{id}/rename` with `{"name": "New name"}` |
| Archive a job | `DELETE /v1/bulk/{type}/{id}/archive`, for a `completed`, `failed`, or `cancelled` job. Restore it with `PUT /v1/bulk/{type}/{id}/restore`. |
| Delete a job | `DELETE /v1/bulk/{type}/{id}/delete` |
`GET /v1/bulk/stats` sums your bulk activity over a period, the last 30 days by default: jobs by status, rows processed, results, and credits billed, in total, per type, and per day.
## Job fields
`GET /v1/bulk/{type}/{id}` returns the job in `data`. `GET /v1/bulk/{type}` returns a list of jobs, without `preview`.
| Field | Description |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `bulk_id` | Job ID |
| `bulk_type` | The type. Phone finder jobs are `phone`. |
| `name`, `user_id` | The job's name, and the user who created it |
| `status` | `pending`, `running`, `completed`, `failed`, or `cancelled` |
| `progress`, `processed` | Percentage and number of rows processed |
| `billed` | `true` once the results were downloaded and the job charged |
| `error_message`, `retry_count` | Why the job failed, and how many times it was retried |
| `created_at`, `started_at`, `completed_at`, `cancelled_at` | When the job was created, started, completed, and cancelled |
| `archived`, `expired`, `expired_at` | Whether the job is archived, whether it has expired, and when it expires |
| `config` | The input and options: `input.rows`, `input.fields` (the zero-based column of each field), `options`, `notify`, and `webhook.url` |
| `metrics` | The results: `rows`, `results` (`found`, `not_found`, `hit_rate`, and `records`), `distribution`, `emails`, `phones`, `credits`, and `timing` |
| `preview` | The first result rows, once `billed` is `true` |
The fields of `config` and `metrics` are described in the [API reference](/api/bulks#get-a-bulk-job).
## Limits
These limits apply per workspace, or per user outside a workspace. Bulk endpoints aren't subject to the plan [rate limits](/rate-limits).
| Limit | Value |
| ------------ | ----------------------------------------------------------------------------------------------- |
| Jobs created | 15 per day for each bulk type, and 30 domain exports |
| Running jobs | 2 per bulk type. While 2 are running, no job of that type can be created, launched, or retried. |
| Downloads | 25 per day for each job, and 30 for a domain export |
| Request size | 4 MB. Larger requests fail with `413`, and a larger file with `400`. |
| Rows | See [Bulk types](#bulk-types) |
Exceeding a daily limit returns `429 rate_limit` with a `Retry-After` header. The running-job limit returns `429 rate_limit` with the message "You already have 2 Domain Search bulks running. Wait for one to finish, then launch this one.", naming the type.
## Billing
Nothing is charged when you create or launch a job. At launch, Tomba checks that you have credits of each kind the job needs; if one is used up, the launch fails with `429 rate_limit`. The job's full cost is charged the first time you download any of its results; later downloads are free. The [duplicate-request rule](/usage-and-quotas#duplicate-requests) doesn't apply, so running the same list again is charged again.
Before the first download, Tomba checks your balance. If it's lower than the job's cost, the download fails with `429 rate_limit` and a message that gives the remaining balance and the cost.
`GET /v1/bulk/{type}/{id}/estimate` returns the cost and your remaining balance: an upper bound before the job completes (`basis` is `upper_bound`), and the exact charge after (`exact`). A completed job also shows its charge in `metrics.credits`.
| Type | Search credits | Verification credits |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
| Domain search | Per domain with at least one address: 1 per 10 addresses returned, at most 10. Plus 5 per address with phone data. | With `verify_emails`: 1 per address returned |
| Similar domains | 1 per domain with at least one similar company | — |
| Company enrichment | 1 per company found. Plus 1 per company with phone data. | — |
| Email finder | 1 per row whose address is verified valid. Plus 5 per row with phone data. | — |
| Email enrichment | 1 per row with a result. Plus 5 per row with phone data. | — |
| LinkedIn finder | 1 per row with a result. Plus 5 per row with phone data. | — |
| Author finder | 1 per article with an author address | — |
| Email verifier | 5 per row with phone data | 1 per row, except disposable addresses |
| Phone finder | 5 per row with at least one phone number | — |
| Phone validator | 1 per valid number | — |
| Websites by technology | 1 per 10 websites found, rounded up | — |
| Domain export | 1 per 10 addresses, rounded up. Plus 5 per address with phone data. | — |
Set `verify_emails` only on domain search jobs. On similar domains, company enrichment, email finder, email enrichment, LinkedIn finder, and author finder jobs, it doesn't run a verification but adds one verification credit per result row.
## Errors
| Status | `errors.type` | Cause |
| ------ | ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `400` | `api_error` | The JSON body or the CSV file can't be read, or the file is over 4 MB |
| `400` | `unknown_record` | No value of the `list` is valid for the type |
| `404` | `unknown_record` | No job of this type has this ID in your account or workspace, or it was deleted |
| `404` | `api_error` | The job has no result file of the requested kind |
| `409` | `invalid_state` | The job's status doesn't allow the action, such as launching a `running` job, or the job has expired |
| `413` | — | The request is over 4 MB |
| `422` | `params_invalid` | A parameter is missing or invalid, or the type doesn't use it. The message names it. |
| `429` | `rate_limit` | A daily limit, the running-job limit, or your credit balance. See [Limits](#limits) and [Billing](#billing). |
The response format and the other error types are in [Errors](/error-handling).
---
## Document: Authentication
Authenticate Tomba API requests with an API key and secret, handle key expiry and rotation, and use OAuth 2.0 access tokens.
URL: /authentication
# Authentication
Every request to the Tomba API carries an API key and your account secret as headers. Apps that act on behalf of other Tomba users use [OAuth 2.0](#oauth-20) access tokens instead.
## Request headers
| Header | Value |
| ---------------- | ---------------------------------------------- |
| `X-Tomba-Key` | An API key: `ta_` followed by 37 characters. |
| `X-Tomba-Secret` | Your account secret: `ts_` followed by a UUID. |
```bash title="curl"
curl "https://api.tomba.io/v1/me" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
```javascript title="Node.js"
const response = await fetch("https://api.tomba.io/v1/me", {
headers: {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
},
});
console.log(await response.json());
```
```python title="Python"
import os
import requests
response = requests.get(
"https://api.tomba.io/v1/me",
headers={
"X-Tomba-Key": os.environ["TOMBA_API_KEY"],
"X-Tomba-Secret": os.environ["TOMBA_SECRET_KEY"],
},
timeout=180,
)
print(response.json())
```
Your account has one secret, shared by all of its keys. Keys are what you create, name, expire, and revoke for each integration.
Keep both values on a server ([Security](/security#api-credentials)). Anyone who obtains them can spend your credits, and every key of an account shares the account's [rate limits](/rate-limits).
## Get your key and secret
Create keys and copy your secret on the [API keys](https://app.tomba.io/api) page of the dashboard. `GET /v1/me` also returns the secret, as `secret_token`.
## Key expiry
- Every key expires. The default and maximum lifetime is 365 days. When you create a key through the API, set `expires_in_days` to any value from 1 to 365.
- Tomba emails the account owner 30 days, 7 days, and 1 day before a key expires.
- `GET /v1/keys` returns each key's `expires_at` and `expired` fields.
A request with an expired key fails with status `400` and error type `api_key_expired`. The response includes an `X-Tomba-Key-Expired` header with the expiry time in RFC 3339 format:
```json
{
"errors": {
"type": "api_key_expired",
"message": "This API key expired on March 1, 2025. Create a new key at https://app.tomba.io/api",
"code": 400
}
}
```
## Rotate keys
To replace a key without downtime:
1. Create a new key in the dashboard or with `POST /v1/keys`.
2. Deploy the new key to your integration.
3. Delete the old key in the dashboard or with `DELETE /v1/keys/{id}`.
```bash
curl -X POST "https://api.tomba.io/v1/keys" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "billing-service", "expires_in_days": 90}'
```
The response returns the new key in `data.message`:
```json
{ "data": { "id": 1234, "message": "ta_xxxx" } }
```
`PUT /v1/keys/{id}` rotates a key in place instead: it issues a new value with a new 365-day term, and the old value stops working immediately. The response doesn't include the new value; read it with `GET /v1/keys/{id}`.
To rotate the secret, use [Settings](https://app.tomba.io/settings) in the dashboard. Every key then works only with the new secret, so update all integrations at once.
| Operation | Endpoint |
| ------------ | ---------------------------------------------------------- |
| List keys | [`GET /v1/keys`](/api/keys#retrieve-api-keys) |
| Get a key | [`GET /v1/keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| Create a key | [`POST /v1/keys`](/api/keys#create-an-api-key) |
| Rename a key | [`PATCH /v1/keys/{id}`](/api/keys#rename-an-api-key) |
| Rotate a key | [`PUT /v1/keys/{id}`](/api/keys#reset-an-api-key) |
| Delete a key | [`DELETE /v1/keys/{id}`](/api/keys#delete-an-api-key) |
The number of active keys an account can hold depends on its plan; expired keys don't count toward it. In a workspace, `GET /v1/keys` returns the keys of every member, including their values.
## OAuth 2.0
Apps that act on behalf of other Tomba users, such as MCP clients and workflow tools, use OAuth 2.0 and send the access token as `Authorization: Bearer `.
| Item | Value |
| --------------------------- | ----------------------------------------------------------------- |
| Discovery | `https://api.tomba.io/.well-known/oauth-authorization-server` |
| Authorization endpoint | `https://app.tomba.io/oauth/authorize` |
| Token endpoint | `https://api.tomba.io/v1/oauth/token` |
| Revocation endpoint | `https://api.tomba.io/v1/oauth/revoke` |
| Dynamic client registration | `https://api.tomba.io/v1/oauth/register` |
| Device authorization | `https://api.tomba.io/v1/oauth/device` |
| Grant types | Authorization code with PKCE (`S256`), refresh token, device code |
| Token lifetime | Access tokens: 15 minutes. Refresh tokens: 30 days. |
Request only the scopes your app needs:
| Scope | Grants access to |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `search` | Domain search, email finder, author finder, LinkedIn finder, phone finder, similar domains, technology, company search, domain suggestions |
| `verify` | Email verifier |
| `enrich` | Email enrichment, person, company, and combined enrichment, email sources |
| `leads` | Leads, lead lists, lead attributes, bulk jobs |
| `account` | Account settings and API keys |
| `read` | Account details, rate limits, email count, email format, location, domain status, phone validator |
`GET /v1/logs` and `GET /v1/usage` don't accept OAuth tokens; call them with a key and secret. Users review and revoke app access under [Connected apps](https://app.tomba.io/settings/connected-apps).
The [remote MCP server](/llm/remote-mcp/introduction) accepts additional credential formats; its page lists them.
## Endpoints without authentication
The [Logo API](/logo-api) and `GET /v1/domain-status` need no credentials.
## Errors
| Status | `errors.type` | Cause |
| ------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `authentication_failed` | A credential header is missing or malformed. |
| 400 | `api_key_expired` | The key is past its expiry date. |
| 401 | `authentication_failed` | The key and secret don't match an account, or the account can't use the API yet: it's unconfirmed, has no password set, or its subscription has expired. The message says which. |
| 403 | `authentication_failed` | The account is blocked or deleted. |
| 401 | `invalid_token` | The OAuth access token is invalid, expired, or revoked. |
| 403 | `insufficient_scope` | The OAuth access token lacks the scope the endpoint needs. |
[Errors](/error-handling) lists every error type.
---
## Document: Anti-spam policy
The anti-spam rules for using Tomba data: prohibited uses, your obligations when you send email, how to report abuse, and how Tomba enforces the rules.
URL: /anti-spam-policy
# Anti-spam policy
This page summarizes the anti-spam rules in Tomba's [Acceptable Use Policy](https://tomba.io/legal/acceptable-use-policy), which governs how you use Tomba and the data you get from it. If this page and the Acceptable Use Policy differ, the Acceptable Use Policy applies.
## Prohibited uses
Don't use Tomba, or data you get from Tomba, to:
- Send unsolicited bulk email, spam, or any commercial message that doesn't meet applicable anti-spam law.
- Send phishing, pretexting, social engineering, fraud, or any other deceptive or misleading message.
- Harass, threaten, stalk, dox, defame, or intimidate anyone.
- Keep contacting someone who has opted out, unsubscribed, or asked not to be contacted.
The Acceptable Use Policy lists the other prohibited uses, such as reselling Tomba data or targeting people in their personal capacity.
## Sending rules
You're responsible for every message you send to a contact you found through Tomba. For each message, you must:
- Have a lawful basis for processing the contact's personal data under the GDPR, the UK GDPR, and any other data protection law that applies.
- Comply with the anti-spam and marketing laws that apply, including the CAN-SPAM Act (US), CASL (Canada), the ePrivacy Directive and its national implementations (EU), and PECR (UK).
- Identify yourself as the sender accurately, include a valid postal address, and use a truthful subject line.
- Include a working, clearly visible unsubscribe mechanism in every commercial message.
- Honor opt-out and unsubscribe requests promptly, and keep those contacts out of future outreach.
- Provide any privacy notice you owe as a data controller, including notice at first contact where Article 14 of the GDPR requires it.
- Not suggest that Tomba endorses, sponsors, sent, or is responsible for your messages.
## Report abuse
To report someone using Tomba in breach of these rules, email [support@tomba.io](mailto:support@tomba.io). Include the account, domain, or message involved, with the message headers or any other evidence. Tomba investigates reports and doesn't share the outcome with third parties.
Report security issues to [security@tomba.io](mailto:security@tomba.io), and data removal requests to [privacy@tomba.io](mailto:privacy@tomba.io).
## Enforcement
When Tomba reasonably believes you've broken these rules, it can take any of these steps, in proportion to the breach:
- Throttle or rate-limit your usage.
- Block IP addresses or request patterns.
- Remove or disable access to the offending content.
- Suspend seats, API keys, or the whole account.
- Terminate your agreement under the [Terms of Service](https://tomba.io/legal/terms-of-service).
- Report the activity to law enforcement or another competent authority where the law requires or allows it.
Where practicable, Tomba gives you notice and a chance to fix a breach that can be fixed. It can act immediately and without notice when there's a security risk, a risk of harm to a person, a threat to the stability of the service, or a legal requirement.
---
## Document: Usage, limits and alerts
See who uses Tomba credits, cap usage per user, and get alerted before your plan runs out.
URL: /salesforce/usage-and-limits
# Usage, limits and alerts
## Usage log
Every lookup is recorded per day, user and source (Record Action, Search, Bulk, Automation, Flow, Agent) in
**Tomba Usage** records. **Tomba Setup › Usage** shows the last 30 days by source and the top users this month,
and the **Tomba Credits by User this Month** report and dashboard chart give the details. Records older than
13 months are deleted automatically.
## Per-user limits
Set a **default monthly credit limit per user** in Tomba Setup, and override it for individual users in the
Usage card. When a user reaches the limit, record actions, searches, bulk runs and agent actions stop with a
message for that user. Automation is not counted — it has its own [monthly budget](/salesforce/automation).
## Low-credit alerts
Every hour Tomba checks your plan's usage. When it reaches the alert threshold (default 90%), or a lookup fails
because the plan has no credits left, users with the **Tomba Admin** permission set get a Salesforce notification
and an email asking them to contact the Tomba account team. You get at most one alert per level per billing period.
Turn on **Pause automation when credits run low** to stop background enrichment until the next period or until
you add credits; record actions keep working.
---
## Document: Single sign-on
Let your team sign in to app.tomba.io with their Salesforce login.
URL: /salesforce/sso
# Single sign-on
## Sign in to Tomba with Salesforce
Salesforce can be the SAML identity provider of your Tomba workspace, using Tomba's standard SSO setup
(Enterprise plan):
1. In Salesforce **Setup › Identity Provider**, click **Enable Identity Provider**.
2. Create a connected app for Tomba with SAML enabled, using the **Entity ID** and **ACS URL** shown on
[app.tomba.io › Settings › Workspace › SSO](https://app.tomba.io/settings/workspace/sso). Send the user's email
as the subject (Name ID format _emailAddress_), and give your Tomba users access to the app.
3. Back in **Setup › Identity Provider**, click **Download Metadata**.
4. On app.tomba.io › Settings › Workspace › SSO, click **Configure SAML SSO** and upload that file.
Test it from a private window with **Sign in with SSO**, then choose how members sign in (_SSO optional_ or
_SSO required_). Invite teammates and manage members on app.tomba.io as usual.
---
## Document: Tomba for Salesforce Solution Overview
Find, verify and enrich B2B contact data inside Salesforce: email finder, email verification, lead and account enrichment, buying committees and data-quality automation for Tomba Enterprise customers.
URL: /salesforce/solution-overview
# Tomba for Salesforce Solution Overview
**Tomba for Salesforce** brings Tomba's B2B email finder, email verifier and company data into Salesforce, so
sales and RevOps teams work with contact data that is complete and deliverable — without exports, spreadsheets
or switching tools.
## Who it's for
| Team | What they get |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| **Sales reps / SDRs** | Find and verify an email in one click on a Lead or Contact; add the people who decide on a deal |
| **Sales & RevOps** | Verify and enrich whole lists, keep records verified over time, and see data health in reports |
| **Salesforce admins** | Field mapping, automation and Flow actions, per-user limits, usage tracking, sandbox safety |
| **Marketing** | Fewer bounces: invalid and disposable addresses are flagged before campaigns go out |
## Key capabilities
**Find and verify.** Find a professional email from a name and company, verify deliverability (valid, accept-all,
unknown, invalid, disposable), find and validate phone numbers.
**Enrich.** Fill job title, department, seniority, LinkedIn and company data — industry, size, revenue, location,
technologies — into the Salesforce fields you choose, filling empty fields only or overwriting, per field.
**Work at any scale.** One record from the record page, a list view selection, every record matching a filter,
or automatically when records are created or their email changes.
**Keep data clean.** Re-verify old emails on a schedule, re-verify emails that bounce and suggest a replacement,
match Leads and Contacts to existing Accounts by company domain.
**Reach the buying committee.** From an Opportunity, find the decision makers at the account by department and
seniority, and add them as Contacts and contact roles.
**Automate.** Flow actions for any process, and Agentforce actions so AI agents can find emails, verify them and
describe companies.
**Stay in control.** Monthly automation budgets, per-user credit limits, low-credit alerts, a usage log by user and
source, reports and a Data Health dashboard.
## How it works
1. An admin installs the package from AppExchange and connects the company's Tomba Enterprise account (OAuth).
2. Users run Tomba from record pages, list views, the Tomba app tabs, Flow or Agentforce.
3. The package calls the Tomba API over HTTPS with credentials stored encrypted in a Salesforce external credential.
4. Results are written to Salesforce fields according to your field mapping, respecting object and field-level
security.
## Requirements
- Salesforce **Enterprise, Unlimited or Developer** edition, Lightning Experience.
- A **Tomba Enterprise** plan.
- A Tomba for Salesforce **company subscription** from AppExchange, starting at $59,988 USD per company per year — see [Pricing & Plans](/salesforce/pricing-and-licenses).
## Security
Credentials never leave Salesforce's encrypted external credential; only the inputs of the lookups you run are sent
to Tomba. See [Security and Data Protection](/salesforce/privacy-and-security).
## Get started
- [Install and connect](/salesforce/installation)
- [Talk to sales](https://app.tomba.io/upgrade?plan=enterprise&utm_source=docs&utm_campaign=salesforce_overview)
---
## Document: Tomba Search
Search Tomba from Salesforce — domain search, email finder, verifier and companies search — and import results as Leads or Accounts.
URL: /salesforce/search
# Tomba Search
The **Tomba Search** tab has four tools:
- **Domain search** — the people and emails Tomba knows at a company. Filter by department, country and email
type; load more pages as needed.
- **Email finder** — the email of one person from a name and a company domain.
- **Email verifier** — whether an email is deliverable.
- **Companies** — companies that match structured filters: countries, industries, keywords, technologies, size,
type, revenue and founding year. There is no free-text or AI query.
Select rows and click **Import as Leads** (people) or **Import as Accounts** (companies). Emails that already
exist as Leads, and domains that already exist as Accounts, are skipped, and the org's duplicate rules apply.
---
## Document: Sandboxes
Keep sandboxes from spending your production Tomba credits.
URL: /salesforce/sandboxes
# Sandboxes
In a sandbox, Tomba **automation is off** by default — creating or loading test records does not enrich them.
Record actions, searches and bulk runs still work when you run them. To test automation, turn on
**Allow automation in this sandbox** in Tomba Setup.
## When you create or refresh a sandbox
Choose **TombaSandboxPostCopy** as the _Apex Class_ in the sandbox options. After the copy it:
- disconnects Tomba (the sandbox must not use production's API key),
- stops the hourly Tomba job,
- keeps automation off.
Then connect the sandbox to a test tomba.io account in Tomba Setup.
---
## Document: Reports, dashboard and record panel
See email deliverability, enrichment activity, the automation backlog and credits used, on records and in reports.
URL: /salesforce/reports-and-dashboard
# Reports, dashboard and record panel
## Tomba Insights on record pages
Add the **Tomba Insights** component to Lead, Contact and Account Lightning record pages (Edit Page in the
Lightning App Builder). It shows the email status and score, when the record was last verified and enriched,
LinkedIn and other Tomba data, and offers one-click **Re-verify** and **Enrich**. One-click runs write fields
according to the [field mapping](/salesforce/field-mapping) without a preview; use the **Tomba** action to review
changes first.
## Reports
The **Tomba Reports** folder contains:
| Report | Shows |
| ----------------------------------------- | -------------------------------------------------------- |
| Leads by Tomba Email Status | Valid, accept-all, unknown, invalid and disposable Leads |
| Contacts by Tomba Email Status | The same for Contacts |
| Lead Email Status by Lead Source | Which sources bring deliverable emails |
| Leads Enriched by Tomba per Month | Enrichment activity |
| Tomba Queue and Errors (Leads) | Records waiting for automation or whose last run failed |
| Tomba Credits Used by Bulk Jobs per Month | Search and verification credits used by bulk jobs |
Clone a report into your own folder to change it; packaged reports are updated with the package.
## Tomba Data Health dashboard
The **Tomba Dashboards** folder has a dashboard with these reports. It shows each viewer the records they can
access.
## Running out of credits
Tomba for Salesforce runs on a Tomba Enterprise plan, whose credits are managed by your Tomba account team.
When the plan runs out of credits, Tomba pauses lookups and automation, and the package shows a
**Contact your account manager** link. Tomba Setup also warns you when you have used a set share of the month's
credits (see [Usage, limits and alerts](/salesforce/usage-and-limits)).
---
## Document: Record actions
Enrich, find, verify and find phones for one Lead, Contact or Account, and add the people Tomba knows at a company.
URL: /salesforce/record-actions
# Record actions
Click **Tomba** on a record and choose what Tomba should do:
| Record | Operation | Uses |
| ------------- | ------------------ | ------------------------------------------------------------------------------- |
| Lead, Contact | **Enrich** | The email to look up the person; without an email, finds it from name + company |
| Lead, Contact | **Find email** | First name, last name and the company website (Contact: the Account's website) |
| Lead, Contact | **Verify email** | The email |
| Lead, Contact | **Find phone** | The email |
| Lead, Contact | **Validate phone** | The Phone (or Mobile) number; sets Tomba Phone Status, Line Type and Carrier |
| Account | **Enrich company** | The website |
| Account | **Technologies** | The website; lists the technologies the company uses |
| Account | **Find people** | The website; pick people and add them as Contacts |
Tomba shows every field it can fill, the current value and Tomba's value. Fields that are empty, and the
Tomba fields, are preselected; fields you already filled are not, unless the
[field mapping](/salesforce/field-mapping) says to overwrite. Untick anything you don't want and click **Save**.
The record is stamped with **Tomba Enriched At** or **Tomba Verified At** and **Tomba Sync Status**, so bulk
runs can skip it later.
**Find people** never creates duplicates: people whose email already exists as a Contact are marked
**In Salesforce** and skipped on import.
---
## Document: Security and Data Protection
Security and data protection in Tomba for Salesforce: what is sent to Tomba, how credentials are stored, how Salesforce permissions are enforced, and how usage is audited.
URL: /salesforce/privacy-and-security
# Security and Data Protection
Tomba for Salesforce is a managed package that runs inside your Salesforce org and calls the Tomba API only for
the work you ask it to do.
## Data flow
1. A user, a bulk job, an automation rule, a Flow or an agent asks Tomba to look up a record.
2. The package sends **only the inputs of that lookup** — for example an email address, or a first name, last name
and company domain — to `https://api.tomba.io` over HTTPS.
3. Tomba answers with the result (email, verification status, company data…).
4. The package writes the result to the fields your [field mapping](/salesforce/field-mapping) allows.
Nothing is sent in the background unless an admin turns on automation, and automation is off in sandboxes unless
explicitly allowed ([Sandboxes](/salesforce/sandboxes)).
## What Tomba stores
Requests are recorded in your tomba.io account's request log (the **Salesforce** source on your usage page) like any
API call, under the tomba.io [privacy policy](https://tomba.io/privacy). The package does not export your Salesforce
records in bulk to Tomba.
## Credentials
- The connection is made with **OAuth** (PKCE) to your Tomba Enterprise account, or with an API key.
- The package creates a dedicated API key named after your org, so you can see and revoke it at
[app.tomba.io/api](https://app.tomba.io/api); it is renewed automatically before it expires.
- The key, secret and refresh token are stored in a Salesforce **external credential**, encrypted by Salesforce.
Users and Apex code cannot read them; they are only added to requests to `api.tomba.io`.
## Salesforce permissions
- Queries and updates run in **user mode**: object permissions, field-level security and sharing are enforced, so
users only see and change records they have access to.
- Fields are written only when the user may edit them and only according to the field mapping.
- Access is granted with the **Tomba User** and **Tomba Admin** permission sets; only Tomba Admins can connect,
change settings and edit field mappings.
## Control and audit
- **Usage log**: every lookup is recorded per day, user and source (record action, search, bulk, automation, Flow,
agent) — see [Usage, limits and alerts](/salesforce/usage-and-limits).
- **Limits**: monthly automation budget and per-user credit limits.
- **Bulk jobs** record who started them, what they processed and the credits used.
## Disconnecting
Disconnecting removes the stored credentials and revokes the OAuth grant. You can also revoke access at
[app.tomba.io › Connected apps](https://app.tomba.io/settings/connected-apps) or delete the `Salesforce – …` key.
## Salesforce Security Review
Tomba for Salesforce is distributed through AppExchange, which requires passing the Salesforce Security Review;
the review status is shown on the AppExchange listing. For security questionnaires, contact your Tomba account
team or [Tomba support](https://help.tomba.io).
---
## Document: Pricing & Plans
Tomba for Salesforce pricing: one company-wide subscription per Salesforce org, starting at $59,988 USD per company per year ($4,999/month billed annually), for Tomba Enterprise customers.
URL: /salesforce/pricing-and-licenses
# Pricing & Plans
Tomba for Salesforce has **one plan**, priced **per company, per year**.
| Plan | Price | Unit / billing | Includes |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | --------------------------------------------------------------- |
| **Tomba for Salesforce Enterprise** | **Starting at $59,988 USD** ($4,999/month, billed annually) — [talk to sales](https://app.tomba.io/upgrade?plan=enterprise&utm_source=docs&utm_campaign=salesforce_pricing) | Per company, yearly | Every Salesforce user in the org, all package features, support |
Lookups use the credits of your **Tomba Enterprise plan**, agreed with your Tomba account team — so the
subscription covers the app, and your Enterprise contract covers the data.
## Company-wide license
You buy one subscription for your Salesforce org on AppExchange. It covers **every user** — there are no seats to
count or assign. Choose who can use Tomba with the **Tomba User** and **Tomba Admin** permission sets, and limit
credit use per user in [Usage, limits and alerts](/salesforce/usage-and-limits).
**Tomba Setup** shows the license status and renewal date. If the subscription expires, users see
"You need a Tomba for Salesforce license" until it is renewed.
## Tomba Enterprise requirement
The tomba.io account you connect must be on a **Tomba Enterprise** plan. If it is not — or the plan ends — Tomba
refuses lookups from Salesforce and the package shows **Tomba for Salesforce requires a Tomba Enterprise plan**
with a link to talk to sales. Credits, renewals and plan changes are handled by your Tomba account manager.
## Evaluating
There is no free trial. Take the **Test Drive** on the AppExchange listing to try the app with sample data, or ask
Tomba sales for a guided demo.
## Nonprofits and education
Discounts are available for nonprofit and education organizations — ask Tomba sales.
## What's included
- Record actions, bulk runs, automation, Flow and Agentforce actions, search and import, buying committee,
account matching, reports and dashboard — see the [Solution Overview](/salesforce/solution-overview).
- Support from the Tomba team ([help.tomba.io](https://help.tomba.io)).
- New versions of the package as they are released.
---
## Document: Tomba for Salesforce
Find, verify and enrich Leads, Contacts and Accounts with Tomba without leaving Salesforce. Uses the credits of your tomba.io plan.
URL: /salesforce/introduction
# Tomba for Salesforce
Tomba for Salesforce is a managed package on the Salesforce AppExchange. It adds Tomba's email finder,
email verifier and enrichment to Leads, Contacts and Accounts: on a single record, on a list view selection,
on every record that matches a filter, or automatically when records are created.
Tomba for Salesforce is a **paid app for Tomba Enterprise customers**. You need:
- a **company subscription** for the package, bought on AppExchange (per company, per year — every user is
covered), and
- a **Tomba Enterprise plan** — lookups use the credits of that tomba.io account, exactly as if you called the
[Tomba API](/api) yourself.
Plans start at **$59,988 USD per company per year** ($4,999/month billed annually). See [Pricing & Plans](/salesforce/pricing-and-licenses) and the [Solution Overview](/salesforce/solution-overview).
## What you can do
| Where | What it does |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [Record action](/salesforce/record-actions) | Enrich, find or verify the email, find the phone of one record; review every field before saving |
| [List views and bulk runs](/salesforce/bulk-jobs) | Run Tomba on selected records, or on every record matching a filter, with live progress |
| [Automation](/salesforce/automation) | Enrich new records, verify changed emails, re-verify old ones, within a monthly credit budget |
| [Tomba Search](/salesforce/search) | Domain search, email finder, verifier and companies search, then import as Leads or Accounts |
| [Flow actions](/salesforce/flow-actions) | Call Tomba or queue records from your own flows |
| [Reports and record panel](/salesforce/reports-and-dashboard) | Email deliverability and enrichment on record pages, in reports and a dashboard |
| [Account matching](/salesforce/account-matching) | Link Leads and Contacts to existing Accounts by company domain (no credits) |
| [Buying committee](/salesforce/buying-committee) | Find decision makers at an opportunity's account and add them as contact roles |
| [Agentforce](/salesforce/agentforce) | Let AI agents find and verify emails and describe companies |
| [Usage and limits](/salesforce/usage-and-limits) | Usage by user and source, per-user limits, low-credit alerts |
| [Field mapping](/salesforce/field-mapping) | Choose which Tomba value goes to which field, and whether it may overwrite |
## How credits are counted
The same rules as the API: a credit is used only when Tomba finds a result, and repeating a lookup within
your billing month is free. Bulk jobs show the credits they used; the numbers match
[your usage page](https://app.tomba.io/settings/usage/requests), where Salesforce requests appear under the
**Salesforce** source.
Not on Enterprise yet? [Talk to
sales](https://app.tomba.io/upgrade?plan=enterprise&utm_source=docs&utm_campaign=salesforce)
about Tomba Enterprise, or take the Test Drive on AppExchange.
## Next steps
- [Install and connect](/salesforce/installation)
- [Privacy and security](/salesforce/privacy-and-security)
---
## Document: Install and connect
Install Tomba for Salesforce from AppExchange, assign permission sets and connect your tomba.io account.
URL: /salesforce/installation
# Install and connect
## 1. Install the package
Install **Tomba for Salesforce** from AppExchange into a sandbox or production org. Choose **Install for
Admins Only**; you will give access with permission sets in the next step.
## 2. Check your license
Tomba for Salesforce is licensed **per company, per year**: one subscription covers every user in the org, so there
is nothing to assign. **Tomba Setup** shows the license status and renewal date; you control who uses Tomba with
the permission sets below.
## 3. Assign permission sets
| Permission set | For |
| --------------- | --------------------------------------------------------------------------------- |
| **Tomba Admin** | Admins who connect Tomba, change settings and automation, and see every Tomba job |
| **Tomba User** | Everyone who runs record actions, searches and bulk jobs |
Users without either permission set can still work with Tomba fields, but cannot call Tomba. Records they
create are picked up by [automation](/salesforce/automation) when it is enabled.
## 4. Connect your Tomba Enterprise account
Open the **Tomba** app › **Tomba Setup** tab and connect the tomba.io account of your **Tomba Enterprise** plan
(the connection is refused otherwise):
- **Connect with Tomba** (recommended): sign in to tomba.io and approve access. The package creates a
dedicated API key named `Salesforce – `, so you can see and revoke it at
[app.tomba.io/api](https://app.tomba.io/api). The key is renewed automatically before it expires.
- **No Tomba account yet?** Click **No Tomba account? Create one with Salesforce**. You sign in with your
Salesforce login and Tomba creates an account with your Salesforce name and email (on the Free plan). Our team then
contacts you to set up Tomba Enterprise, and you get an email to set your password for app.tomba.io. Once Enterprise
is active, come back and click **Connect with Tomba**.
- **Use an API key**: paste an existing key and secret from [app.tomba.io/api](https://app.tomba.io/api).
Keys expire after at most a year; Setup warns you 30 days before, and you then reconnect.
Credentials are stored in a Salesforce external credential. Neither users nor Apex code can read them.
## 5. Add the actions to your layouts
- **Record action**: add the **Tomba** action to the Lead, Contact and Account Lightning record pages
(Page Layout › Mobile & Lightning Actions).
- **Tomba Insights**: drag the component onto the Lead, Contact and Account record pages in the Lightning App
Builder.
- **List view button**: add the **Tomba** button to the Lead, Contact and Account list view
(Search Layouts › List View).
## 6. Optional: automation
Turn on the options you want in **Tomba Setup › Automation**. Saving schedules an hourly job that runs as you;
see [Automation](/salesforce/automation).
---
## Document: Flow actions
Use Tomba in Salesforce Flow: run a lookup on a record, or queue records for background enrichment.
URL: /salesforce/flow-actions
# Flow actions
Tomba adds two actions to Flow Builder, in the **Tomba** category.
## Tomba: Run on Record
Calls Tomba right away and returns the result.
| Input | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Record ID | A Lead, Contact or Account |
| Operation | Leads/Contacts: `enrich`, `find`, `verify`, `phone`, `validate_phone`, `bounce`, `match`; Accounts: `enrich`, `technology`, `index_domains` |
| Save results to the record | Default on. Off returns the values without changing the record |
Outputs: **Status** (`Done`, `Not Found`, `Skipped`, `Error`), **Message**, **Fields updated**, **Email**,
**Email status**, **Email score**, **Phone**, **Position**, **LinkedIn**, **Company domain**, **Credits used**.
Because it calls Tomba, use it in screen flows, scheduled paths, the **Run Asynchronously** path of
record-triggered flows, or autolaunched flows. One flow interview handles up to about 90 records; use the
queue action or a [bulk run](/salesforce/bulk-jobs) for more.
Example: a screen flow on the Lead page that verifies the email and shows "Email is invalid" when **Email status**
is `invalid`.
## Tomba: Queue for Enrichment
Flags records **Queued** so Tomba processes them in the background (within minutes, or at the next hourly run).
It makes no callout, so it works anywhere, including the immediate path of record-triggered flows.
| Input | Description |
| --------- | ------------------------------------------------- |
| Record ID | A Lead, Contact or Account |
| Operation | `enrich` (default), or any operation listed above |
Example: when a Lead is converted, queue the new Contact for `verify`. Queued records count toward the
[automation budget](/salesforce/automation).
---
## Document: Field mapping
Choose which Tomba value is written to which Salesforce field, and whether it fills empty fields only or overwrites.
URL: /salesforce/field-mapping
# Field mapping
Edit field mapping in **Tomba Setup › Field mapping**: pick the object, change the target field, policy or
active flag of any mapping, or **Add mapping** to send another Tomba value to a field of your choice, then
**Save and deploy**. Changes are deployed as custom metadata in the background (a few seconds) and apply to the
next lookup. You need the Customize Application permission.
Behind the scenes this is the **Tomba Field Mapping** custom metadata type. Each record says: for this object,
write this Tomba value to this field, with this policy.
| Policy | Meaning |
| ----------- | ---------------------------------------------------------------- |
| `FillEmpty` | Only write when the field is blank (default for standard fields) |
| `Overwrite` | Always write (default for Tomba fields) |
| `Never` | Don't write |
You can change **Target field**, **Policy** and **Active** on the records that come with the package, and add
your own records, for example to send `seniority` to a custom field.
## Tomba values
| Value | Comes from |
| ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `email`, `email_status`, `email_score` | Email finder, enrichment, verifier |
| `first_name`, `last_name`, `position`, `department`, `seniority` | Email finder, enrichment, domain search |
| `linkedin`, `twitter`, `country_code`, `phone` | Email finder, enrichment, phone finder |
| `company_name`, `company_domain`, `company_industry`, `company_employees` | Enrichment, company enrichment |
| `company_description`, `company_revenue`, `company_founded`, `company_linkedin`, `company_city`, `company_country_code` | Company enrichment |
| `phone_status`, `phone_line_type`, `phone_carrier`, `phone_intl` | Phone validator |
| `company_technologies` | Technologies |
Values are converted to the field type. Values that don't fit (a range like `51-200` in a number field) are
skipped, and fields the user cannot edit are never written.
---
## Document: FAQ and troubleshooting
Answers to common questions about Tomba for Salesforce.
URL: /salesforce/faq
# FAQ and troubleshooting
**What does it cost?** One subscription per company, per year, starting at $59,988 USD ($4,999/month billed
annually), bought on AppExchange — it covers every user in your org. There is no free trial; use the Test Drive. The connected tomba.io account must be on Tomba Enterprise, whose credits pay for lookups. See
[Pricing & Plans](/salesforce/pricing-and-licenses).
**"Tomba for Salesforce requires a Tomba Enterprise plan"** — the connected tomba.io account is not Enterprise,
or the Enterprise contract has ended. Talk to Tomba sales or your account manager.
**"You need a Tomba for Salesforce license"** — the org's subscription has expired or isn't active yet. Check
Setup › Installed Packages › Tomba for Salesforce, and contact Tomba to renew.
**Why is the Tomba action missing on my records?** Add it to the Lightning record page or page layout, and make
sure you have the **Tomba User** permission set.
**"Tomba is not connected or the connection expired"** — an admin reconnects in **Tomba Setup**. This happens
when the key was deleted on tomba.io, the password was changed, or the OAuth grant was revoked.
**Records stay Queued.** Check that the hourly job is scheduled (Tomba Setup › Automation), that the monthly
budget isn't used up, and the last error shown in Setup. Click **Run hourly job as me** if the admin who scheduled
it left.
**Lookups are skipped.** Find email needs a first name, last name and a website (on the Lead, or the Contact's
Account). Verify and phone need an email. Account enrichment needs a website.
**Can I use one tomba.io account for several orgs?** Yes. Each org gets its own API key.
---
## Document: Keep Your Salesforce Contact Data Deliverable
A practical guide to email verification, re-verification, bounce handling, enrichment policies and automation budgets in Salesforce with Tomba.
URL: /salesforce/data-quality-guide
# Keep Your Salesforce Contact Data Deliverable
Contact data decays: people change jobs, companies change domains, and mailboxes close. This guide describes a
practical routine to keep Salesforce contact data deliverable with Tomba for Salesforce, and the settings that
implement each step.
## 1. Verify at the point of entry
Check every new email as it arrives, before anyone sends to it.
- Turn on **Verify emails when they are added or changed** ([Automation](/salesforce/automation)).
- Use the **Tomba Email Status** field in list views, validation rules or campaign filters: send to `valid`,
review `accept_all` and `unknown`, exclude `invalid` and `disposable`.
## 2. Fill the gaps without overwriting your data
Enrich new records, but decide per field what Tomba may change.
- Turn on **Enrich new Leads / Contacts / Accounts**.
- In the [field mapping](/salesforce/field-mapping), keep standard fields your users maintain on **Fill empty
fields**, and let Tomba **Overwrite** only the Tomba fields (email status, score, LinkedIn, department).
## 3. Re-verify on a schedule
An address that was valid a year ago may not be today.
- Set **Re-verify emails older than N days** to match how often you contact your database — for example 90 days
for active pipelines, longer for dormant records.
- Repeating a lookup within your billing month uses no extra credits.
## 4. Act on bounces
When Salesforce bounce management marks an email as bounced, Tomba re-verifies it and, when it is bad, looks for
a replacement address.
- Turn on **Re-verify emails that bounce**.
- Review suggestions in the **Bounced Emails with a Tomba Suggestion** report or on the record's Tomba Insights
panel, and click **Use this email** to apply one. Tomba never replaces an email on its own.
## 5. Connect people to the right companies
Duplicate and orphan records hide your real coverage of an account.
- Run **Index company domains** once on Accounts, then **Match to Accounts** on Leads and Contacts
([Account matching](/salesforce/account-matching)).
- Turn on matching for new records and, if you want, linking Contacts that have no Account.
## 6. Keep spending predictable
- Set a **monthly automation budget** so background enrichment never surprises you.
- Set **per-user limits** and watch the **Usage** card and the **Tomba Credits by User this Month** report.
- Choose a **low-credit alert** threshold so admins hear about it before credits run out.
## 7. Measure
The **Tomba Data Health** dashboard shows email status by object and lead source, enrichment activity, the
automation backlog and credits used. Review it monthly with your RevOps team.
---
## Document: Buying committee
Find the decision makers at an opportunity's account and add them as contacts and contact roles.
URL: /salesforce/buying-committee
# Buying committee
On an **Opportunity** (or an Account), the **Tomba Buying Committee** action lists the people Tomba knows at
the company:
1. Choose departments (executive, finance, IT…) and seniority (executive, senior, junior), then **Search**.
2. Tomba suggests a contact role for each person — executives become **Decision Maker** (finance executives
**Economic Buyer**), IT and engineering **Technical Buyer**, senior people **Evaluator**, others
**Influencer**. Change any role before adding.
3. **Add to opportunity** creates Contacts for new people under the account and adds everyone selected as
Opportunity Contact Roles. People who are already Contacts are reused; existing roles are kept.
A search uses Tomba search credits like a domain search. Add the action to the Opportunity and Account page
layouts (Mobile & Lightning Actions).
---
## Document: List views and bulk runs
Run Tomba on many records: from a list view selection or from a filter, with live progress, credit totals and automatic resume.
URL: /salesforce/bulk-jobs
# List views and bulk runs
## From a list view
Select records in a Lead, Contact or Account list view and click **Tomba**. Choose the operation and whether
to skip records Tomba already processed, then **Start**.
## From a filter
The **Tomba Bulk** tab runs Tomba on every record that matches conditions you build (field, operator, value;
up to 10 conditions). **Count records** shows how many match and the most credits the run can use.
## Progress and results
Every run is a **Tomba Job** with live progress, records found, errors, and the search and verification
credits used. The **Tomba Jobs** tab lists recent jobs and lets you cancel or resume them.
| Stop reason | What happened | What to do |
| --------------------- | ----------------------------------------------------- | ----------------------------------------------------------- |
| `rate_limited` | Your plan's rate limit was hit | Nothing — resumes automatically |
| `network`, `timeout` | A temporary problem | Nothing — resumes automatically |
| `out_of_credits` | Your plan has no credits left | Ask your Tomba account manager for credits, then **Resume** |
| `enterprise_required` | The tomba.io account is not (or no longer) Enterprise | Talk to Tomba sales, then **Resume** |
| `auth` | The Tomba connection was revoked or expired | Reconnect in **Tomba Setup**, then **Resume** |
Resuming never repeats records that were already processed. A job processes at most 50,000 records
(adjustable in Setup).
---
## Document: Automation
Enrich new records, verify changed emails and re-verify old ones automatically, within a monthly credit budget.
URL: /salesforce/automation
# Automation
Turn these on in **Tomba Setup › Automation**:
| Option | When it runs |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Enrich new Leads / Contacts | A Lead or Contact is created |
| Enrich new Accounts | An Account is created, or its website changes |
| Verify emails when added or changed | A Lead or Contact gets a new email |
| Find emails for records without one | A Lead or Contact is created without an email |
| Re-verify emails older than N days | Hourly, for emails last verified more than N days ago |
| Re-verify emails that bounce | Salesforce marks the email as bounced (needs bounce management). If the email is bad, Tomba looks for a replacement and stores it in **Tomba Suggested Email** — the Tomba Insights panel offers **Use this email** |
Records are marked **Tomba Sync Status = Queued** when they are saved and processed in the background a few
minutes later. Records created by users without Tomba access (web-to-lead, integrations) are processed by the
hourly job, which runs as the admin who saved the settings.
**Monthly credit budget** caps the credits automation may use per month. When it is reached, records stay
Queued until the next month or until you raise the budget. Record actions and bulk jobs don't count toward it.
To keep a data load out of automation, give the loading user the **Tomba: Bypass Automation** custom permission.
In a sandbox, automation is off unless you allow it — see [Sandboxes](/salesforce/sandboxes). Automation also
pauses after a low-credit alert when you choose so — see [Usage, limits and alerts](/salesforce/usage-and-limits).
---
## Document: Agentforce
Let Salesforce AI agents find emails, verify them and describe companies with Tomba.
URL: /salesforce/agentforce
# Agentforce
Tomba adds three actions that Agentforce agents can use, plus the record-based [Flow actions](/salesforce/flow-actions):
| Action | The agent can answer |
| ------------------- | ----------------------------------------------------- |
| Tomba: Find Email | "What's the email of Patrick Collison at stripe.com?" |
| Tomba: Verify Email | "Is jane@acme.com still valid?" |
| Tomba: Company Info | "What does acme.io do and how big are they?" |
## Add them to an agent
1. In **Setup › Agentforce Assets › Actions**, create a new action from each **Apex** invocable named
_Tomba: …_ (or deploy the ready-made topic from the package's `agentforce/` folder).
2. In **Agent Builder**, add the **Tomba Contact Data** topic (or the actions) to your agent and activate it.
Agent users need the **Tomba User** permission set. Every lookup uses your tomba.io credits, counts toward the
user's [monthly limit](/salesforce/usage-and-limits) and appears under the **Agent** source in usage.
## Remote MCP server (all Tomba tools)
Agentforce can also use the [Tomba remote MCP server](/llm/remote-mcp/introduction), which gives agents every
Tomba tool — domain search, email finder and verifier, enrichment, phone finder and more — without building an
action per tool.
MCP servers in Agentforce (the Agentforce Registry) are a Salesforce beta;
menu names can change.
1. In **Setup › Agentforce Registry**, add a new MCP server with the URL `https://mcp.tomba.io/mcp`.
2. Choose **OAuth 2.0** authentication. Salesforce reads Tomba's authorization server from the MCP server, then
asks you to sign in to Tomba and approve access — the same [MCP login](/llm/remote-mcp/introduction#oauth-20)
as any other MCP client.
3. **Allow** the tools your agents need (for example `email_finder`, `email_verifier`, `domain_search`), then add
them to a topic of your agent in **Agent Builder** and activate it.
Tool calls use the credits of the Tomba account that signed in.
---
## Document: Account matching
Match Leads and Contacts to existing Accounts by company domain — no Tomba credits used.
URL: /salesforce/account-matching
# Account matching
Tomba for Salesforce links people to the companies you already have, using the company **domain**. It runs
inside Salesforce and uses **no Tomba credits**.
## How it works
- Every Account gets a **Tomba Domain** (`stripe.com`) from its Website, kept up to date automatically. For
existing Accounts, run the **Index company domains** bulk job once (Tomba Bulk › Accounts).
- A Lead's domain comes from its Website, otherwise from its email. Webmail addresses (gmail.com, outlook.com…)
are ignored.
- The Lead or Contact gets **Tomba Matched Account**. When several Accounts share the domain, the oldest one is
chosen and **Tomba Match Ambiguous** is checked.
## Turn it on
In **Tomba Setup › Automation › Account matching**:
| Option | Effect |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| Match Leads and Contacts to Accounts by company domain | New and changed records are matched when saved, and records enriched by Tomba are matched afterwards |
| Link Contacts without an Account | Sets the Contact's Account to the match (otherwise only Tomba Matched Account is filled) |
To match existing records, run **Match to Accounts** from the Tomba Bulk tab or a list view. The **Tomba Insights**
panel shows the match and offers **Link to account** on Contacts without an Account.
---
## Document: Agent payments quickstart
Make a first paid request to agents.tomba.io with the InFlow CLI, choose the payment currency, and pay a challenge without the CLI.
URL: /mpp/quickstart
# Agent payments quickstart
Pay for one Tomba request with the InFlow CLI, then see what the payment exchange looks like on the wire. Prices and billing rules are on [Tomba for AI agents](/mpp/introduction#pricing).
## Before you begin
You need an [InFlow](https://inflowpay.ai) account in the production environment with a USDC or USDT balance. You don't need a Tomba account or API key.
## Pay for a request with the InFlow CLI
1. Install the InFlow CLI. On macOS and Linux:
```bash
curl -fsSL https://inflowcli.ai/install.sh | bash
```
On Windows, in PowerShell:
```powershell
irm https://inflowcli.ai/install.ps1 | iex
```
Both commands download and run an installer script. To read the script before you run it, open the URL in a browser.
2. Sign in to InFlow. The client name appears on the approval page that opens in your browser.
```bash
inflow auth login --client-name "my-agent"
```
3. Pay for a request:
```bash
inflow mpp pay "https://agents.tomba.io/domain-search?domain=stripe.com" --interval 5 --max-attempts 180
```
The CLI reads the `402` challenge, creates the payment, waits until InFlow reports it ready, sends the request again with the credential, and prints the result. Tomba's response is in `body`, and `outcome` is `paid` when the payment settled. If InFlow asks you to approve the payment, approve it while the command waits.
Without `--interval`, the command returns after creating the payment, with a `transaction_id` and the `inflow mpp fetch` command that completes the request.
Other endpoints work the same way. Use the query parameters of the matching [Tomba API endpoint](/mpp/introduction#endpoints):
```bash
inflow mpp pay "https://agents.tomba.io/email-finder?domain=stripe.com&first_name=Jane&last_name=Doe" --interval 5 --max-attempts 180
inflow mpp pay "https://agents.tomba.io/email-verifier?email=jane.doe@stripe.com" --interval 5 --max-attempts 180
inflow mpp pay "https://agents.tomba.io/technology?domain=stripe.com" --interval 5 --max-attempts 180
```
## Approve payments automatically
By default InFlow holds every MPP charge for manual approval. The payment sits in
[Approvals](https://app.inflowpay.ai/approvals/) as **Pending approval** until you click
the green tick, which is why the commands above pass `--interval 5 --max-attempts 180` —
the CLI is polling for up to fifteen minutes while it waits for you. An unattended agent
just stalls.
A policy removes the wait. Create one at
[app.inflowpay.ai/policies/add](https://app.inflowpay.ai/policies/add/):
| Field | Value |
| ----------------- | ---------------------------------------------------------------------- |
| **Policy Type** | `MPP` |
| **Policy Action** | `Approve Requests` |
| **Currency** | The currency you pay in — `USDC` or `USDT` |
| **Budget** | Total the policy may approve in one period, for example `10` |
| **Threshold** | Optional cap on a single payment. Must not exceed the budget |
| **Period** | `Once`, `Daily`, `Monthly` or `Yearly` |
| **Reserved** | Sets the budget aside from your available balance. `Once` periods only |
| **Seller** | `All Sellers`, or `A Specific Seller` to limit the policy to Tomba |
| **Expires** | Optional. The policy stops matching new requests after this time |
Tomba's seller id, if you scope the policy to this service alone:
```text
4e0db086-a9c2-4291-a278-acb98f994572
```
Once it is active, charges are approved as they arrive and `inflow mpp pay` returns
without polling.
A policy matches on currency. One written for `USDT` does not approve a `USDC`
challenge, and the payment waits for manual approval again — see
[Choose a currency](#choose-a-currency).
A `Once` period never resets, so the policy stops approving when its budget is spent
and payments silently queue again. Check **Spent** against **Budget** on the policy if
an agent starts waiting. Use `Daily` or `Monthly` for a standing allowance.
### Create the policy from the API
The dashboard form maps onto `POST /v1/policies`. Only `type` and `action` are required:
```bash
curl -X POST https://api.inflowpay.ai/v1/policies \
-H "X-API-Key: $INFLOW_PRIVATE_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "MPP",
"action": "APPROVE",
"currency": "USDT",
"budget": 10,
"threshold": 0.05,
"period": "MONTHLY",
"sellerId": "4e0db086-a9c2-4291-a278-acb98f994572"
}'
```
It answers `200` with the created policy:
```json
{
"policyId": "9aaaf91e-0ae4-4b01-8970-aa29e0d2e6ad",
"type": "MPP",
"action": "APPROVE",
"status": "ACTIVE",
"currency": "USDT",
"budget": "0.01",
"threshold": "0.005",
"period": "ONCE",
"requesterId": "4e0db086-a9c2-4291-a278-acb98f994572",
"created": "2026-10-06T10:54:01.903Z",
"updated": "2026-10-06T10:54:01.903Z"
}
```
`action` is one of `APPROVE`, `DECLINE` or `REVIEW`; `period` one of `ONCE`, `DAILY`,
`MONTHLY` or `YEARLY`; `currency` one of `USDC`, `USDT` or `PYUSD`. Omit `sellerId` to
apply the policy to every seller.
`budget`, `threshold`, `currency` and `period` only take effect when `action` is
`APPROVE`. Send them with `REVIEW` or `DECLINE` and they are **dropped without an
error** — the request still returns `200`, and the policy it creates has no spending
limit at all. Read the policy back after creating it and check the fields you sent
are present.
The seller you send as `sellerId` is returned as `requesterId`. A policy with no
`requesterId` applies to every seller.
List what you have with `GET /v1/policies`, read one with `GET /v1/policies/{policyId}`,
and remove one with `DELETE /v1/policies/{policyId}/delete`.
## Choose a currency
Every endpoint offers one challenge in USDC and one in USDT. Check what the endpoint offers and what your account can pay with:
```bash
inflow mpp inspect "https://agents.tomba.io/domain-search?domain=stripe.com"
inflow mpp supported
inflow balances list
```
Then pay in the currency you hold:
```bash
inflow mpp pay "https://agents.tomba.io/domain-search?domain=stripe.com" --currency USDT --interval 5 --max-attempts 180
```
Without `--currency`, the CLI doesn't check which currency your balance
holds. If it takes the challenge in the other currency, the payment fails
for lack of funds.
## What the exchange looks like
Call an endpoint without a credential to see the challenges:
```bash
curl -i "https://agents.tomba.io/domain-search?domain=stripe.com"
```
The response has status `402` and one `WWW-Authenticate` header that lists a `Payment` challenge per currency, separated by commas (values shortened here):
```http
HTTP/2 402
content-type: application/problem+json
cache-control: no-store
www-authenticate: Payment id="uVqFx76…", realm="agents.tomba.io", method="inflow", intent="charge", request="eyJhbW91bnQiOiIwLjAwMzEyIiwiY3VycmVuY3kiOiJVU0RDIiwi…", expires="2026-09-29T17:08:48.175Z", opaque="eyJfbXBweF9zY29wZSI6…", Payment id="jK_Djgg…", realm="agents.tomba.io", method="inflow", intent="charge", request="eyJhbW91bnQiOiIwLjAwMzEyIiwiY3VycmVuY3kiOiJVU0RUIiwi…", expires="2026-09-29T17:08:48.569Z", opaque="eyJfbXBweF9zY29wZSI6…"
```
- `request` is base64url-encoded JSON with the terms, for example `{"amount":"0.00312","currency":"USDC","methodDetails":{"rail":"balance"},"recipient":"…"}`.
- `opaque` binds the challenge to the endpoint you called.
- `expires` is the time after which the challenge, and any credential issued for it, is no longer accepted.
The body repeats the price and the CLI commands, and adds `agent_instructions` and `agent_memory` fields written for LLM agents:
```json
{
"error": {
"code": "payment_required",
"message": "Payment required. Use the InFlow CLI to pay and retry."
},
"payment": {
"amount": "0.00312",
"currency": "USDC",
"accepted_currencies": ["USDC", "USDT"],
"display": "$3.12 per 1,000 requests",
"supported_rails": ["mpp"]
},
"how_to_pay": {
"inflow": "inflow mpp pay https://agents.tomba.io/domain-search?domain=stripe.com",
"inspect": "inflow mpp inspect https://agents.tomba.io/domain-search?domain=stripe.com"
}
}
```
A paid response has Tomba's status and body, plus these headers:
| Header | Value |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| `Payment-Receipt` | Proof of payment, as base64url-encoded JSON. Only on charged responses. |
| `X-Payment-Charged` | `true` if the request was charged, `false` if it wasn't. See [Billing](/mpp/introduction#billing). |
| Rate limit headers | Forwarded from the Tomba API; see [Rate limits](/mpp/introduction#rate-limits). |
| `X-Request-ID` | Correlation id for this response. Quote it in a support request. Echoes an inbound one. |
## Pay without the CLI
To pay from your own code, make the same calls the CLI makes. Authenticate to InFlow with your account's API key in the `X-API-KEY` header.
1. Send the request and read the `WWW-Authenticate` header of the `402` response. Split it into its `Payment` challenges, and choose the one whose decoded `request` has the currency you hold.
2. Create a transaction for that challenge. Send the challenge's parameters as a JSON object, with every value unchanged:
```bash
curl -X POST "https://api.inflowpay.ai/v1/transactions/mpp" \
-H "X-API-KEY: $INFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"challenge": {
"id": "",
"realm": "agents.tomba.io",
"method": "inflow",
"intent": "charge",
"request": "",
"expires": "",
"opaque": ""
},
"options": {}
}'
```
3. The response has a `state`. While it's `pending`, wait `retryAfterSeconds` seconds and poll the transaction until the state is `ready`. A `failed` or `expired` state ends the attempt.
```bash
curl "https://api.inflowpay.ai/v1/transactions//mpp" \
-H "X-API-KEY: $INFLOW_API_KEY"
```
4. Send the original request again with the `credential` from the ready transaction:
```bash
curl -i "https://agents.tomba.io/domain-search?domain=stripe.com" \
-H "Authorization: Payment "
```
[Use with AI agents](/mpp/ai-agents) has this flow as a [Python helper](/mpp/ai-agents#python-helper), and a JavaScript version built on InFlow's buyer SDK.
---
## Document: Tomba for AI agents
Pay-per-request access to Tomba data at agents.tomba.io: no Tomba account or API key, each request paid through InFlow with the Machine Payments Protocol.
URL: /mpp/introduction
# Tomba for AI agents
`https://agents.tomba.io` serves Tomba's finder, verifier, and enrichment endpoints to AI agents that have no Tomba account. Instead of sending an API key, the agent pays for each request through [InFlow](https://inflowpay.ai) with the Machine Payments Protocol (MPP). If you have a Tomba account, call the [Tomba API](/introduction) with your key and secret instead.
## How it works
1. The agent calls an endpoint, for example `GET https://agents.tomba.io/email-verifier?email=jane.doe@stripe.com`.
2. The service answers `402 Payment Required` with a `WWW-Authenticate` header that holds one `Payment` challenge for each accepted currency.
3. The agent pays one challenge through InFlow and receives a payment credential.
4. The agent sends the same request again with the header `Authorization: Payment `.
5. The service checks the credential and calls the Tomba API. It settles the payment only if Tomba returned usable data, then returns Tomba's response with a `Payment-Receipt` header.
The [InFlow CLI](https://inflowcli.ai) runs steps 2 to 4 for you; see the [Quickstart](/mpp/quickstart). To build them into an agent, see [Use with AI agents](/mpp/ai-agents).
## Endpoints
Every endpoint uses `GET`, takes the query parameters of the Tomba API endpoint listed next to it, and returns that endpoint's response body.
| Endpoint | Tomba API endpoint |
| --------------------- | -------------------------------------------------------------------- |
| `/domain-search` | [Domain search](/api/finder#domain-search) |
| `/email-finder` | [Email finder](/api/finder#email-finder) |
| `/email-verifier` | [Email verifier](/api/verifier#email-verifier) |
| `/email-count` | [Email count](/api/finder#email-count) |
| `/email-format` | [Email format](/api/finder#email-format) |
| `/email-sources` | [Email sources](/api/email#email-sources) |
| `/author-finder` | [Author finder](/api/finder#author-finder) |
| `/linkedin` | [LinkedIn finder](/api/finder#linkedin-finder) |
| `/enrich` | [Email enrichment](/api/finder#email-enrichment) |
| `/people/find` | [Person enrichment](/api/enrichment#person-api) |
| `/companies/find` | [Company enrichment](/api/enrichment#company-api) |
| `/combined/find` | [Combined enrichment](/api/enrichment#combined-api) |
| `/phone-finder` | [Phone finder](/api/phone#phone-finder) |
| `/phone-validator` | [Phone validator](/api/phone#phone-validator) |
| `/similar` | [Similar domains](/api/domain#similar) |
| `/technology` | [Technology](/api/domain#technology) |
| `/location` | [Location](/api/finder#location) |
| `/domain-status` | [Domain status](/api/domain#domain-status) |
| `/domain-suggestions` | [Domain suggestions](/api/domain-suggestions#get-domain-suggestions) |
### Discovery documents
These paths are free and need no payment.
| Path | Contents |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `/.well-known/mpp.json` | Price, accepted currencies, endpoints, and InFlow CLI commands |
| `/openapi.json` | OpenAPI 3.1 description of the endpoints |
| `/llms.txt` | Instructions for LLM agents |
| `/skill.md` | Skill file for agent runtimes |
| `/.well-known/odp`, `/odp/offerings`, `/odp/collections` | Offering Discovery Protocol catalog, with each endpoint as a priced offering |
| `/.well-known/aep` | Agent Enrollment Protocol document. Enrolling is optional and changes neither access nor price. |
| `/.well-known/agent-card.json` | Agent card |
| `/health` | Service status. Add `?deep=1` to check that the service can issue payment challenges. |
### Browse the catalog
The ODP catalog lists every paid endpoint as an Offering, grouped into Collections, each
priced per request. Responses use the `application/odp+json` media type.
| Operation | Path |
| ------------------------- | --------------------------------- |
| Service document | `/.well-known/odp` |
| List offerings | `/odp/offerings` |
| Get one offering | `/odp/offerings/{id}` |
| List collections | `/odp/collections` |
| Get one collection | `/odp/collections/{id}` |
| Offerings in a collection | `/odp/collections/{id}/offerings` |
Add `?representation=full` to a listing for complete resources, or `terse` for identity
and price only. Listings default to terse and a single resource defaults to full; any
other value is rejected with a `400` problem body.
`?limit=` takes 1 to 100 and defaults to 50 — anything outside that, or not a number, is
a `400`. When more results remain, the page carries a `next` reference:
```json
{
"odp_version": "1.0",
"items": [{ "id": "domain-search", "name": "Find all email addresses…" }],
"next": "/odp/offerings?cursor=eyJleHBpcmVzQXQiOjE3OTEy…&representation=terse&limit=1"
}
```
Follow `next` exactly as given — it is an origin-relative path that already carries your
`limit` and `representation`, so don't rebuild it from the cursor. The last page omits
`next` rather than sending `null`. Cursors are signed and stay valid for one hour.
Tomba is also listed in the [InFlow directory](https://directory.inflowpay.ai/services/fa6404b2-3560-4d48-93a4-07bce54279d4/view/).
## Pricing
Each request costs 0.00312 USDC or 0.00312 USDT — $3.12 per 1,000 requests — paid from your InFlow balance. Every `402` response offers one challenge per currency, and you pay one of them. The InFlow CLI and the InFlow buyer SDK don't pick the currency you hold on their own; see [Choose a currency](/mpp/quickstart#choose-a-currency).
The service settles payments in InFlow's production environment, so pay with a production InFlow account.
InFlow holds each charge for manual approval unless you add a policy that approves them. An unattended agent will wait, so set one up before you automate: [Approve payments automatically](/mpp/quickstart#approve-payments-automatically).
## Billing
You're charged only when Tomba returns usable data. Once the service has called Tomba for a paid request, the response carries `X-Payment-Charged: true` or `X-Payment-Charged: false`.
| Outcome | Charged | Status you receive |
| --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------ |
| Tomba returns data | Yes, with a `Payment-Receipt` header | Tomba's status |
| Tomba returns a negative answer, such as an `undeliverable` verification or a domain search with no email addresses | Yes, with a `Payment-Receipt` header | Tomba's status |
| Tomba returns an error status, such as `422` for invalid parameters or `429` | No | Tomba's status |
| Tomba returns a success status with no result — an empty or `null` `data`, such as `/phone-finder` with no number on record | No | Tomba's status |
| Tomba returns a success status with an `errors` object | No | Tomba's status |
| Tomba returns a body that isn't JSON | No | `502` |
When a response isn't charged, the credential stays unspent: correct the request and send it again with the same credential before the challenge's `expires` time.
- A credential pays for one request to the endpoint it was issued for. It can't be spent on another endpoint, or again after it settles.
- Every paid request is charged, including a repeat of an earlier request.
## Errors
| Status | Meaning |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `402` | The request has no credential, or the credential was rejected because it expired, was already spent, or was issued for another endpoint. The response carries new challenges, and for a rejected credential the body says why. |
| `404` | Unknown endpoint. The body lists the available endpoints. |
| `502` | InFlow couldn't be reached, or Tomba returned no usable data (with `X-Payment-Charged: false`). In both cases your credential wasn't spent; retry with it. |
| `503` | The payment service is unavailable. Retry after the number of seconds in `Retry-After`. |
Other statuses, such as `400`, `422`, and `429`, come from the Tomba API; see [Status codes](/error-handling#status-codes).
## Rate limits
There is no rate limit. Send as many paid requests as you like, as fast as you like —
your spend is the only cap, and you control that with your InFlow balance and
[policy](/mpp/quickstart#approve-payments-automatically).
Responses still carry [rate limit headers](/rate-limits#rate-limit-headers), listed in
`Access-Control-Expose-Headers` so a browser client can read them. Should a `429` ever
come back, it carries `Retry-After`, isn't charged, and leaves your credential unspent —
wait and retry with the same one.
---
## Document: Use with AI agents
Give an AI agent Tomba tools that pay for each request through InFlow: helpers for JavaScript and Python, with Vercel AI SDK, LangChain, and CrewAI examples.
URL: /mpp/ai-agents
# Use with AI agents
The examples on this page give an agent Tomba tools that call [agents.tomba.io](/mpp/introduction) and pay for each request from an InFlow balance. No Tomba account or API key is involved. Each example has two parts: a helper that makes a paid request, and tools for your agent framework that call the helper.
## Before you begin
- An [InFlow](https://inflowpay.ai) account in the production environment, with a USDC or USDT balance and an API key. Put the key in the `INFLOW_API_KEY` environment variable.
- An API key for your model provider: `ANTHROPIC_API_KEY` for the Vercel AI SDK and LangChain examples, and `OPENAI_API_KEY` for CrewAI's default model.
The examples were checked against `@inflowpayai/mpp-buyer` 0.7.3, `mppx` 0.8.19, `ai` 7.0.122, `@ai-sdk/anthropic` 4.0.68, `httpx` 0.28.1, `langchain` 1.4.3, `langchain-anthropic` 1.7.5, and `crewai` 1.15.23.
## JavaScript
The JavaScript helper uses InFlow's buyer SDK, which needs Node.js 22 or later. Node.js 22.18 and later run the TypeScript files below directly; put them in a project whose `package.json` sets `"type": "module"`.
```bash
npm install @inflowpayai/mpp-buyer@0.7 mppx@0.8
```
### Payment-aware fetch
`mppx.fetch` sends the request, pays the `402` challenge through InFlow, and sends the request again with the credential. Save this as `tomba.ts`:
```typescript
import { Mppx, inflow } from "@inflowpayai/mpp-buyer";
const apiKey = process.env.INFLOW_API_KEY;
if (!apiKey) throw new Error("Set INFLOW_API_KEY");
const mppx = Mppx.create({
// Pay only through mppx.fetch; leave globalThis.fetch unchanged.
polyfill: false,
methods: [inflow({ apiKey, environment: "production" })],
});
// Calls an agents.tomba.io endpoint and pays for it when it answers 402.
export async function tomba(
path: string,
params: Record,
): Promise {
const url = new URL(path, "https://agents.tomba.io");
url.search = new URLSearchParams(params).toString();
const response = await mppx.fetch(url);
const body: unknown = await response.json();
if (!response.ok) {
const charged = response.headers.get("X-Payment-Charged");
throw new Error(
`Tomba returned ${response.status} (charged: ${charged}): ${JSON.stringify(body)}`,
);
}
return body;
}
```
A payment that InFlow rejects makes `mppx.fetch` throw. To record what you paid, read the receipt of a successful response with `Receipt.fromResponse(response)`, which `@inflowpayai/mpp-buyer` exports.
### Vercel AI SDK
```bash
npm install ai@7 @ai-sdk/anthropic@4 zod@4
```
Save this as `agent.ts` next to `tomba.ts`, and run it with `node agent.ts`:
```typescript
import { anthropic } from "@ai-sdk/anthropic";
import { generateText, stepCountIs, tool } from "ai";
import { z } from "zod";
import { tomba } from "./tomba.ts";
const result = await generateText({
model: anthropic("claude-opus-5-5"),
tools: {
domain_search: tool({
description: "Find email addresses at a company from its domain.",
inputSchema: z.object({
domain: z
.string()
.describe("Company domain, for example stripe.com"),
}),
execute: ({ domain }) => tomba("/domain-search", { domain }),
}),
email_verifier: tool({
description: "Check whether an email address is deliverable.",
inputSchema: z.object({ email: z.string().email() }),
execute: ({ email }) => tomba("/email-verifier", { email }),
}),
},
stopWhen: stepCountIs(5),
prompt: "Find an engineering contact at stripe.com and verify the address.",
});
console.log(result.text);
```
## Python
The Python examples need Python 3.10 or later.
### Python helper
The helper makes the calls described in [Pay without the CLI](/mpp/quickstart#pay-without-the-cli). It pays in the currency set in `TOMBA_MPP_CURRENCY` (`USDC` if unset).
```bash
pip install "httpx==0.28.*"
```
Save this as `tomba_mpp.py`:
```python
"""Call agents.tomba.io and pay for each request through InFlow."""
import base64
import json
import os
import re
import time
import httpx
AGENTS_URL = "https://agents.tomba.io"
INFLOW_URL = "https://api.inflowpay.ai"
INFLOW_API_KEY = os.environ["INFLOW_API_KEY"]
CURRENCY = os.environ.get("TOMBA_MPP_CURRENCY", "USDC")
def _payment_challenges(header):
"""Parse the Payment challenges in a WWW-Authenticate value."""
challenges = []
for part in re.split(r",\s*(?=Payment\s)", header):
scheme, _, params = part.strip().partition(" ")
if scheme == "Payment":
pairs = re.findall(r'(\w+)="((?:[^"\\]|\\.)*)"', params)
challenges.append({k: re.sub(r"\\(.)", r"\1", v) for k, v in pairs})
return challenges
def _currency(challenge):
encoded = challenge["request"]
padded = encoded + "=" * (-len(encoded) % 4)
return json.loads(base64.urlsafe_b64decode(padded))["currency"]
def _pay(challenge):
"""Fulfil a challenge with InFlow and return the payment credential."""
headers = {"X-API-KEY": INFLOW_API_KEY}
response = httpx.post(
f"{INFLOW_URL}/v1/transactions/mpp",
headers=headers,
json={"challenge": challenge, "options": {}},
timeout=30,
)
response.raise_for_status()
transaction = response.json()
while transaction["state"] == "pending":
time.sleep(transaction.get("retryAfterSeconds", 5))
response = httpx.get(
f"{INFLOW_URL}/v1/transactions/{transaction['transactionId']}/mpp",
headers=headers,
timeout=30,
)
response.raise_for_status()
transaction = response.json()
if transaction["state"] != "ready":
raise RuntimeError(f"InFlow payment {transaction['state']}")
return transaction["credential"]
def paid_get(path, params):
"""GET an agents.tomba.io endpoint, paying when it answers 402."""
url = f"{AGENTS_URL}{path}"
response = httpx.get(url, params=params, timeout=60)
if response.status_code == 402:
offers = _payment_challenges(response.headers["WWW-Authenticate"])
challenge = next(c for c in offers if _currency(c) == CURRENCY)
credential = _pay(challenge)
response = httpx.get(
url,
params=params,
headers={"Authorization": f"Payment {credential}"},
timeout=60,
)
if response.is_error:
charged = response.headers.get("X-Payment-Charged")
raise RuntimeError(
f"Tomba returned {response.status_code} (charged: {charged}): {response.text}"
)
return response.json()
```
### LangChain
```bash
pip install "langchain==1.4.*" "langchain-anthropic==1.7.*"
```
```python
from langchain.agents import create_agent
from langchain.tools import tool
from tomba_mpp import paid_get
@tool
def domain_search(domain: str) -> dict:
"""Find email addresses at a company from its domain, for example stripe.com."""
return paid_get("/domain-search", {"domain": domain})
@tool
def email_verifier(email: str) -> dict:
"""Check whether an email address is deliverable."""
return paid_get("/email-verifier", {"email": email})
agent = create_agent("anthropic:claude-opus-5-5", tools=[domain_search, email_verifier])
if __name__ == "__main__":
result = agent.invoke(
{"messages": [{"role": "user", "content": "Find an engineering contact at stripe.com and verify the address."}]}
)
print(result["messages"][-1].text)
```
### CrewAI
```bash
pip install "crewai==1.15.*"
```
The agent uses CrewAI's default model. To use another one, pass `llm` to `Agent`.
```python
from crewai import Agent, Crew, Task
from crewai.tools import tool
from tomba_mpp import paid_get
@tool("Domain search")
def domain_search(domain: str) -> dict:
"""Find email addresses at a company from its domain, for example stripe.com."""
return paid_get("/domain-search", {"domain": domain})
@tool("Email verifier")
def email_verifier(email: str) -> dict:
"""Check whether an email address is deliverable."""
return paid_get("/email-verifier", {"email": email})
researcher = Agent(
role="Lead researcher",
goal="Find and verify contacts at target companies",
backstory="You research B2B contacts with Tomba.",
tools=[domain_search, email_verifier],
)
task = Task(
description="Find an engineering contact at stripe.com and verify the address.",
expected_output="The contact's name, email address, and verification status.",
agent=researcher,
)
if __name__ == "__main__":
print(Crew(agents=[researcher], tasks=[task]).kickoff())
```
## Choose a currency
Each `402` offers one challenge in USDC and one in USDT, and your InFlow account must hold the currency of the challenge you pay.
- The Python helper pays in `TOMBA_MPP_CURRENCY`. It raises `StopIteration` when no challenge offers that currency.
- `mppx.fetch` pays the first challenge its methods can handle. To prefer a currency, pass `orderChallenges` to `Mppx.create`; it receives the candidate challenges and returns them in the order to try:
```typescript
import { Mppx, inflow } from "@inflowpayai/mpp-buyer";
const apiKey = process.env.INFLOW_API_KEY;
if (!apiKey) throw new Error("Set INFLOW_API_KEY");
const mppx = Mppx.create({
polyfill: false,
methods: [inflow({ apiKey, environment: "production" })],
// Try the USDT challenge first.
orderChallenges: (candidates) =>
[...candidates].sort(
(a, b) =>
Number(b.challenge.request.currency === "USDT") -
Number(a.challenge.request.currency === "USDT"),
),
});
```
## Errors and billing
Both helpers raise an error when the final response has an error status. The message includes the status, the value of `X-Payment-Charged`, and Tomba's response body. A response with `X-Payment-Charged: false` cost nothing. [Billing](/mpp/introduction#billing) and [Errors](/mpp/introduction#errors) list every outcome.
Each paid request is charged, including repeats, so store results your agent may need again instead of requesting them twice.
---
## Document: Troubleshooting
Messages from Tomba for Snowflake and how to fix them.
URL: /snowflake/troubleshooting
# Troubleshooting
## Could not reach api.tomba.io. In Snowsight open Data Products > Apps > (this app) > Security …
The connection to the Tomba API isn't approved yet. Open **Data Products » Apps » TOMBA » Security** and
approve **Tomba API**. See [Installation](/snowflake/installation#2-approve-the-connection-and-the-privileges).
## Cannot read CRM.PUBLIC.X … GRANT …
The app has no access to the table, or the name is wrong. Run the `GRANT` statements shown in the message as
the table's owner, and check the database, schema and table names.
## CRM.PUBLIC.X has no column 'US'
A `…_column` argument received a value instead of a column name. For example, `validate_phones` takes the
**name of the column** holding country codes (`'COUNTRY'`), not a country (`'US'`).
## … needs columns for (domain, first_name, last_name) or …
The columns you passed don't cover any input combination of the tool. Each tutorial lists the accepted
combinations; use `run_tool` for combinations the table procedure doesn't take.
## Daily lookup limit for this install reached
You reached today's limit (200 lookups while the install is new or on a listing trial). The run stops cleanly: rows already done
are kept, and it [resumes automatically](/snowflake/resume-and-auto-resume) after 00:00 UTC. Contact support if you need
more today.
## Credit limit for this run reached / Monthly credit budget reached
The run hit its `max_credits`, or the month's budget. Resume it, or raise the budget with
`set_setting('monthly_credit_budget', …)`. See [Cache and budgets](/snowflake/cache-and-budgets).
## Background runs, auto-resume and schedules need the EXECUTE TASK and EXECUTE MANAGED TASK privileges
Approve them on the app's **Security** tab, or grant them:
```sql
GRANT EXECUTE TASK ON ACCOUNT TO APPLICATION TOMBA;
GRANT EXECUTE MANAGED TASK ON ACCOUNT TO APPLICATION TOMBA;
```
Regular runs keep working without them.
## A background run stays QUEUED
Background runs are processed one at a time, oldest first, so a run stays queued while an earlier one is
running. If no run is `RUNNING` in `TOMBA.core.runs` and it is still queued, check the task privileges above,
then cancel it (`cancel_run`) and start it again.
## Snowflake billing failed, so this run was stopped
Rows written before the failure are kept. Contact [support@tomba.io](mailto:support@tomba.io) with the run id
from the message.
## This install is suspended / Tomba refused this request
Contact [support@tomba.io](mailto:support@tomba.io) with your Snowflake organization and account name.
## This app version (…) is no longer supported; upgrade the app
A newer version is required. Upgrade from **Data Products » Apps » TOMBA**.
## Results look wrong for some rows
Check the `INPUT_…` columns: values are cleaned before the lookup (domains lose `https://` and paths, emails
are lower-cased). Rows with `LOOKUP_STATUS = 'invalid'` couldn't be used and weren't sent. For match quality
issues, email [support@tomba.io](mailto:support@tomba.io) with examples.
## Still stuck?
Email [support@tomba.io](mailto:support@tomba.io), or check [status.tomba.io](https://status.tomba.io).
---
## Document: The Streamlit app
Use Tomba for Snowflake without SQL: the Tomba app in Snowsight, page by page.
URL: /snowflake/streamlit-app
# The Streamlit app
Open the app from **Data Products » Apps » TOMBA**. The **Tomba** app gives you every tool without writing
SQL. It calls the same procedures, so lookups made here are billed and recorded exactly like SQL calls.
Use the menu on the left to move between pages. The sidebar also shows your install's status (daily lookups
left), with links to Tomba's products and legal pages.
## Overview
A setup checklist (connection approved, connected to Tomba, table access), your daily lookup limit, and your
usage and spend over the last 30 days. If you set a monthly credit budget, a bar shows how much is used. It
also has a quick-start SQL snippet.
## Lookup
Look up one person, email, phone or company.
1. Pick a tool.
2. For tools with several input combinations (the Email Finder accepts domain + first/last name, domain +
full name, company + names…), choose one under **Search by**.
3. Fill in the fields and click **Look up**.
The result shows what was found, the credits used and the amount charged. A note says when the answer came
from the cache (free).
## Enrich a table
Run a tool over a whole table, step by step:
1. **Tool:** pick the tool.
2. **Input table:** enter `DATABASE.SCHEMA.TABLE`. The app previews it, or shows the `GRANT` statements to run
if it can't read it.
3. **Match columns:** columns are matched automatically (`EMAIL`, `COMPANY_DOMAIN`, `FIRST_NAME`, …); change
them if needed.
4. **Output:** the result table's name (it includes the date and time, so a new run doesn't replace an older
result), the maximum number of rows, and the tool's options.
5. **Cost and run:** the most the run can cost, a credit limit for the run, and **Run in the background**
(recommended for large tables). Tick the confirmation and click **Run**.
6. **Keep it enriched (optional):** create a [schedule](/snowflake/schedules) that looks up the table's new rows every
day, week or hour.
## Find companies
Build a table of companies from filters: countries, cities, industries, keywords, technologies, size, type,
revenue and founding year. See [Companies Search](/snowflake/tools/companies-search).
## Results
Every result table: a preview, **Download CSV** (the first 10,000 rows), the SQL to copy the table into your
database, and a confirmed **Drop table**.
## History
- **Running now:** progress bars for running and queued runs, with **Cancel**.
- **Stopped early:** runs that stopped (daily limit, cancel, credit limit, failed lookups), with **Resume** and
an **Auto-resume** switch.
- **All runs:** every run, with filters by tool and status, and totals for rows, credits and charges.
## Schedules
Your [schedules](/snowflake/schedules): their timing, last run, **Pause**/**Resume schedule**, **Run now** and
**Delete**. Changing schedules needs the `app_admin` role.
## Settings
Performance, cache and budget settings. Everyone can see them; saving needs `app_admin`. See the
[settings reference](/snowflake/reference/settings).
## Help & legal
Pricing and the credits each tool uses, access and privacy notes, links to every Tomba product, Tomba's legal
pages and support.
---
## Document: Schedules
Keep a Snowflake table enriched automatically: Tomba looks up only its new rows on a schedule.
URL: /snowflake/schedules
# Schedules
A schedule keeps a table enriched. At each scheduled time, the app looks up only the rows added since the
last run and appends them to the output table. Rows already done are never looked up or charged again.
Creating and changing schedules needs the `app_admin` role.
## Create a schedule
```sql
CALL TOMBA.core.create_schedule(
'DAILY_LEADS', -- name
'email_finder', -- tool
'CRM.PUBLIC.LEADS', -- input table
{'domain': 'COMPANY_DOMAIN', 'full_name': 'CONTACT_NAME'}, -- columns
'LEADS_ENRICHED', -- output table
'0 6 * * *', -- when (CRON, UTC)
{'max_credits': 1000}); -- options, plus an optional credit limit per run
```
The columns and options work as in [`run_tool`](/snowflake/enrich-a-table#other-input-combinations). The
Streamlit app creates schedules from the last step of **Enrich a table**.
## When it runs
`cron` has 5 fields (minute, hour, day of month, month, day of week), in UTC. Schedules run at most hourly,
so the minute field must be a single minute.
| Schedule | `cron` |
| ------------------------- | -------------- |
| Every day at 06:00 UTC | `0 6 * * *` |
| Every Monday at 07:30 UTC | `30 7 * * 1` |
| Every hour | `0 * * * *` |
| Weekdays at 18:00 UTC | `0 18 * * 1-5` |
## Manage schedules
```sql
CALL TOMBA.core.list_schedules(); -- every schedule and its last run
CALL TOMBA.core.pause_schedule('DAILY_LEADS');
CALL TOMBA.core.resume_schedule('DAILY_LEADS');
CALL TOMBA.core.run_schedule('DAILY_LEADS'); -- run it now
CALL TOMBA.core.drop_schedule('DAILY_LEADS'); -- the output table stays
```
Each scheduled run appears in `TOMBA.core.runs` as `schedule:`.
## Good to know
- Schedules run on serverless compute, billed to your account like any serverless task. A run with no new
rows finishes in seconds.
- Use `max_credits` to cap each run, and the [monthly budget](/snowflake/cache-and-budgets#monthly-budget) to cap them
all.
- Schedules need the app's task privileges (see [Installation](/snowflake/installation#2-approve-the-connection-and-the-privileges)).
---
## Document: Resume and auto-resume
Continue a Tomba for Snowflake run that stopped early, without paying again for rows already done.
URL: /snowflake/resume-and-auto-resume
# Resume and auto-resume
A run can stop before the end: the daily lookup limit is reached, a credit limit is hit, you cancel it, or
some lookups fail. The rows already done are kept, and the run can continue where it stopped. Rows
already looked up are neither looked up again nor charged again.
## Resume by running it again
Run the same call again: same tool, input table, columns, options and output table. If the previous run
stopped early, or had failed lookups, the app continues it:
```sql
CALL TOMBA.core.find_emails('CRM.PUBLIC.LEADS', 'COMPANY_DOMAIN', 'FIRST_NAME', 'LAST_NAME', 'LEADS_FOUND', 100000);
-- ... stops at the daily limit. Tomorrow, the same call continues it:
CALL TOMBA.core.find_emails('CRM.PUBLIC.LEADS', 'COMPANY_DOMAIN', 'FIRST_NAME', 'LAST_NAME', 'LEADS_FOUND', 100000);
```
The summary shows `resumed: true`, `rows_already_done`, and the new rows looked up. Rows marked `skipped` or
`error` are retried; `found`, `not_found` and `invalid` rows are kept.
When the previous run finished cleanly, or you change the tool, columns or options, the output table is
replaced instead.
## Resume a specific run
```sql
CALL TOMBA.core.resume_run('');
```
Find run ids in `TOMBA.core.runs`, or use **Resume** on the History page.
## Auto-resume
Runs that stopped for a temporary reason resume by themselves:
| Stopped because | Auto-resumes |
| -------------------------------------------------- | --------------------------------------- |
| The daily lookup limit was reached | Yes, after it resets at 00:00 UTC |
| The monthly credit budget was reached | Yes, when a new month starts |
| Some lookups failed (network, temporary errors) | Yes |
| The run was interrupted (timeout) | Yes |
| You cancelled it | No |
| The run's credit limit (`max_credits`) was reached | No: resume it yourself if you want more |
| The Tomba quota or the install was refused | No: contact support |
The app checks every day at 00:05 UTC and tries each run up to 3 times. Auto-resume uses the app's
background task, so it needs the task privileges (see
[Installation](/snowflake/installation#2-approve-the-connection-and-the-privileges)).
Turn it on or off for one run:
```sql
CALL TOMBA.core.set_auto_resume('', FALSE);
```
The History page has the same **Auto-resume** switch.
## See what stopped and why
```sql
SELECT run_id, tool, output_table, status, stop_kind, auto_resume, rows_total, message
FROM TOMBA.core.runs
WHERE status IN ('PARTIAL', 'FAILED', 'CANCELLED', 'ABANDONED')
ORDER BY started_at DESC;
```
---
## Document: Results and history
Find, copy and clean up Tomba for Snowflake results, and track every run, its credits and charges.
URL: /snowflake/results-and-history
# Results and history
## Result tables
Every table run writes to `TOMBA.results.`, in your account. Roles with `app_public` can read
them.
```sql
SHOW TABLES IN SCHEMA TOMBA.results;
SELECT * FROM TOMBA.results.LEADS_FOUND WHERE lookup_status = 'found';
```
Results stay until you drop them or uninstall the app. To keep them, copy them to your own database:
```sql
CREATE TABLE my_db.public.leads_found AS SELECT * FROM TOMBA.results.LEADS_FOUND;
```
Drop a result table you no longer need:
```sql
CALL TOMBA.core.drop_result('LEADS_FOUND');
```
The Streamlit app's **Results** page previews tables, downloads them as CSV, and drops them after a
confirmation.
## Run history
`TOMBA.core.runs` has one row per call: lookups, table runs, company searches, background and scheduled
runs.
```sql
SELECT started_at, tool, procedure_name, status, rows_total, rows_found, output_rows,
search_credits + verification_credits AS credits, charged_usd, output_table, message
FROM TOMBA.core.runs
ORDER BY started_at DESC
LIMIT 50;
```
Useful columns:
- `status`: `QUEUED`, `RUNNING`, `SUCCEEDED`, `PARTIAL`, `FAILED`, `FAILED_BILLING`, `CANCELLED` or `ABANDONED`
- `progress_pct`, `rows_expected`: progress of a running run
- `stop_kind`, `auto_resume`: why a run stopped, and whether it resumes by itself
- `rows_cached`: rows answered from the cache
- `charged_usd`: what Snowflake charges you for the run
## Usage by day
```sql
SELECT * FROM TOMBA.core.usage_by_day ORDER BY day DESC;
CALL TOMBA.core.usage_summary(30); -- the last 30 days, per day and tool
```
---
## Document: Snowflake quickstart
Your first lookups with Tomba for Snowflake: one value, then a whole table, then the results.
URL: /snowflake/quickstart
# Snowflake quickstart
This assumes you have [installed the app](/snowflake/installation) as `TOMBA` and approved the Tomba API
connection.
## 1. Look up one value
```sql
CALL TOMBA.core.email_verifier('jane.doe@acme.com');
```
The result is a `VARIANT` with `status`, the `results` (here the verification columns), the `credits` used and
the amount charged. Single lookups are the quickest way to see what a tool returns.
## 2. Create a small table
```sql
CREATE OR REPLACE TABLE my_db.public.leads (COMPANY_DOMAIN STRING, FIRST_NAME STRING, LAST_NAME STRING);
INSERT INTO my_db.public.leads VALUES
('acme.com', 'Jane', 'Doe'),
('globex.com', 'John', 'Smith');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.leads TO APPLICATION TOMBA;
```
## 3. Run a tool over the table
```sql
CALL TOMBA.core.find_emails('MY_DB.PUBLIC.LEADS', 'COMPANY_DOMAIN', 'FIRST_NAME', 'LAST_NAME',
'LEADS_FOUND', 100);
```
The arguments are the input table, the columns holding the domain, first and last name, the name of the
output table, and `max_rows`. The call returns a summary: rows read, found, not found, credits and the amount
charged.
## 4. Read the results
```sql
SELECT INPUT_DOMAIN, INPUT_FIRST_NAME, INPUT_LAST_NAME, EMAIL, SCORE, VERIFICATION_STATUS, POSITION
FROM TOMBA.results.LEADS_FOUND
WHERE lookup_status = 'found';
```
Every output table keeps your input values (`INPUT_…` columns) so you can join the results back.
## 5. Keep a copy in your database
```sql
CREATE TABLE my_db.public.leads_with_emails AS
SELECT * FROM TOMBA.results.LEADS_FOUND;
```
---
## Document: Privacy and security
What data Tomba for Snowflake sends, where results are stored, and how access is controlled.
URL: /snowflake/privacy-and-security
# Privacy and security
## What is sent to Tomba
- **Only the values you pass to a procedure:** the columns you name (domains, names, emails, URLs, phone
numbers) or the filters of a company search. The rest of your table never leaves Snowflake.
- **Only to `api.tomba.io`, over HTTPS.** The app's network rule allows no other destination, and it works
only after you approve the **Tomba API** connection.
- **Install details, when the app activates:** your Snowflake organization and account names, account
locator, region, and the app's name and version.
- **Usage counts for billing:** run id, tool, number of rows and hits, credits and amount charged. Never your
data.
- **Request logs:** as for any Tomba API key, each lookup that returns a result is recorded in Tomba's request
log, with the values sent.
## What stays in your account
- Result tables, in the app's `results` schema, readable by the roles you grant `app_public`.
- Run history, settings, the cache and schedules, inside the app.
- The app's Tomba credentials, in a private schema that no role can read. They can only call Tomba's lookup
endpoints.
Tomba doesn't keep a copy of your tables.
## Access control
- The app reads only the tables you `GRANT` it, and you can revoke a grant at any time.
- People use the app through its roles, `app_public` and `app_admin` (see
[Installation](/snowflake/installation#4-give-your-team-access)).
- Background runs and schedules run as the app, on serverless tasks, using the same grants.
## Legal
- [Terms of Service](https://tomba.io/legal/terms-of-service)
- [Privacy Policy](https://tomba.io/legal/privacy-policy)
- [Data Processing Addendum](https://tomba.io/legal/data-processing-addendum)
- [GDPR](https://tomba.io/legal/gdpr)
- [Security Policy](https://tomba.io/legal/security-policy)
- [Acceptable Use Policy](https://tomba.io/legal/acceptable-use-policy)
- [Privacy Notice for California Residents](https://tomba.io/legal/privacy-notice-for-california-residents)
---
## Document: Pricing and limits
What Tomba for Snowflake costs, how credits are charged per tool, the trial, and the app's limits.
URL: /snowflake/pricing-and-limits
# Pricing and limits
## Price
You pay $0.0098 per Tomba credit, billed by Snowflake on your Snowflake invoice. Search and verification
credits cost the same. You don't need a Tomba account or plan.
Snowflake bills whole cents, so each charge is rounded to the nearest cent. A table run is charged once per
credit type for each chunk of up to `chunk_size` rows (default 1,000), a companies search once per page of 50
companies, and a single lookup once:
| Credits | Charge |
| ------- | ------ |
| 1 | $0.01 |
| 3 | $0.03 |
| 51 | $0.50 |
| 1,000 | $9.80 |
## Credits per tool
You pay only for results. A lookup that finds nothing uses no credits.
| Tool | Credits when successful |
| -------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Email Finder, Email Enrichment, LinkedIn Finder, Author Finder | 1 search credit when an email is found |
| Email Verifier | 1 verification credit; `unknown` and `disposable` results are free |
| Domain Search | 1 search credit per 10 emails requested, when emails are found |
| Person Enrichment, Company Enrichment | 1 search credit when found |
| Combined Enrichment | 2 search credits when found |
| Phone Finder | 5 search credits when a valid phone is found (1 when searching by domain) |
| Phone Validator | 1 search credit per validation |
| Similar Domains | 1 search credit when found |
| Companies Search | 1 search credit per 50 companies returned |
| `enrich_mobile` option | +5 search credits per phone found |
These free cases use no credits:
- not found and invalid input
- errors
- results from the app's [cache](/snowflake/cache-and-budgets)
- repeats of the same lookup within the month
- duplicate rows in a run (looked up once)
Each output row's `CREDITS` column, and each run in `TOMBA.core.runs`, show exactly what was used.
## Trial
If the listing offers a trial, every call processes at most 100 rows during it, and nothing is charged.
New installs also start with a limit of 200 lookups per day (UTC). The limit is lifted automatically once
your install is confirmed in Snowflake's records, usually within a few hours; during a listing trial it stays
in place. `CALL TOMBA.core.status()` shows your limit and what is left today.
## Limits and speed
| | |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| Lookups in parallel | 60 by default (setting `max_workers`, 1–100) |
| Requests per second | about 28 (setting `requests_per_second`, at most 30) |
| Rows per call | 100,000 by default (`max_rows`), capped at 1,000,000 (setting `max_rows_cap`, up to 10,000,000) |
| Companies per search | 5,000 |
| Speed | about 100,000 rows per hour; use [background runs](/snowflake/background-runs) for large tables |
Use an X-Small warehouse: the work waits on the network, so a larger warehouse doesn't make runs faster.
Background runs and schedules use serverless compute, billed to your account by Snowflake.
---
## Document: Tomba for Snowflake
Find, verify and enrich professional emails, phones and companies on your Snowflake tables with the Tomba Native App, billed through your Snowflake account.
URL: /snowflake/introduction
# Tomba for Snowflake
Tomba for Snowflake is a Snowflake Native App that brings Tomba's B2B contact data to your tables. Point it
at a table of leads, contacts or accounts, and it finds emails, verifies them, enriches people and companies,
and finds or validates phone numbers. Results land in tables in your account, ready to join back to your data.
You don't need a Tomba account: Snowflake bills you per Tomba credit, on your Snowflake invoice.
## What you can do
| Tool | What it does |
| ----------------------------------------------------------- | ---------------------------------------------------- |
| [Email Finder](/snowflake/tools/email-finder) | A professional email from a name and a company |
| [Email Verifier](/snowflake/tools/email-verifier) | Whether an email is deliverable |
| [Domain Search](/snowflake/tools/domain-search) | The emails and people Tomba knows at a company |
| [Email Enrichment](/snowflake/tools/email-enrichment) | The person behind an email |
| [Person Enrichment](/snowflake/tools/person-enrichment) | A full person profile from an email |
| [Company Enrichment](/snowflake/tools/company-enrichment) | Firmographics from a domain |
| [Combined Enrichment](/snowflake/tools/combined-enrichment) | Person and company in one lookup |
| [LinkedIn Finder](/snowflake/tools/linkedin-finder) | The email behind a LinkedIn profile |
| [Author Finder](/snowflake/tools/author-finder) | The email of an article's author |
| [Phone Finder](/snowflake/tools/phone-finder) | A phone number from an email, domain or LinkedIn URL |
| [Phone Validator](/snowflake/tools/phone-validator) | Validate and format phone numbers |
| [Similar Domains](/snowflake/tools/similar-domains) | Companies similar to a domain |
| [Companies Search](/snowflake/tools/companies-search) | A list of companies from structured filters |
Every tool works on one value (a quick check) or on a whole table (60 lookups in parallel by default).
## Two ways to use it
- **SQL:** call the app's procedures from a worksheet, a notebook, dbt or a task:
```sql
CALL TOMBA.core.find_emails('CRM.PUBLIC.LEADS', 'COMPANY_DOMAIN', 'FIRST_NAME', 'LAST_NAME', 'LEADS_FOUND', 1000);
```
- **The Tomba app in Snowsight:** a point-and-click interface to look up values, enrich tables, search
companies, and follow your runs. See [The Streamlit app](/snowflake/streamlit-app).
## Large tables
- [Background runs](/snowflake/background-runs): large tables run in the app's serverless task, with progress,
cancel and resume.
- [Resume](/snowflake/resume-and-auto-resume): a run that stops early (daily limit, credit limit, cancel)
continues where it stopped, without charging again for rows already done.
- [Schedules](/snowflake/schedules): keep a table enriched by looking up only its new rows, every day or hour.
- [Cache and budgets](/snowflake/cache-and-budgets): repeat lookups are free from the cache, and credit limits
per run or per month stop runs before they overspend.
## Your data
From your tables, only the values you pass to a procedure (domains, names, emails, URLs, phone numbers) are
sent to the Tomba API, over a connection you approve. Results stay in your Snowflake account. See
[Privacy and security](/snowflake/privacy-and-security).
---
## Document: Snowflake installation
Install Tomba for Snowflake from the Snowflake Marketplace, approve its connection to the Tomba API, and give it access to your tables.
URL: /snowflake/installation
# Snowflake installation
Install the app, approve its connection and privileges, and grant it read access to the tables you want to
enrich.
## 1. Get the app
1. In Snowsight, open **Data Products » Marketplace** and search for **Tomba**.
2. Open the listing and click **Get**. Choose a name for the app (the examples in these docs use `TOMBA`) and
a warehouse. An **X-Small** warehouse is enough: the work waits on the network, not on compute.
3. Try it for free first if the listing offers a trial (see [Pricing and limits](/snowflake/pricing-and-limits#trial)).
## 2. Approve the connection and the privileges
Open the app from **Data Products » Apps » TOMBA** and go to the **Security** tab:
- **Tomba API connection:** approve it. It lets the app reach only `api.tomba.io` (HTTPS) and nothing else.
Until you approve it, every lookup stops with a message asking you to.
- **Privileges:** the app asks for `CREATE EXTERNAL ACCESS INTEGRATION` (to create that connection) and
`EXECUTE TASK` / `EXECUTE MANAGED TASK` (to run background runs, auto-resume and schedules on serverless
compute). They are usually granted automatically; if not, grant them on the same tab.
## 3. Let the app read your tables
The app only reads tables you grant it. Run this as the owner of each table (views work too):
```sql
GRANT USAGE ON DATABASE crm TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA crm.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE crm.public.leads TO APPLICATION TOMBA;
```
If you forget, the app tells you which `GRANT` statements to run.
## 4. Give your team access
The app has two roles:
| Role | Can |
| ------------ | ---------------------------------------------------------------------------------------- |
| `app_public` | Run every tool, read results and history, create background runs, resume and cancel runs |
| `app_admin` | Everything above, plus change settings, clear the cache, and create or change schedules |
```sql
GRANT APPLICATION ROLE TOMBA.app_public TO ROLE analyst;
GRANT APPLICATION ROLE TOMBA.app_admin TO ROLE sysadmin;
```
## 5. Check the setup
```sql
CALL TOMBA.core.status();
```
Look for `external_access` with status `APPROVED`. `activated` turns true on your first lookup, when the app
connects to Tomba by itself. The result also shows your daily lookup limit and what is left today.
Then continue with the [Quickstart](/snowflake/quickstart).
Snowflake installs app updates for you. When a new version is available, you can also upgrade from
**Data Products » Apps**. Upgrades keep your results and run history.
---
## Document: FAQ
Frequently asked questions about Tomba for Snowflake.
URL: /snowflake/faq
# FAQ
**Do I need a Tomba account?**
No. The app connects to Tomba by itself on your first lookup, and Snowflake bills you.
**Can I use my tomba.io plan or credits?**
No. Usage through Snowflake is billed by Snowflake and doesn't use a tomba.io plan.
**How much does it cost?**
See [Pricing and limits](/snowflake/pricing-and-limits).
**Am I charged twice for the same lookup?**
No. See the free cases under [Credits per tool](/snowflake/pricing-and-limits#credits-per-tool).
**How long does a large table take?**
See [Limits and speed](/snowflake/pricing-and-limits#limits-and-speed), and run large tables as
[background runs](/snowflake/background-runs).
**What happens if a run stops halfway?**
Rows already done are kept, and the run can continue. See [Resume and auto-resume](/snowflake/resume-and-auto-resume).
**Can I keep a table enriched automatically?**
Yes, with a [schedule](/snowflake/schedules).
**How do I limit spend?**
See [Cache and budgets](/snowflake/cache-and-budgets).
**Which warehouse should I use?**
X-Small. See [Limits and speed](/snowflake/pricing-and-limits#limits-and-speed).
**Where are my results?**
In `TOMBA.results.`. See [Results and history](/snowflake/results-and-history).
**Does Tomba see my tables?**
No. See [Privacy and security](/snowflake/privacy-and-security).
**Can I use it from dbt, tasks or notebooks?**
Yes. The tools are stored procedures: call them from anything that runs SQL in your account.
**Where do I get help?**
Email [support@tomba.io](mailto:support@tomba.io).
---
## Document: Enrich a table
Run any Tomba tool over a Snowflake table: arguments, input combinations with run_tool, output tables and good practice.
URL: /snowflake/enrich-a-table
# Enrich a table
Every tool has a **table procedure** that reads a table you granted the app, looks up each row, and writes an
output table in the app's `results` schema.
```sql
CALL TOMBA.core.verify_emails('CRM.PUBLIC.CONTACTS', 'EMAIL', 'CONTACTS_VERIFIED', 5000);
```
## Arguments
- **input_table:** fully qualified, `DATABASE.SCHEMA.TABLE`. Views work too.
- **…\_column:** the **names of columns** in that table (not values). Use `'"Mixed Case"'` for quoted column
names.
- **output_table:** the name of the result table: letters, digits and underscores. It is written to
`TOMBA.results.`.
- **max_rows:** the most distinct rows to look up in this call (default 100,000).
The table procedure of each tool is listed in its tutorial and in the
[procedures reference](/snowflake/reference/procedures).
## What a run does
1. It reads the distinct values of the columns you pass. Duplicate rows are looked up once. Rows with
a NULL in the required columns are left out, and rows whose values can't be used are marked `invalid`.
2. It looks the rows up, 60 at a time by default (setting `max_workers`).
3. It writes the results to the output table and returns a summary: rows, found, not found, invalid, errors,
skipped, credits and the amount charged.
Running again with the same tool, table, columns, options and output table continues a run that stopped
early (see [Resume](/snowflake/resume-and-auto-resume)). Otherwise the output table is replaced.
## Other input combinations
The table procedures take the most common inputs. `run_tool` runs any tool with any input combination and
options:
```sql
-- emails from a full name and a company name
CALL TOMBA.core.run_tool('email_finder', 'CRM.PUBLIC.LEADS',
{'company': 'COMPANY_NAME', 'full_name': 'CONTACT_NAME'}, 'LEADS_BY_COMPANY', 1000, {});
-- company phone numbers from domains
CALL TOMBA.core.run_tool('phone_finder', 'CRM.PUBLIC.ACCOUNTS', {'domain': 'WEBSITE'}, 'ACCOUNT_PHONES', 1000, {});
```
The keys of the mapping are the tool's input names (shown in each tutorial, or with
`CALL TOMBA.core.list_tools()`); the values are your column names. Options go in the last argument, together
with an optional `max_credits` (see [Cache and budgets](/snowflake/cache-and-budgets)).
## The output table
Each row has:
- `INPUT_` columns: your values, unchanged, for joining back
- the tool's columns (listed in each tutorial)
- `LOOKUP_STATUS`: `found`, `not_found`, `invalid` (the value can't be used, so it wasn't sent), `error` or
`skipped`
- `CREDITS`, `DATA` (the complete Tomba record), `SOURCE` (`api` or `cache`) and more; see
[Output columns](/snowflake/reference/output-columns)
```sql
-- join the emails back to your leads
SELECT l.*, r.EMAIL, r.SCORE
FROM crm.public.leads l
JOIN TOMBA.results.LEADS_FOUND r
ON r.INPUT_DOMAIN = l.COMPANY_DOMAIN AND r.INPUT_FIRST_NAME = l.FIRST_NAME AND r.INPUT_LAST_NAME = l.LAST_NAME
WHERE r.LOOKUP_STATUS = 'found';
```
## Good practice
- **Start small.** Run 100 rows first to check the match rate on your data.
- **Large tables in the background.** Use `run_tool_async` so nothing waits on the run (see
[Background runs](/snowflake/background-runs)).
- **Cap the cost.** Pass `{'max_credits': n}` in `run_tool` options (see [Cache and budgets](/snowflake/cache-and-budgets)).
- **Use an X-Small warehouse.** A bigger warehouse doesn't make lookups faster.
---
## Document: Cache and budgets
Save credits with Tomba for Snowflake: reuse recent results for free, and cap spend per run or per month.
URL: /snowflake/cache-and-budgets
# Cache and budgets
## Result cache
When the same lookup (same tool, input and options) was done recently, the app reuses that result instead of
calling Tomba again: no API call, no credit, no charge.
- Results are reused for 30 days by default (setting `cache_days`, 0 to 365; 0 turns the cache off).
- Cached rows have `SOURCE = 'cache'` in the output table, and single lookups return `"source": "cache"`.
- Only real answers are cached (`found` and `not_found`). Errors are always retried.
```sql
CALL TOMBA.core.set_setting('cache_days', '7'); -- app_admin
CALL TOMBA.core.clear_cache('email_verifier'); -- app_admin; '' clears every tool
```
Lower `cache_days` when freshness matters, for example for email verification before a campaign.
## Credit limit per run
Add `max_credits` to a run's options. The run is sized so it never goes over: it stops before a batch of
rows could exceed the limit, with status `PARTIAL`.
```sql
CALL TOMBA.core.run_tool('domain_search', 'CRM.PUBLIC.ACCOUNTS', {'domain': 'WEBSITE'}, 'ACCOUNT_EMAILS', 10000,
{'emails_per_domain': 20, 'max_credits': 5000});
```
The limit counts the worst case per row (for example 2 credits per domain for 20 emails, or +5 per phone with
`enrich_mobile`), so the actual spend is usually lower. [Resume](/snowflake/resume-and-auto-resume) the run to
continue past the limit.
## Monthly budget
Cap all runs in a calendar month (UTC), in Tomba credits:
```sql
CALL TOMBA.core.set_setting('monthly_credit_budget', '100000'); -- app_admin; 0 = no limit
```
When the budget is reached, runs stop as `PARTIAL` and resume automatically when the next month starts. The
Overview page of the Streamlit app shows how much of the budget is used.
Budgets count Tomba credits. [Pricing and limits](/snowflake/pricing-and-limits) lists the credits each tool
uses and their price.
---
## Document: Background runs
Run large Snowflake tables through Tomba in the background: queue, follow progress and cancel.
URL: /snowflake/background-runs
# Background runs
A table of 100,000 rows takes about an hour. Instead of waiting on the `CALL`, run it in the background: the
app queues the run, returns at once, and does the work in its own serverless task.
## Start a background run
`run_tool_async` takes the same arguments as [`run_tool`](/snowflake/enrich-a-table#other-input-combinations):
```sql
CALL TOMBA.core.run_tool_async('email_finder', 'CRM.PUBLIC.LEADS',
{'domain': 'COMPANY_DOMAIN', 'first_name': 'FIRST_NAME', 'last_name': 'LAST_NAME'},
'LEADS_FOUND', 500000, {'max_credits': 20000});
```
It returns `{run_id, status: 'QUEUED', output_table}`. Company searches have the same form:
`search_companies_async(filters, output_table, max_results)`.
In the Streamlit app, **Run in the background** on the Enrich a table page (and on Find companies) does the
same, and takes you to **History**.
## Follow progress
```sql
SELECT run_id, tool, status, progress_pct, rows_total, rows_expected, charged_usd, message
FROM TOMBA.core.runs
WHERE status IN ('QUEUED', 'RUNNING')
ORDER BY started_at DESC;
```
`progress_pct` is the share of rows done. The **History** page shows the same as progress bars.
## Cancel
```sql
CALL TOMBA.core.cancel_run('');
```
A running run stops after its current chunk of up to `chunk_size` rows (default 1,000; see
[Settings](/snowflake/reference/settings)); a queued run never starts. Rows already done are kept, and you can
[resume](/snowflake/resume-and-auto-resume) the run later.
## Good to know
- Background runs need the app's `EXECUTE TASK` and `EXECUTE MANAGED TASK` privileges (see
[Installation](/snowflake/installation#2-approve-the-connection-and-the-privileges)). Without them, the call explains
what to approve; regular runs still work.
- The serverless task uses Snowflake compute, billed to your account like any serverless task. Lookups wait
on the network, so it stays small.
- Runs are processed one at a time, oldest first.
---
## Document: Rust SDK
Install and use the tomba crate, the official Rust client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/rust-email-finder
# Rust SDK
The `tomba` crate is the official Rust client for the [Tomba API](/introduction). All methods hang off one blocking client, and each method sends one request.
## Requirements
- Rust 1.85 or later. `Cargo.toml` declares `rust-version = "1.63"`, but the current releases of the crate's dependencies (`reqwest` 0.12 and the crates it pulls in) need Rust 1.85.
- On Linux, OpenSSL development headers, for `reqwest`'s default TLS backend.
## Install
```bash
cargo add tomba
```
The client uses `reqwest::blocking`, so each call blocks the current thread. In async code, run calls with `tokio::task::spawn_blocking`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values in a `TombaConfig` to `Tomba::init`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```rust
use std::env;
use tomba::{Tomba, TombaConfig};
fn main() -> Result<(), Box> {
let client = Tomba::init(TombaConfig {
key: env::var("TOMBA_API_KEY")?,
secret: env::var("TOMBA_SECRET_KEY")?,
})?;
let result = client.email_finder("stripe.com", "Jane", "Doe", None)?;
if let Some(email) = result.data["data"]["email"].as_str() {
println!("{email}");
}
Ok(())
}
```
Every method returns `Result`. `TombaResponse.data` is the JSON body as a `serde_json::Value`, and `TombaResponse.rate_limit` holds the rate limit headers. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result.data["data"]["email"]`.
## Errors
`TombaError` has four variants:
| Variant | When |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Api { message, code }` | The API responded with status 400 or higher. `code` is the HTTP status; `message` is the raw response body, for example `{"errors":{"type":"authentication_failed","message":"Please enter a valid KEY.","code":400}}`. |
| `Http(reqwest::Error)` | The request failed before a response arrived, for example a timeout. |
| `Parse(serde_json::Error)` | A successful response body isn't JSON. |
| `InvalidParam(String)` | A bulk method got an unknown bulk type. No request is sent. |
```rust
use std::env;
use tomba::{Tomba, TombaConfig, TombaError};
fn main() -> Result<(), Box> {
let client = Tomba::init(TombaConfig {
key: env::var("TOMBA_API_KEY")?,
secret: env::var("TOMBA_SECRET_KEY")?,
})?;
match client.email_finder("stripe.com", "Jane", "Doe", None) {
Ok(result) => println!("{}", result.data["data"]["email"]),
Err(TombaError::Api { code, message }) => eprintln!("HTTP {code}: {message}"),
Err(err) => eprintln!("{err}"),
}
Ok(())
}
```
To branch on `errors.type`, parse `message` with `serde_json::from_str`. Each type is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `result.rate_limit` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) as `Option` fields: `x_second_rate_limit`, `x_minute_rate_limit`, `x_daily_rate_limit`, `x_minute_request_left`, `x_daily_request_left`, `x_minute_reset_seconds`, `x_daily_reset_seconds`, `retry_after`, `rate_limit_policy`, and `rate_limit`. `tomba::parse_rate_limit` reads the same headers from a `reqwest::header::HeaderMap`.
A `429` returns `TombaError::Api` with `code` 429 and no headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
All methods are on `Tomba`, the value returned by `Tomba::init`, and return `Result`. Request bodies are `&serde_json::Value`.
| Method | Endpoint |
| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `account()` | [`GET /me`](/api/account#get-account) |
| `usage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `logs(page: Option, limit: Option)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `domain_search(domain: &str, enrich_mobile: Option, webhook_url: Option<&str>)` | [`GET /domain-search`](/api/finder#domain-search) |
| `email_finder(domain: &str, first_name: &str, last_name: &str, webhook_url: Option<&str>)` | [`GET /email-finder`](/api/finder#email-finder) |
| `author_finder(url: &str, webhook_url: Option<&str>)` | [`GET /author-finder`](/api/finder#author-finder) |
| `linkedin_finder(url: &str, enrich_mobile: Option, full: Option, webhook_url: Option<&str>)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `email_format(domain: &str)` | [`GET /email-format`](/api/finder#email-format) |
| `count(domain: &str)` | [`GET /email-count`](/api/finder#email-count) |
| `email_verifier(email: &str, webhook_url: Option<&str>, enrich_mobile: Option)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `email_sources(email: &str)` | [`GET /email-sources`](/api/email#email-sources) |
| `phone_finder(email: &str, webhook_url: Option<&str>)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `phone_validator(phone: &str)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `status(domain: &str)` | [`GET /domain-status`](/api/domain#domain-status) |
| `autocomplete(query: &str)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `similar_domains(domain: &str)` | [`GET /similar`](/api/domain#similar) |
| `list_keys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `get_key(id: &str)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `create_key(body: &Value)` | [`POST /keys`](/api/keys#create-an-api-key) |
| `delete_key(id: &str)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `list_leads(domain: Option<&str>)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `get_lead(id: &str)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `create_lead(body: &Value)` | [`POST /leads`](/api/leads#create-a-lead) |
| `update_lead(id: &str, body: &Value)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `delete_lead(id: &str)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `list_leads_lists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `get_leads_list(id: &str)` | [`GET /leads_lists/{id}`](/api/lead-lists#retrieve-a-single-leads-list) |
| `create_leads_list(body: &Value)` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `update_leads_list(id: &str, body: &Value)` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `delete_leads_list(id: &str)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `list_bulk(bulk_type: &str)` | [`GET /bulk/{type}`](/api/bulks) |
| `create_bulk(bulk_type: &str, body: &Value)` | [`POST /bulk/{type}`](/api/bulks) |
| `get_bulk(bulk_type: &str, id: &str)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `launch_bulk(bulk_type: &str, id: &str)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `bulk_progress(bulk_type: &str, id: &str)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `rename_bulk(bulk_type: &str, id: &str, name: &str)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `archive_bulk(bulk_type: &str, id: &str)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `delete_bulk(bulk_type: &str, id: &str)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`bulk_type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types); the order of calls is described in [Lifecycle](/bulks#lifecycle). `create_bulk` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs.
## Source
[github.com/tomba-io/rust](https://github.com/tomba-io/rust) (Apache-2.0).
---
## Document: Ruby SDK
Install and use the tomba gem, the official Ruby client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/ruby-email-finder
# Ruby SDK
The `tomba` gem is the official Ruby client for the [Tomba API](/introduction). Each API area is a service class in the `Tomba` module, and each method sends one request.
## Requirements
Ruby 2.5 or later, as declared in the gemspec. The gem uses only the standard library.
## Install
```bash
gem install tomba
```
Or add `gem 'tomba'` to your `Gemfile`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `set_key` and `set_secret`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```ruby
require 'tomba'
client = Tomba::Client.new
client.set_key(ENV.fetch('TOMBA_API_KEY')).set_secret(ENV.fetch('TOMBA_SECRET_KEY'))
finder = Tomba::Finder.new(client)
result = finder.email_finder(domain: 'stripe.com', first_name: 'Jane', last_name: 'Doe')
puts result['data']['data']['email']
```
Every method returns a hash with two keys: `'data'`, the parsed JSON body, and `'rate_limit'`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result['data']['data']['email']`.
## Errors
The SDK raises `Tomba::Exception` when the API responds with status 400 or higher, when the body isn't JSON, when the connection fails, and before sending a request when a required argument is `nil`.
| Attribute | Value |
| ---------- | ------------------------------------------------------------------------------------------------------------------- |
| `code` | The status from the error body's `errors.code`, which matches the HTTP status. `nil` when no response was received. |
| `message` | The API's `errors.message`. |
| `response` | The parsed error body, `{ 'errors' => { 'type' => …, 'message' => …, 'code' => … } }`, or `nil`. |
```ruby
require 'tomba'
client = Tomba::Client.new
client.set_key(ENV.fetch('TOMBA_API_KEY')).set_secret(ENV.fetch('TOMBA_SECRET_KEY'))
begin
finder = Tomba::Finder.new(client)
result = finder.email_finder(domain: 'stripe.com', first_name: 'Jane', last_name: 'Doe')
puts result['data']['data']['email']
rescue Tomba::Exception => e
puts "#{e.code} #{e.message}"
puts e.response['errors']['type'] if e.response
end
```
Each `errors.type` is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `result['rate_limit']` holds the response's [rate limit headers](/rate-limits#rate-limit-headers), keyed by header name: `x-second-rate-limit`, `x-minute-rate-limit`, `x-daily-rate-limit`, `x-minute-request-left`, `x-daily-request-left`, `x-minute-reset-seconds`, `x-daily-reset-seconds`, `retry-after`, `ratelimit-policy`, and `ratelimit`. Numbers are integers, and a header that is absent is `nil`.
A `429` raises `Tomba::Exception` with `code` 429, without the response headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Connections and reads time out after 120 seconds; the timeouts aren't configurable.
## Method reference
Create each service with the client, for example `Tomba::Finder.new(client)`. Arguments shown as `name:` are keyword arguments.
| Method | Endpoint |
| ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `Account#get_account` | [`GET /me`](/api/account#get-account) |
| `Usage#get_usage` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs#get_logs(page: nil, limit: nil)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Domain#domain_search(domain:, page: nil, limit: nil, department: nil, enrich_mobile: nil, webhook_url: nil)` | [`GET /domain-search`](/api/finder#domain-search) |
| `Finder#email_finder(domain:, first_name:, last_name:, webhook_url: nil)` | [`GET /email-finder`](/api/finder#email-finder) |
| `Finder#author_finder(url:, webhook_url: nil)` | [`GET /author-finder`](/api/finder#author-finder) |
| `Format#email_format(domain)` | [`GET /email-format`](/api/finder#email-format) |
| `Location#get_location(domain)` | [`GET /location`](/api/finder#location) |
| `Count#email_count(domain:)` | [`GET /email-count`](/api/finder#email-count) |
| `Verifier#email_verifier(email:, webhook_url: nil)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources#email_sources(email:)` | [`GET /email-sources`](/api/email#email-sources) |
| `Finder#phone_finder(email:, webhook_url: nil)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Phone#finder(params, webhook_url: nil)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Phone#validator(phone, country_code: nil)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status#domain_status(domain:)` | [`GET /domain-status`](/api/domain#domain-status) |
| `Status#auto_complete(query:)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Similar#websites(domain)` | [`GET /similar`](/api/domain#similar) |
| `Technology#list(domain)` | [`GET /technology`](/api/domain#technology) |
| `Keys#get_keys` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Keys#get_key(id:)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `Keys#create_key` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Keys#reset_key(id:)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Keys#delete_key(id:)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Leads#list_leads(page: nil, limit: nil, domain: nil)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Leads#get_lead(id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Leads#create_lead(**data)` | [`POST /leads`](/api/leads#create-a-lead) |
| `Leads#update_lead(id, **data)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Leads#delete_lead(id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `LeadsLists#get_lists` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `LeadsLists#delete_list_id(id:)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `Bulk#list(type, params: {})` | [`GET /bulk/{type}`](/api/bulks) |
| `Bulk#create(type, data)` | [`POST /bulk/{type}`](/api/bulks) |
| `Bulk#get(type, id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `Bulk#progress(type, id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `Bulk#rename(type, id, name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
`type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types). `Bulk#create` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs.
## Source
[github.com/tomba-io/ruby](https://github.com/tomba-io/ruby) (Apache-2.0). Release notes are in [CHANGES](https://github.com/tomba-io/ruby/blob/master/CHANGES).
---
## Document: R SDK
Install and use the tomba package, the official R client for the Tomba API: authentication, errors, rate limit data, and every function.
URL: /libraries/r-email-finder
# R SDK
The `tomba` package is the official R client for the [Tomba API](/introduction). Every function takes a `Tomba` client object and sends one request. This page describes version 1.1.0, which is available from GitHub.
## Requirements
- R 3.5.0 or later, as declared in `DESCRIPTION`.
- The `httr` and `jsonlite` packages, installed as dependencies.
## Install
```r
install.packages("remotes")
remotes::install_github("tomba-io/r")
```
CRAN has version 1.0.1. It has only `account`, `autocomplete`, `count`, `domain_search`, `email_finder`, `email_sources`, `email_verifier`, `logs`, `status`, and `usage`, and those return the response body directly, without the `data` and `rate_limit` wrapper described below.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both, or set them in `~/.Renviron`:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The package doesn't read environment variables itself. Pass the values as `Tomba(key = ..., secret = ...)`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```r
library(tomba)
client <- Tomba(key = Sys.getenv("TOMBA_API_KEY"), secret = Sys.getenv("TOMBA_SECRET_KEY"))
result <- email_finder(client, domain = "stripe.com", fname = "Jane", lname = "Doe")
print(result$data$data$email)
```
Every function returns a list with two elements: `data`, the response body parsed with `jsonlite::fromJSON`, and `rate_limit`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result$data$data$email`.
## Errors
When the API responds with status 400 or higher, the function signals an R error whose message is `Tomba API error : `. Catch it with `tryCatch`:
```r
library(tomba)
client <- Tomba(key = Sys.getenv("TOMBA_API_KEY"), secret = Sys.getenv("TOMBA_SECRET_KEY"))
result <- tryCatch(
email_finder(client, domain = "stripe.com", fname = "Jane", lname = "Doe"),
error = function(e) {
message(conditionMessage(e))
NULL
}
)
if (!is.null(result)) print(result$data$data$email)
```
The response body in the message is the JSON error, `{"errors":{"type":…,"message":…,"code":…}}`. Each `errors.type` is described in [Errors](/error-handling#error-types). `person_find`, `company_find`, and `combined_find` don't signal an error when nothing is found: those endpoints answer with status 200 and an `errors` body, so check `result$data$errors`.
## Rate limits and retries
The package doesn't retry. `result$rate_limit` is a named list of the response's [rate limit headers](/rate-limits#rate-limit-headers) that are present, with character values: `x-second-rate-limit`, `x-minute-rate-limit`, `x-daily-rate-limit`, `x-minute-request-left`, `x-daily-request-left`, `x-minute-reset-seconds`, `x-daily-reset-seconds`, `retry-after`, `ratelimit-policy`, and `ratelimit`.
A `429` signals an error without the response headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
Every function takes the client as `obj`, its first argument.
| Function | Endpoint |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `account(obj)` | [`GET /me`](/api/account#get-account) |
| `usage(obj)` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `logs(obj, page = NULL, limit = NULL)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `domain_search(obj, domain, enrich_mobile = NULL, webhook_url = NULL)` | [`GET /domain-search`](/api/finder#domain-search) |
| `email_finder(obj, domain, fname, lname, webhook_url = NULL)` | [`GET /email-finder`](/api/finder#email-finder) |
| `author_finder(obj, url, webhook_url = NULL)` | [`GET /author-finder`](/api/finder#author-finder) |
| `linkedin_finder(obj, url, webhook_url = NULL)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `email_format(obj, domain)` | [`GET /email-format`](/api/finder#email-format) |
| `get_location(obj, domain)` | [`GET /location`](/api/finder#location) |
| `count(obj, domain)` | [`GET /email-count`](/api/finder#email-count) |
| `email_verifier(obj, email, webhook_url = NULL)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `email_sources(obj, email)` | [`GET /email-sources`](/api/email#email-sources) |
| `person_find(obj, email)` | [`GET /people/find`](/api/enrichment#person-api) |
| `company_find(obj, domain)` | [`GET /companies/find`](/api/enrichment#company-api) |
| `combined_find(obj, email)` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `phone_finder(obj, email, webhook_url = NULL)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `phone_validator(obj, phone, country_code = NULL)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `status(obj, domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `autocomplete(obj, search)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `similar_domains(obj, domain)` | [`GET /similar`](/api/domain#similar) |
| `technology_check(obj, domain)` | [`GET /technology`](/api/domain#technology) |
| `companies_search(obj, data = list())` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `list_flags(obj, page = NULL, limit = NULL)` | [`GET /flag`](/api/flag#list-my-flags) |
| `list_keys(obj)` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `get_key(obj, id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `create_key(obj, data = list())` | [`POST /keys`](/api/keys#create-an-api-key) |
| `reset_key(obj, id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `delete_key(obj, id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `list_leads(obj, page = NULL, limit = NULL, domain = NULL)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `get_lead(obj, lead_id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `create_lead(obj, data = list())` | [`POST /leads`](/api/leads#create-a-lead) |
| `update_lead(obj, lead_id, data = list())` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `delete_lead(obj, lead_id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `list_leads_lists(obj)` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `create_leads_list(obj, data = list())` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `update_leads_list(obj, id, data = list())` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `delete_leads_list(obj, id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `list_lead_attributes(obj)` | [`GET /attributes`](/api/lead-attributes#retrieve-lead-attributes) |
| `create_lead_attribute(obj, data = list())` | [`POST /attributes`](/api/lead-attributes#create-a-lead-attribute) |
| `update_lead_attribute(obj, id, data = list())` | [`PUT /attributes/{id}`](/api/lead-attributes#update-a-lead-attribute) |
| `delete_lead_attribute(obj, id)` | [`DELETE /attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute) |
| `list_bulks(obj, bulk_type, page = NULL, limit = NULL)` | [`GET /bulk/{type}`](/api/bulks) |
| `create_bulk(obj, bulk_type, data = list())` | [`POST /bulk/{type}`](/api/bulks) |
| `get_bulk(obj, bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `launch_bulk(obj, bulk_type, bulk_id)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `bulk_progress(obj, bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `rename_bulk(obj, bulk_type, bulk_id, name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `archive_bulk(obj, bulk_type, bulk_id)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `delete_bulk(obj, bulk_type, bulk_id)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`bulk_type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types); the order of calls is described in [Lifecycle](/bulks#lifecycle). `create_bulk` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs.
## Source
[github.com/tomba-io/r](https://github.com/tomba-io/r) (Apache-2.0).
---
## Document: Python SDK
Install and use tomba-io, the official Python client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/python-email-finder
# Python SDK
`tomba-io` is the official Python client for the [Tomba API](/introduction). Each API area is a service class, and each method sends one request.
## Requirements
- Python 3.8 or later. The package declares no minimum version; its CI tests Python 3.8 to 3.12.
- [`requests`](https://pypi.org/project/requests/), installed as a dependency.
## Install
```bash
pip install tomba-io
```
The import name is `tomba`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `set_key()` and `set_secret()`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```python
import os
from tomba import Client, Finder
client = Client()
client.set_key(os.environ["TOMBA_API_KEY"]).set_secret(os.environ["TOMBA_SECRET_KEY"])
result = Finder(client).email_finder("stripe.com", "Jane", "Doe")
print(result["data"]["data"]["email"])
```
Every method returns a dict with two keys: `data`, the decoded JSON body, and `rate_limit`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result["data"]["data"]["email"]`.
## Errors
The SDK raises `TombaException` when the API responds with status 400 or higher, when the request fails, and before sending a request when a required argument is `None`.
| Attribute | Value |
| ---------- | ------------------------------------------------------------------------------------------------------ |
| `code` | The HTTP status. `0` when no response was received, for example a missing argument or a network error. |
| `message` | The API's `errors.message`, or the underlying error. |
| `response` | The decoded error body, `{"errors": {"type": …, "message": …, "code": …}}`, or `None`. |
```python
import os
from tomba import Client, Finder, TombaException
client = Client()
client.set_key(os.environ["TOMBA_API_KEY"]).set_secret(os.environ["TOMBA_SECRET_KEY"])
try:
result = Finder(client).email_finder("stripe.com", "Jane", "Doe")
except TombaException as error:
print(error.code, error.message)
if error.response:
print(error.response["errors"]["type"])
else:
print(result["data"]["data"]["email"])
```
`Enrichment.person()`, `company()`, and `combined()` don't raise when nothing is found: those endpoints answer with status 200 and an `errors` body, so check `result["data"].get("errors")`. Each `errors.type` is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `result["rate_limit"]` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) as `second_limit`, `minute_limit`, `daily_limit`, `minute_remaining`, `daily_remaining`, `minute_reset`, `daily_reset`, `retry_after`, `policy`, and `rate_limit`. Numbers are integers, and a header that is absent or `0` becomes `None`.
A `429` raises `TombaException` with `code` 429, without the `Retry-After` header. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds. Change the timeout with `client.set_timeout(seconds)`.
## Method reference
Create each service with the client, for example `Finder(client)`. Every class is importable from `tomba`. IDs are strings.
| Method | Endpoint |
| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `Account.get_account()` | [`GET /me`](/api/account#get-account) |
| `Usage.get_usage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs.get_logs(page=None, limit=None)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Domain.domain_search(domain, page=None, limit=None, department=None, country=None, enrich_mobile=None, webhook_url=None)` | [`GET /domain-search`](/api/finder#domain-search) |
| `Finder.email_finder(domain, first_name, last_name, webhook_url=None)` | [`GET /email-finder`](/api/finder#email-finder) |
| `Finder.author_finder(url, webhook_url=None)` | [`GET /author-finder`](/api/finder#author-finder) |
| `Finder.linkedin_finder(url)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Finder.enrichment(email, webhook_url=None)` | [`GET /enrich`](/api/finder#email-enrichment) |
| `Finder.format(domain)`, `Format.email_format(domain)` | [`GET /email-format`](/api/finder#email-format) |
| `Finder.location(domain)`, `Location.get_location(domain)` | [`GET /location`](/api/finder#location) |
| `Count.email_count(domain)` | [`GET /email-count`](/api/finder#email-count) |
| `Verifier.email_verifier(email, enrich_mobile=None, webhook_url=None)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources.email_sources(email)` | [`GET /email-sources`](/api/email#email-sources) |
| `Enrichment.person(email, webhook_url=None)` | [`GET /people/find`](/api/enrichment#person-api) |
| `Enrichment.company(domain)` | [`GET /companies/find`](/api/enrichment#company-api) |
| `Enrichment.combined(email)` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `Phone.finder(email=None, domain=None, linkedin=None, full=None, webhook_url=None)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Phone.validator(phone)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status.domain_status(domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `Status.auto_complete(query)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Similar.websites(domain)` | [`GET /similar`](/api/domain#similar) |
| `Technology.list(domain)` | [`GET /technology`](/api/domain#technology) |
| `Reveal.search_companies(**params)` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `Flag.list_flags(page=None, limit=None)` | [`GET /flag`](/api/flag#list-my-flags) |
| `Keys.get_keys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Keys.get_key(id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `Keys.create_key(**params)` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Keys.reset_key(id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Keys.delete_key(id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Leads.list_leads(page=None, limit=None, domain=None)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Leads.get_lead(lead_id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Leads.create_lead(**params)` | [`POST /leads`](/api/leads#create-a-lead) |
| `Leads.update_lead(lead_id, **params)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Leads.delete_lead(lead_id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `LeadsLists.get_lists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `LeadsLists.create_list(**params)` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `LeadsLists.update_list_id(id, **params)` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `LeadsLists.delete_list_id(id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `LeadsAttributes.get_lead_attributes()` | [`GET /attributes`](/api/lead-attributes#retrieve-lead-attributes) |
| `LeadsAttributes.create_lead_attribute(**params)` | [`POST /attributes`](/api/lead-attributes#create-a-lead-attribute) |
| `LeadsAttributes.update_lead_attribute(id, **params)` | [`PUT /attributes/{id}`](/api/lead-attributes#update-a-lead-attribute) |
| `LeadsAttributes.delete_lead_attribute(id)` | [`DELETE /attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute) |
| `Bulk.list_bulks(bulk_type, page=None, limit=None)` | [`GET /bulk/{type}`](/api/bulks) |
| `Bulk.create_bulk(bulk_type, **params)` | [`POST /bulk/{type}`](/api/bulks) |
| `Bulk.get_bulk(bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `Bulk.launch_bulk(bulk_type, bulk_id)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `Bulk.bulk_progress(bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `Bulk.download_bulk(bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `Bulk.rename_bulk(bulk_type, bulk_id, name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `Bulk.archive_bulk(bulk_type, bulk_id)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `Bulk.delete_bulk(bulk_type, bulk_id)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`bulk_type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types); the order of calls is described in [Lifecycle](/bulks#lifecycle). `create_bulk` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs. `download_bulk` returns the results file, a CSV, as bytes in `result["data"]`.
## Source
[github.com/tomba-io/python](https://github.com/tomba-io/python) (Apache-2.0). Release notes are in [CHANGELOG.md](https://github.com/tomba-io/python/blob/master/CHANGELOG.md).
---
## Document: PHP SDK
Install and use tomba-io/php, the official PHP client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/php-email-finder
# PHP SDK
`tomba-io/php` is the official PHP client for the [Tomba API](/introduction). Each API area is a service class, and each method sends one request.
## Requirements
- PHP 8.1 or later, as declared in `composer.json`.
- The `curl` and `json` extensions.
## Install
```bash
composer require tomba-io/php
```
Classes are autoloaded from the `Tomba` namespace; the services are in `Tomba\Services`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `setKey()` and `setSecret()`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```php
setKey(getenv('TOMBA_API_KEY'))
->setSecret(getenv('TOMBA_SECRET_KEY'));
$result = (new Finder($client))->emailFinder('stripe.com', 'Jane', 'Doe');
echo $result['data']['data']['email'], PHP_EOL;
```
Every method returns an array with two keys: `data`, the decoded JSON body, and `rate_limit`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `$result['data']['data']['email']`.
## Errors
The SDK throws `Tomba\TombaException` when the API responds with status 400 or higher, when cURL fails, and before sending a request when a required argument is empty.
| Method | Value |
| --------------- | ---------------------------------------------------------------------------------------------- |
| `getCode()` | The HTTP status. `0` when the error happened before a response was received. |
| `getMessage()` | The API's `errors.message`, or the cURL error. |
| `getResponse()` | The decoded error body, `['errors' => ['type' => …, 'message' => …, 'code' => …]]`, or `null`. |
```php
setKey(getenv('TOMBA_API_KEY'))
->setSecret(getenv('TOMBA_SECRET_KEY'));
try {
$result = (new Finder($client))->emailFinder('stripe.com', 'Jane', 'Doe');
echo $result['data']['data']['email'], PHP_EOL;
} catch (TombaException $e) {
echo $e->getCode(), ' ', $e->getMessage(), PHP_EOL;
echo $e->getResponse()['errors']['type'] ?? '', PHP_EOL;
}
```
`Enrichment::person()`, `company()`, and `combined()` don't throw when nothing is found: those endpoints answer with status 200 and an `errors` body, so check `$result['data']['errors']`. Each `errors.type` is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `$result['rate_limit']` holds the response's [rate limit headers](/rate-limits#rate-limit-headers), keyed by header name: `x-second-rate-limit`, `x-minute-rate-limit`, `x-daily-rate-limit`, `x-minute-request-left`, `x-daily-request-left`, `x-minute-reset-seconds`, `x-daily-reset-seconds`, `retry-after`, `ratelimit-policy`, and `ratelimit`. Numbers are integers, and a header that is absent is `null`.
A `429` throws `TombaException` with code 429, without the response headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
Create each service with the client, for example `new Finder($client)`.
| Method | Endpoint |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `Account::getAccount()` | [`GET /me`](/api/account#get-account) |
| `Usage::getUsage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs::getLogs(?int $page = null, ?int $limit = null)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Domain::domainSearch(string $domain, int $page = null, int $limit = null, string $department = null, bool $enrichMobile = null, string $webhookUrl = null)` | [`GET /domain-search`](/api/finder#domain-search) |
| `Finder::emailFinder(string $domain, string $firstName, string $lastName, string $webhookUrl = null)` | [`GET /email-finder`](/api/finder#email-finder) |
| `Finder::authorFinder(string $url, string $webhookUrl = null)` | [`GET /author-finder`](/api/finder#author-finder) |
| `Finder::linkedinFinder(string $url, string $webhookUrl = null)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Format::emailFormat(string $domain)` | [`GET /email-format`](/api/finder#email-format) |
| `Location::getLocation(string $domain)` | [`GET /location`](/api/finder#location) |
| `Count::emailCount(string $domain)` | [`GET /email-count`](/api/finder#email-count) |
| `Verifier::emailVerifier(string $email, string $webhookUrl = null)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources::emailSources(string $email)` | [`GET /email-sources`](/api/email#email-sources) |
| `Enrichment::person(string $email, string $webhookUrl = null)` | [`GET /people/find`](/api/enrichment#person-api) |
| `Enrichment::company(string $domain)` | [`GET /companies/find`](/api/enrichment#company-api) |
| `Enrichment::combined(string $email)` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `Finder::phoneFinder(string $email, string $webhookUrl = null)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Phone::phoneFinder(array $params, string $webhookUrl = null)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Phone::phoneValidator(string $phone, ?string $countryCode = null)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status::domainStatus(string $domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `Status::autoComplete(string $query)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Similar::websites(string $domain)` | [`GET /similar`](/api/domain#similar) |
| `Technology::list(string $domain)` | [`GET /technology`](/api/domain#technology) |
| `Reveal::companiesSearch(array $params)` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `Flag::listFlags(?int $page = null, ?int $limit = null)` | [`GET /flag`](/api/flag#list-my-flags) |
| `Keys::getKeys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Keys::getKey(string $id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `Keys::createKey()` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Keys::resetKey(string $id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Keys::deleteKey(string $id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Leads::listLeads(?int $page = null, ?int $limit = null, ?string $domain = null)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Leads::getLead(string $id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Leads::createLead(array $data)` | [`POST /leads`](/api/leads#create-a-lead) |
| `Leads::updateLead(string $id, array $data)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Leads::deleteLead(string $id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `LeadsLists::getLists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `LeadsLists::deleteListId(string $id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `LeadsAttributes::getLeadAttributes()` | [`GET /attributes`](/api/lead-attributes#retrieve-lead-attributes) |
| `LeadsAttributes::deleteLeadAttribute(string $id)` | [`DELETE /attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute) |
| `Bulk::listBulks(string $type, ?array $params = null)` | [`GET /bulk/{type}`](/api/bulks) |
| `Bulk::createBulk(string $type, array $data)` | [`POST /bulk/{type}`](/api/bulks) |
| `Bulk::getBulk(string $type, int $id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `Bulk::launchBulk(string $type, int $id)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `Bulk::bulkProgress(string $type, int $id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `Bulk::downloadBulk(string $type, int $id)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `Bulk::renameBulk(string $type, int $id, string $name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `Bulk::archiveBulk(string $type, int $id)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `Bulk::deleteBulk(string $type, int $id)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`$type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types); the order of calls is described in [Lifecycle](/bulks#lifecycle). `createBulk()` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs. `downloadBulk()` returns the results file, a CSV, as a string in `$result['data']`.
## Source
[github.com/tomba-io/php](https://github.com/tomba-io/php) (Apache-2.0). Release notes are in [CHANGELOG.md](https://github.com/tomba-io/php/blob/master/CHANGELOG.md).
---
## Document: Perl SDK
Install and use Tomba::Finder, the official Perl client for the Tomba API on CPAN: authentication, errors, rate limit data, and every method.
URL: /libraries/perl-email-finder
# Perl SDK
`Tomba::Finder` is the official Perl client for the [Tomba API](/introduction), published on CPAN in the `Tomba-Finder` distribution. One `Tomba::Finder` object exposes every method, and each method sends one request.
## Requirements
- Perl 5.26.1 or later, as declared in `Makefile.PL`.
- `LWP::UserAgent`, `JSON`, and `URI`, installed as dependencies. HTTPS requests also need `LWP::Protocol::https`.
## Install
```bash
cpanm Tomba::Finder
```
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `Tomba::Finder->new($key, $secret)`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. The constructor dies if either value is shorter than 39 characters. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```perl
use strict;
use warnings;
use Tomba::Finder;
my $tomba = Tomba::Finder->new($ENV{TOMBA_API_KEY}, $ENV{TOMBA_SECRET_KEY});
my $result = $tomba->EmailFinder('stripe.com', 'Jane', 'Doe');
die "$result->{data}{errors}{message}\n" if $result->{data}{errors};
print $result->{data}{data}{email} // '', "\n";
```
Every method returns a hash reference with two keys: `data`, the decoded JSON body, and `rate_limit`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `$result->{data}{data}{email}`.
## Errors
Methods don't die on API errors. They return the decoded error body in `data`, so check `$result->{data}{errors}` after every call:
```perl
use strict;
use warnings;
use Tomba::Finder;
my $tomba = Tomba::Finder->new($ENV{TOMBA_API_KEY}, $ENV{TOMBA_SECRET_KEY});
my $result = $tomba->EmailFinder('stripe.com', 'Jane', 'Doe');
if (my $error = $result->{data}{errors}) {
print "$error->{code} $error->{type}: $error->{message}\n";
}
else {
print $result->{data}{data}{email} // '', "\n";
}
```
`errors.code` is the HTTP status. Each `errors.type` is described in [Errors](/error-handling#error-types). `GET` methods die when a response body isn't JSON, for example a gateway timeout page.
## Rate limits and retries
The SDK doesn't retry. `$result->{rate_limit}` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) that are present, keyed by header name: `x-second-rate-limit`, `x-minute-rate-limit`, `x-daily-rate-limit`, `x-minute-request-left`, `x-daily-request-left`, `x-minute-reset-seconds`, `x-daily-reset-seconds`, `retry-after`, `ratelimit-policy`, and `ratelimit`.
On a `429`, `$result->{rate_limit}{'retry-after'}` holds the wait in seconds. Retry as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
Call every method on the `Tomba::Finder` object. Each method is also available on its own module, for example `Tomba::Domain->new($key, $secret)->DomainSearch(...)`. `$opts` is an optional hash reference.
| Method | Endpoint |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `Account()` | [`GET /me`](/api/account#get-account) |
| `Usage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs($params)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `DomainSearch($domain, $opts)`, with `enrich_mobile` and `webhook_url` in `$opts` | [`GET /domain-search`](/api/finder#domain-search) |
| `EmailFinder($domain, $first_name, $last_name, $opts)`, with `webhook_url` in `$opts` | [`GET /email-finder`](/api/finder#email-finder) |
| `AuthorFinder($url, $opts)` | [`GET /author-finder`](/api/finder#author-finder) |
| `LinkedinFinder($url, $opts)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Enrichment($email, $opts)` | [`GET /enrich`](/api/finder#email-enrichment) |
| `EmailFormat($domain)` | [`GET /email-format`](/api/finder#email-format) |
| `GetLocation($domain)` | [`GET /location`](/api/finder#location) |
| `Count($domain)` | [`GET /email-count`](/api/finder#email-count) |
| `EmailVerifier($email, $opts)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `EmailSources($email)` | [`GET /email-sources`](/api/email#email-sources) |
| `PersonFind($email)` | [`GET /people/find`](/api/enrichment#person-api) |
| `CompanyFind($domain)` | [`GET /companies/find`](/api/enrichment#company-api) |
| `CombinedFind($email)` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `PhoneFinder($params)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `PhoneValidator($phone, $country_code)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status($domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `Autocomplete($search)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Similar($domain)` | [`GET /similar`](/api/domain#similar) |
| `Technology($domain)` | [`GET /technology`](/api/domain#technology) |
| `CompaniesSearch($body)` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `ListFlags($params)` | [`GET /flag`](/api/flag#list-my-flags) |
| `CreateFlag($body)` | [`POST /flag`](/api/flag#flag-incorrect-data) |
| `ListKeys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `GetKey($key_id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `CreateKey()` | [`POST /keys`](/api/keys#create-an-api-key) |
| `ResetKey($key_id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `DeleteKey($key_id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `ListLeads($params)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `GetLead($lead_id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `CreateLead($body)` | [`POST /leads`](/api/leads#create-a-lead) |
| `UpdateLead($lead_id, $body)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `DeleteLead($lead_id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `ListLeadsLists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `GetLeadsList($list_id)` | [`GET /leads_lists/{id}`](/api/lead-lists#retrieve-a-single-leads-list) |
| `CreateLeadsList($body)` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `UpdateLeadsList($list_id, $body)` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `DeleteLeadsList($list_id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `ListAttributes()` | [`GET /attributes`](/api/lead-attributes#retrieve-lead-attributes) |
| `GetAttribute($attribute_id)` | [`GET /attributes/{id}`](/api/lead-attributes#retrieve-a-single-lead-attribute) |
| `CreateAttribute($body)` | [`POST /attributes`](/api/lead-attributes#create-a-lead-attribute) |
| `UpdateAttribute($attribute_id, $body)` | [`PUT /attributes/{id}`](/api/lead-attributes#update-a-lead-attribute) |
| `DeleteAttribute($attribute_id)` | [`DELETE /attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute) |
| `ListBulks($type, $params)` | [`GET /bulk/{type}`](/api/bulks) |
| `CreateBulk($type, $body)` | [`POST /bulk/{type}`](/api/bulks) |
| `GetBulk($type, $id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `LaunchBulk($type, $id)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `BulkProgress($type, $id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `BulkDownload($type, $id)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `RenameBulk($type, $id, $name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `ArchiveBulk($type, $id)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `DeleteBulk($type, $id)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`$type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types); the order of calls is described in [Lifecycle](/bulks#lifecycle). `CreateBulk` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs.
## Source
[github.com/tomba-io/perl](https://github.com/tomba-io/perl) (Apache-2.0). Release notes are in [Changes](https://github.com/tomba-io/perl/blob/master/Changes).
---
## Document: Node.js SDK
Install and use tomba, the official Node.js client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/node-email-finder
# Node.js SDK
`tomba` is the official Node.js client for the [Tomba API](/introduction). Each API area is a service class, and each method returns a promise for one request.
## Requirements
- Node.js. `package.json` declares no minimum version; the SDK is built and tested on Node.js 20.
- [`axios`](https://www.npmjs.com/package/axios), installed as a dependency.
## Install
```bash
npm install tomba
```
The package is CommonJS with TypeScript declarations. The examples on this page are ES modules: save them as `.mjs` files, or set `"type": "module"` in your `package.json`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `setKey()` and `setSecret()`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```javascript
import { TombaClient, Finder } from "tomba";
const client = new TombaClient()
.setKey(process.env.TOMBA_API_KEY)
.setSecret(process.env.TOMBA_SECRET_KEY);
const result = await new Finder(client).emailFinder({
domain: "stripe.com",
firstName: "Jane",
lastName: "Doe",
});
console.log(result.data.data.email);
```
Every method resolves to `{ data, rateLimit }`, where `data` is the decoded JSON body. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result.data.data.email`. The TypeScript declarations type `result.data` as the payload itself, so TypeScript code needs a cast to read `result.data.data`.
## Errors
Methods reject with `TombaException`, a subclass of `Error`, when the API responds with status 400 or higher, and before sending a request when a required argument is missing.
| Property | Value |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message` | The API's `errors.message`. |
| `code` | The axios response object: `code.status` is the HTTP status and `code.headers` holds the response headers. `undefined` when no response was received. |
| `response` | The decoded error body, `{ errors: { type, message, code } }`. |
```javascript
import { TombaClient, Finder, TombaException } from "tomba";
const client = new TombaClient()
.setKey(process.env.TOMBA_API_KEY)
.setSecret(process.env.TOMBA_SECRET_KEY);
try {
const result = await new Finder(client).emailFinder({
domain: "stripe.com",
firstName: "Jane",
lastName: "Doe",
});
console.log(result.data.data.email);
} catch (error) {
if (!(error instanceof TombaException)) throw error;
console.log(
error.code?.status,
error.message,
error.response?.errors?.type,
);
}
```
`Enrichment.person()`, `company()`, and `combined()` don't reject when nothing is found: those endpoints answer with status 200 and an `errors` body, so check `result.data.errors`. Each `errors.type` is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `result.rateLimit` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) as `secondLimit`, `minuteLimit`, `dailyLimit`, `minuteRemaining`, `dailyRemaining`, `minuteReset`, `dailyReset`, `retryAfter`, `policy`, and `rateLimit`. Numeric headers are parsed as integers, and a header that is absent or `0` becomes `null`.
A `429` rejects with `TombaException`; read the wait time from `error.code.headers["retry-after"]` and retry as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds. Change the timeout with `client.setTimeout(milliseconds)`.
## Method reference
Create each service with the client, for example `new Finder(client)`. Every class is a named export of `tomba`.
| Method | Endpoint |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `Account.getAccount()` | [`GET /me`](/api/account#get-account) |
| `Usage.getUsage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs.getLogs({ page, limit })` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Domain.domainSearch({ domain, company, page, limit, country, department, enrich_mobile, webhook_url })` | [`GET /domain-search`](/api/finder#domain-search) |
| `Finder.emailFinder({ domain, company, firstName, lastName, fullName, enrichMobile, webhook_url })` | [`GET /email-finder`](/api/finder#email-finder) |
| `Finder.authorFinder(url, webhook_url)` | [`GET /author-finder`](/api/finder#author-finder) |
| `Finder.linkedinFinder(url, enrich_mobile, full, webhook_url)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Finder.emailEnrichment(email, enrich_mobile, webhook_url)` | [`GET /enrich`](/api/finder#email-enrichment) |
| `Format.emailFormat(domain)` | [`GET /email-format`](/api/finder#email-format) |
| `Location.getLocation(domain)` | [`GET /location`](/api/finder#location) |
| `Count.emailCount(domain)` | [`GET /email-count`](/api/finder#email-count) |
| `Verifier.emailVerifier(email, enrich_mobile, webhook_url)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources.emailSources(email)` | [`GET /email-sources`](/api/email#email-sources) |
| `Enrichment.person(email)` | [`GET /people/find`](/api/enrichment#person-api) |
| `Enrichment.company(domain)` | [`GET /companies/find`](/api/enrichment#company-api) |
| `Enrichment.combined(email)` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `Phone.finder({ email, domain, linkedin, full, webhook_url })` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Phone.validator(phone, country_code)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status.domainStatus(domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `Status.autoComplete(query)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Similar.websites(domain)` | [`GET /similar`](/api/domain#similar) |
| `Technology.list(domain)` | [`GET /technology`](/api/domain#technology) |
| `Reveal.companiesSearch({ query, filters, page })` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `Flag.listFlags({ page, limit })` | [`GET /flag`](/api/flag#list-my-flags) |
| `Flag.createFlag({ flag_type, value, reason, comment })` | [`POST /flag`](/api/flag#flag-incorrect-data) |
| `Keys.getKeys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Keys.getKey(id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `Keys.createKey()` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Keys.resetKey(id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Keys.deleteKey(id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Leads.listLeads({ page, limit, domain })` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Leads.getLead(id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Leads.createLead(data)` | [`POST /leads`](/api/leads#create-a-lead) |
| `Leads.updateLead(id, data)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Leads.deleteLead(id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `LeadsLists.getLists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `LeadsLists.getList(id)` | [`GET /leads_lists/{id}`](/api/lead-lists#retrieve-a-single-leads-list) |
| `LeadsLists.createList({ name })` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `LeadsLists.updateList(id, { name })` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `LeadsLists.deleteList(id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `LeadsAttributes.getLeadAttributes()` | [`GET /attributes`](/api/lead-attributes#retrieve-lead-attributes) |
| `LeadsAttributes.getLeadAttribute(id)` | [`GET /attributes/{id}`](/api/lead-attributes#retrieve-a-single-lead-attribute) |
| `LeadsAttributes.createLeadAttribute({ name, type })` | [`POST /attributes`](/api/lead-attributes#create-a-lead-attribute) |
| `LeadsAttributes.updateLeadAttribute(id, { name, type })` | [`PUT /attributes/{id}`](/api/lead-attributes#update-a-lead-attribute) |
| `LeadsAttributes.deleteLeadAttribute(id)` | [`DELETE /attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute) |
| `Bulk.list(type, { page, limit })` | [`GET /bulk/{type}`](/api/bulks) |
| `Bulk.create(type, data)` | [`POST /bulk/{type}`](/api/bulks) |
| `Bulk.get(type, id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `Bulk.launch(type, id)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `Bulk.progress(type, id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `Bulk.download(type, id)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `Bulk.rename(type, id, name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `Bulk.archive(type, id)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `Bulk.delete(type, id)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types); the order of calls is described in [Lifecycle](/bulks#lifecycle). `Bulk.create()` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs. `Bulk.download()` returns the results file, a CSV, as a string in `result.data`.
## Source
[github.com/tomba-io/node](https://github.com/tomba-io/node) (Apache-2.0). Release notes are in [CHANGELOG.md](https://github.com/tomba-io/node/blob/master/CHANGELOG.md).
---
## Document: Lua SDK
Install and use the tomba rock, the official Lua client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/lua-email-finder
# Lua SDK
The `tomba` rock is the official Lua client for the [Tomba API](/introduction). All methods are on one client object, and each method sends one request.
## Requirements
- Lua 5.1, 5.2, or 5.3. The rockspec allows Lua 5.1 or later, but the `xml` rock that `lua-requests` depends on requires a version earlier than 5.4.
- [LuaRocks](https://luarocks.org/) and a C++ compiler, to build the dependencies `lua-requests` and `lua-cjson`.
## Install
```bash
luarocks install xml CFLAGS="-O2 -fPIC -std=gnu++14"
luarocks install tomba
```
The first command builds the `xml` rock in C++14 mode. Without it, `luarocks install tomba` fails on compilers that default to C++17, such as GCC 11 and later, because `xml` doesn't compile as C++17.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `Tomba:init(key, secret)`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. `init` raises an error if either value is shorter than 39 characters. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```lua
local Tomba = require("tomba")
local tomba = Tomba:init(os.getenv("TOMBA_API_KEY"), os.getenv("TOMBA_SECRET_KEY"))
local result = tomba:email_finder("stripe.com", "Jane", "Doe")
print(result.data.data.email)
```
Every method returns a table with two fields: `data`, the decoded JSON body, and `rate_limit`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result.data.data.email`. JSON `null` values decode to `cjson.null`.
## Errors
How a failure surfaces depends on the HTTP method:
- `GET` methods raise a Lua error when the status isn't 200. The error message is the raw response body, prefixed with the SDK's file and line number.
- `POST`, `PUT`, and `DELETE` methods don't raise on API errors. They return the result table for any status, so check `result.data.errors`.
Catch `GET` errors with `pcall` and decode the body:
```lua
local Tomba = require("tomba")
local cjson = require("cjson")
local tomba = Tomba:init(os.getenv("TOMBA_API_KEY"), os.getenv("TOMBA_SECRET_KEY"))
local ok, result = pcall(tomba.email_finder, tomba, "stripe.com", "Jane", "Doe")
if ok then
print(result.data.data.email)
else
-- The error message is the response body, prefixed with the SDK's file and line.
local decoded, body = pcall(cjson.decode, tostring(result):match("{.*") or "")
if decoded and type(body) == "table" and body.errors then
print(body.errors.code, body.errors.type, body.errors.message)
else
print(result)
end
end
```
`errors.code` is the HTTP status. Each `errors.type` is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `result.rate_limit` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) that are present, keyed by header name, with string values: `x-second-rate-limit`, `x-minute-rate-limit`, `x-daily-rate-limit`, `x-minute-request-left`, `x-daily-request-left`, `x-minute-reset-seconds`, `x-daily-reset-seconds`, `retry-after`, `ratelimit-policy`, and `ratelimit`.
A `429` on a `GET` method raises an error without the response headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
Call every method on the client with a colon, for example `tomba:email_finder(...)`. `opts` is an optional table.
| Method | Endpoint |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `account()` | [`GET /me`](/api/account#get-account) |
| `usage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `logs(params)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `domain_search(domain, opts)`, with `opts.enrich_mobile` and `opts.webhook_url` | [`GET /domain-search`](/api/finder#domain-search) |
| `email_finder(domain, first_name, last_name, opts)`, with `opts.webhook_url` | [`GET /email-finder`](/api/finder#email-finder) |
| `author_finder(url, opts)` | [`GET /author-finder`](/api/finder#author-finder) |
| `linkedin_finder(url, opts)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `enrichment(email, opts)` | [`GET /enrich`](/api/finder#email-enrichment) |
| `email_format(domain)` | [`GET /email-format`](/api/finder#email-format) |
| `get_location(domain)` | [`GET /location`](/api/finder#location) |
| `count(domain)` | [`GET /email-count`](/api/finder#email-count) |
| `email_verifier(email, opts)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `email_sources(email)` | [`GET /email-sources`](/api/email#email-sources) |
| `person_find(email)` | [`GET /people/find`](/api/enrichment#person-api) |
| `company_find(domain)` | [`GET /companies/find`](/api/enrichment#company-api) |
| `combined_find(email)` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `phone_finder(params)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `phone_validator(phone, country_code)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `status(domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `autocomplete(search)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `similar(domain)` | [`GET /similar`](/api/domain#similar) |
| `technology(domain)` | [`GET /technology`](/api/domain#technology) |
| `companies_search(body)` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `list_flags(params)` | [`GET /flag`](/api/flag#list-my-flags) |
| `create_flag(body)` | [`POST /flag`](/api/flag#flag-incorrect-data) |
| `list_keys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `get_key(key_id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `create_key()` | [`POST /keys`](/api/keys#create-an-api-key) |
| `reset_key(key_id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `delete_key(key_id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `list_leads(params)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `get_lead(lead_id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `create_lead(body)` | [`POST /leads`](/api/leads#create-a-lead) |
| `update_lead(lead_id, body)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `delete_lead(lead_id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `list_leads_lists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `get_leads_list(list_id)` | [`GET /leads_lists/{id}`](/api/lead-lists#retrieve-a-single-leads-list) |
| `create_leads_list(body)` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `update_leads_list(list_id, body)` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `delete_leads_list(list_id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `list_attributes()` | [`GET /attributes`](/api/lead-attributes#retrieve-lead-attributes) |
| `get_attribute(attribute_id)` | [`GET /attributes/{id}`](/api/lead-attributes#retrieve-a-single-lead-attribute) |
| `create_attribute(body)` | [`POST /attributes`](/api/lead-attributes#create-a-lead-attribute) |
| `update_attribute(attribute_id, body)` | [`PUT /attributes/{id}`](/api/lead-attributes#update-a-lead-attribute) |
| `delete_attribute(attribute_id)` | [`DELETE /attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute) |
| `list_bulks(bulk_type, params)` | [`GET /bulk/{type}`](/api/bulks) |
| `create_bulk(bulk_type, data)` | [`POST /bulk/{type}`](/api/bulks) |
| `get_bulk(bulk_type, id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `launch_bulk(bulk_type, id)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `bulk_progress(bulk_type, id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `bulk_download(bulk_type, id)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `rename_bulk(bulk_type, id, name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `archive_bulk(bulk_type, id)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `delete_bulk(bulk_type, id)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`bulk_type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types); the order of calls is described in [Lifecycle](/bulks#lifecycle). `create_bulk` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs.
## Source
[github.com/tomba-io/lua](https://github.com/tomba-io/lua), licensed Apache-2.0 in its rockspec.
---
## Document: Go SDK
Install and use github.com/tomba-io/go, the official Go client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/go-email-finder
# Go SDK
`github.com/tomba-io/go` is the official Go client for the [Tomba API](/introduction). All methods hang off one client value, and each method sends one request.
## Requirements
Go 1.18 or later, as declared in `go.mod`. The module has no third-party dependencies.
## Install
```bash
go get github.com/tomba-io/go
```
Import the `github.com/tomba-io/go/tomba` package.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `tomba.New(key, secret)`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```go
package main
import (
"fmt"
"log"
"os"
"github.com/tomba-io/go/tomba"
)
func main() {
client := tomba.New(os.Getenv("TOMBA_API_KEY"), os.Getenv("TOMBA_SECRET_KEY"))
result, err := client.EmailFinder(tomba.Params{
"domain": "stripe.com",
"first_name": "Jane",
"last_name": "Doe",
})
if err != nil {
log.Fatal(err)
}
if data, ok := result.Data.(map[string]interface{}); ok {
fmt.Println(data["email"])
}
}
```
Most methods decode the response body into a type from the `github.com/tomba-io/go/tomba/models` package; the keys, leads, lists, attributes, and flag methods return the raw body as `json.RawMessage`. `models.Finder.Data` is an `interface{}`, so assert it to a map before reading fields.
`tomba.Params` is a `map[string]interface{}`. On `GET` requests only `string`, `int`, and `bool` values are sent, and empty strings and zero integers are skipped.
## Errors
Every method returns an `error` when the request fails or the API responds with a status other than 200 or 201. The error text holds only the HTTP status, for example `error: 400 Bad Request, status code: 400`. The SDK discards the response body, so the API's `errors.type` and `errors.message` aren't available; the causes behind each status are listed in [Errors](/error-handling#status-codes).
```go
package main
import (
"fmt"
"log"
"os"
"github.com/tomba-io/go/tomba"
)
func main() {
client := tomba.New(os.Getenv("TOMBA_API_KEY"), os.Getenv("TOMBA_SECRET_KEY"))
result, err := client.EmailFinder(tomba.Params{
"domain": "stripe.com",
"first_name": "Jane",
"last_name": "Doe",
})
if err != nil {
if wait := client.LastRateLimit.RetryAfter; wait > 0 {
log.Fatalf("rate limited, retry in %d seconds", wait)
}
log.Fatal(err) // for example "error: 400 Bad Request, status code: 400"
}
fmt.Println(result.Data)
}
```
`PersonFind`, `CompanyFind`, and `CombinedFind` don't return an error when nothing is found: those endpoints answer with status 200 and an `errors` body, which doesn't fit the result model, so the model comes back empty.
## Rate limits and retries
The SDK doesn't retry. After every response, including failed ones, the client stores the [rate limit headers](/rate-limits#rate-limit-headers) in `client.LastRateLimit`, a `tomba.RateLimit` with the fields `SecondLimit`, `MinuteLimit`, `DailyLimit`, `MinuteRemaining`, `DailyRemaining`, `MinuteReset`, `DailyReset`, `RetryAfter`, `Policy`, and `RateLimit`. A header that is absent reads as `0`. Each call overwrites the field, so read it right after the call, and don't rely on it when one client sends requests from several goroutines. `tomba.ParseRateLimit(*http.Response)` parses the same headers from any response.
`RetryAfter` is set only on a `429`. Wait at least that long, as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
All methods are on `*tomba.Tomba`, the value returned by `tomba.New`. Result types are in the `models` package.
| Method | Endpoint |
| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `Account() (models.Account, error)` | [`GET /me`](/api/account#get-account) |
| `Logs(params ...Params) (models.Logs, error)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `DomainSearch(params Params) (models.Search, error)` | [`GET /domain-search`](/api/finder#domain-search) |
| `EmailFinder(params Params) (models.Finder, error)` | [`GET /email-finder`](/api/finder#email-finder) |
| `AuthorFinder(params Params) (models.Finder, error)` | [`GET /author-finder`](/api/finder#author-finder) |
| `LinkedinFinder(params Params) (models.Finder, error)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Enrichment(params Params) (models.Finder, error)` | [`GET /enrich`](/api/finder#email-enrichment) |
| `EmailFormat(domain string) (models.Format, error)` | [`GET /email-format`](/api/finder#email-format) |
| `EmployeesCount(domain string) (models.Employees, error)` | [`GET /location`](/api/finder#location) |
| `Count(domain string) (models.Count, error)` | [`GET /email-count`](/api/finder#email-count) |
| `EmailVerifier(params Params) (models.Verifier, error)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources(email string) (models.Source, error)` | [`GET /email-sources`](/api/email#email-sources) |
| `PersonFind(params Params) (models.PersonResponse, error)` | [`GET /people/find`](/api/enrichment#person-api) |
| `CompanyFind(params Params) (models.CompanyResponse, error)` | [`GET /companies/find`](/api/enrichment#company-api) |
| `CombinedFind(params Params) (models.CombinedResponse, error)` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `PhoneFinder(params Params) (models.Phone, error)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `PhoneValidator(params Params) (models.PhoneValidator, error)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status(domain string) (models.Status, error)` | [`GET /domain-status`](/api/domain#domain-status) |
| `AutoComplete(query string) (models.DomainSuggestions, error)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `SimilarDomains(domain string) (models.Similar, error)` | [`GET /similar`](/api/domain#similar) |
| `TechnologyCheck(domain string) (models.Technology, error)` | [`GET /technology`](/api/domain#technology) |
| `SearchCompanies(request *models.RevealSearchRequest) (models.RevealSearchResponse, error)` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `ListFlags(params Params) (json.RawMessage, error)` | [`GET /flag`](/api/flag#list-my-flags) |
| `CreateFlag(params Params) (json.RawMessage, error)` | [`POST /flag`](/api/flag#flag-incorrect-data) |
| `ListKeys() (json.RawMessage, error)` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `GetKey(id string) (json.RawMessage, error)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `CreateKey(params Params) (json.RawMessage, error)` | [`POST /keys`](/api/keys#create-an-api-key) |
| `DeleteKey(id string) (json.RawMessage, error)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `ListLeads(params Params) (json.RawMessage, error)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `GetLead(id string) (json.RawMessage, error)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `CreateLead(params Params) (json.RawMessage, error)` | [`POST /leads`](/api/leads#create-a-lead) |
| `UpdateLead(id string, params Params) (json.RawMessage, error)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `DeleteLead(id string) (json.RawMessage, error)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `ListLeadsLists(params Params) (json.RawMessage, error)` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `GetLeadsList(id string) (json.RawMessage, error)` | [`GET /leads_lists/{id}`](/api/lead-lists#retrieve-a-single-leads-list) |
| `CreateLeadsList(params Params) (json.RawMessage, error)` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `UpdateLeadsList(id string, params Params) (json.RawMessage, error)` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `DeleteLeadsList(id string) (json.RawMessage, error)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `ListAttributes(params Params) (json.RawMessage, error)` | [`GET /attributes`](/api/lead-attributes#retrieve-lead-attributes) |
| `GetAttribute(id string) (json.RawMessage, error)` | [`GET /attributes/{id}`](/api/lead-attributes#retrieve-a-single-lead-attribute) |
| `CreateAttribute(params Params) (json.RawMessage, error)` | [`POST /attributes`](/api/lead-attributes#create-a-lead-attribute) |
| `UpdateAttribute(id string, params Params) (json.RawMessage, error)` | [`PUT /attributes/{id}`](/api/lead-attributes#update-a-lead-attribute) |
| `DeleteAttribute(id string) (json.RawMessage, error)` | [`DELETE /attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute) |
| `GetAllBulks(bulkType models.BulkType, params *models.BulkGetParams) (*models.BulkListResponse, error)` | [`GET /bulk/{type}`](/api/bulks) |
| `CreateBulk(bulkType models.BulkType, params *models.BulkCreateParams) (*models.BulkCreateResponse, error)` | [`POST /bulk/{type}`](/api/bulks) |
| `CreateSearchBulk(params *models.BulkSearchCreateParams) (*models.BulkCreateResponse, error)` | [`POST /bulk/search`](/api/bulks#create-a-bulk-job) |
| `GetBulk(bulkType models.BulkType, id int64) (*models.BulkDetailResponse, error)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `LaunchBulk(bulkType models.BulkType, id int64) (*models.BulkSuccessResponse, error)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `GetBulkProgress(bulkType models.BulkType, id int64) (*models.BulkProgress, error)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `DownloadBulk(bulkType models.BulkType, id int64, params *models.BulkDownloadParams) ([]byte, error)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `SaveBulkResults(bulkType models.BulkType, id int64, filePath string, downloadType string) error` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `RenameBulk(bulkType models.BulkType, id int64, params *models.BulkRenameParams) (*models.BulkSuccessResponse, error)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `ArchiveBulk(bulkType models.BulkType, id int64) (*models.BulkSuccessResponse, error)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `DeleteBulk(bulkType models.BulkType, id int64) (*models.BulkSuccessResponse, error)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`models.BulkType` has one constant per [bulk type](/bulks#bulk-types), from `models.BulkTypeSearch` to `models.BulkTypePhoneValidator`; the order of calls is described in [Lifecycle](/bulks#lifecycle). `GetAllSearchBulks`, `GetAllFinderBulks`, and the other `GetAll…Bulks(params)` methods call `GetAllBulks` for one type. `SaveBulkResults` downloads the results and writes them to `filePath`.
## Source
[github.com/tomba-io/go](https://github.com/tomba-io/go) (Apache-2.0). Release notes are in [CHANGELOG.md](https://github.com/tomba-io/go/blob/master/CHANGELOG.md).
---
## Document: Elixir SDK
Install and use the tomba Hex package, the official Elixir client for the Tomba API: authentication, errors, rate limit data, and every function.
URL: /libraries/elixir-email-finder
# Elixir SDK
The `tomba` Hex package is the official Elixir client for the [Tomba API](/introduction). Each API area is a module, each function sends one request, and every function returns `{:ok, result}` or `{:error, %Tomba.Error{}}`.
## Requirements
- Elixir 1.12 or later, as declared in `mix.exs`.
- [`httpoison`](https://hex.pm/packages/httpoison) and [`jason`](https://hex.pm/packages/jason), installed as dependencies.
## Install
Add the package to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:tomba, "~> 1.0"}
]
end
```
Then run `mix deps.get`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `Tomba.client/3`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. The optional third argument takes `:timeout` in milliseconds (default `120_000`) and `:base_url`. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```elixir
client = Tomba.client(System.fetch_env!("TOMBA_API_KEY"), System.fetch_env!("TOMBA_SECRET_KEY"))
{:ok, result} =
Tomba.Finder.email_finder(client, %{
"domain" => "stripe.com",
"first_name" => "Jane",
"last_name" => "Doe"
})
IO.puts(result.data["data"]["email"])
```
Run a script like this one in your project with `mix run first.exs`. Functions take their parameters as a map with string keys, named as in the [API reference](/api). `result` is a map with two keys: `:data`, the decoded JSON body, and `:rate_limit`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result.data["data"]["email"]`.
## Errors
Functions return `{:error, %Tomba.Error{}}`, rather than raising, when the API responds with a status outside 200 to 299 or the request fails.
| Field | Value |
| ------------- | ------------------------------------------------------------------------------------------------------------------ |
| `status_code` | The HTTP status. `0` when no response was received. |
| `message` | The API's `errors.message`, or the transport error. |
| `response` | `%{data: body, rate_limit: headers}`, where `body` is the decoded error body. `nil` when no response was received. |
```elixir
client = Tomba.client(System.fetch_env!("TOMBA_API_KEY"), System.fetch_env!("TOMBA_SECRET_KEY"))
params = %{"domain" => "stripe.com", "first_name" => "Jane", "last_name" => "Doe"}
case Tomba.Finder.email_finder(client, params) do
{:ok, result} ->
IO.puts(result.data["data"]["email"])
{:error, %Tomba.Error{} = error} ->
IO.puts("#{error.status_code} #{error.message}")
IO.puts(get_in(error.response, [:data, "errors", "type"]))
end
```
A function called with a map that lacks a required key, such as `"domain"` for `email_finder`, raises `FunctionClauseError` without sending a request. `Tomba.Enrichment.person/2`, `company/2`, and `combined/2` return `{:ok, result}` when nothing is found: those endpoints answer with status 200 and an `errors` body, so check `result.data["errors"]`. Each `errors.type` is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `result.rate_limit` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) that are present, keyed by lowercase header name, with string values: `"x-second-rate-limit"`, `"x-minute-rate-limit"`, `"x-daily-rate-limit"`, `"x-minute-request-left"`, `"x-daily-request-left"`, `"x-minute-reset-seconds"`, `"x-daily-reset-seconds"`, `"retry-after"`, `"ratelimit-policy"`, and `"ratelimit"`.
On a `429`, read the wait time from `error.response.rate_limit["retry-after"]` and retry as described in [Handle 429 responses](/rate-limits#handle-429-responses).
## Method reference
Every function takes the client as its first argument. Map arguments list the keys the function requires.
| Function | Endpoint |
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `Tomba.Account.account(client)` | [`GET /me`](/api/account#get-account) |
| `Tomba.Usage.get_usage(client)` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Tomba.Logs.get_logs(client, params \\ %{})` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Tomba.Domain.domain_search(client, %{"domain" => _})` | [`GET /domain-search`](/api/finder#domain-search) |
| `Tomba.Finder.email_finder(client, %{"domain" => _, "first_name" => _, "last_name" => _})` | [`GET /email-finder`](/api/finder#email-finder) |
| `Tomba.Finder.author_finder(client, %{"url" => _})` | [`GET /author-finder`](/api/finder#author-finder) |
| `Tomba.Finder.linkedin_finder(client, %{"url" => _})` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Tomba.Finder.email_enrichment(client, %{"email" => _})` | [`GET /enrich`](/api/finder#email-enrichment) |
| `Tomba.Format.email_format(client, %{"domain" => _})` | [`GET /email-format`](/api/finder#email-format) |
| `Tomba.Location.get_location(client, %{"domain" => _})` | [`GET /location`](/api/finder#location) |
| `Tomba.Count.email_count(client, %{"domain" => _})` | [`GET /email-count`](/api/finder#email-count) |
| `Tomba.Verifier.email_verifier(client, %{"email" => _})` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Tomba.Sources.email_sources(client, %{"email" => _})` | [`GET /email-sources`](/api/email#email-sources) |
| `Tomba.Enrichment.person(client, %{"email" => _})` | [`GET /people/find`](/api/enrichment#person-api) |
| `Tomba.Enrichment.company(client, %{"domain" => _})` | [`GET /companies/find`](/api/enrichment#company-api) |
| `Tomba.Enrichment.combined(client, %{"email" => _})` | [`GET /combined/find`](/api/enrichment#combined-api) |
| `Tomba.Phone.phone_finder(client, %{"email" => _})` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Tomba.Phone.phone_validator(client, %{"phone" => _})` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Tomba.Status.domain_status(client, %{"domain" => _})` | [`GET /domain-status`](/api/domain#domain-status) |
| `Tomba.Status.domain_suggestions(client, %{"query" => _})` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Tomba.Similar.websites(client, %{"domain" => _})` | [`GET /similar`](/api/domain#similar) |
| `Tomba.Technology.list(client, %{"domain" => _})` | [`GET /technology`](/api/domain#technology) |
| `Tomba.Reveal.companies_search(client, params \\ %{})` | [`POST /reveal/search`](/api/reveal#search-companies) |
| `Tomba.Flag.list_flags(client, params \\ %{})` | [`GET /flag`](/api/flag#list-my-flags) |
| `Tomba.Flag.create_flag(client, %{"email" => _})` | [`POST /flag`](/api/flag#flag-incorrect-data) |
| `Tomba.Keys.list_keys(client)` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Tomba.Keys.get_key(client, id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `Tomba.Keys.create_key(client)` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Tomba.Keys.reset_key(client, id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Tomba.Keys.delete_key(client, id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Tomba.Leads.list_leads(client, params \\ %{})` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Tomba.Leads.get_lead(client, lead_id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Tomba.Leads.create_lead(client, params)` | [`POST /leads`](/api/leads#create-a-lead) |
| `Tomba.Leads.update_lead(client, lead_id, params)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Tomba.Leads.delete_lead(client, lead_id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `Tomba.LeadsLists.list(client)` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `Tomba.LeadsLists.get(client, id)` | [`GET /leads_lists/{id}`](/api/lead-lists#retrieve-a-single-leads-list) |
| `Tomba.LeadsLists.create(client, params)` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `Tomba.LeadsLists.update(client, id, params)` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `Tomba.LeadsLists.delete(client, id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `Tomba.Bulk.list(client, bulk_type, params \\ %{})` | [`GET /bulk/{type}`](/api/bulks) |
| `Tomba.Bulk.create(client, bulk_type, params)` | [`POST /bulk/{type}`](/api/bulks) |
| `Tomba.Bulk.get(client, bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `Tomba.Bulk.launch(client, bulk_type, bulk_id)` | [`PUT /bulk/{type}/{id}`](/api/bulks) |
| `Tomba.Bulk.progress(client, bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `Tomba.Bulk.download(client, bulk_type, bulk_id)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `Tomba.Bulk.rename(client, bulk_type, bulk_id, name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
| `Tomba.Bulk.archive(client, bulk_type, bulk_id)` | [`DELETE /bulk/{type}/{id}/archive`](/api/bulks) |
| `Tomba.Bulk.delete(client, bulk_type, bulk_id)` | [`DELETE /bulk/{type}/{id}/delete`](/api/bulks) |
`bulk_type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types) (`Tomba.Bulk.valid_bulk_types/0` returns them); the order of calls is described in [Lifecycle](/bulks#lifecycle). `Tomba.Bulk.create/3` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs. `Tomba.Bulk.download/3` returns the results file, a CSV, as a string in `result.data`.
## Source
[github.com/tomba-io/elixir](https://github.com/tomba-io/elixir) (Apache-2.0).
---
## Document: Deno SDK
Install and use @tomba/sdk, the official Deno client for the Tomba API on JSR: authentication, errors, rate limit data, and every method.
URL: /libraries/deno-email-finder
# Deno SDK
`@tomba/sdk` is the official Deno client for the [Tomba API](/introduction), published on JSR. Each API area is a service class, and each method returns a promise for one request. This page describes version 1.0.4, the current release on JSR.
## Requirements
Deno 2. `deno.json` declares no minimum version; the SDK is linted and tested on Deno 2.x. Run your program with `--allow-net`, and with `--allow-env` if it reads your keys from the environment.
## Install
```bash
deno add jsr:@tomba/sdk
```
This adds `@tomba/sdk` to the imports in your `deno.json`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `setKey()` and `setSecret()`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```typescript
import { Client, Finder } from "@tomba/sdk";
const client = new Client()
.setKey(Deno.env.get("TOMBA_API_KEY")!)
.setSecret(Deno.env.get("TOMBA_SECRET_KEY")!);
const result = await new Finder(client).emailFinder(
"stripe.com",
"Jane",
"Doe",
);
const body = result.data as { data: { email: string | null } };
console.log(body.data.email);
```
Every method resolves to `{ data, rateLimit }`, where `data` is the decoded JSON body, typed `unknown`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `data.data.email`.
## Errors
The SDK throws `TombaException` when the API responds with status 400 or higher, when the request fails, and before sending a request when a required argument is missing. `TombaException` doesn't extend `Error`, and for API errors its `message` is `undefined` and its `code` is `0`: read the details from `response`, the decoded error body.
```typescript
import { Client, Finder, TombaException } from "@tomba/sdk";
type ErrorBody = { errors: { type: string; message: string; code: number } };
const client = new Client()
.setKey(Deno.env.get("TOMBA_API_KEY")!)
.setSecret(Deno.env.get("TOMBA_SECRET_KEY")!);
try {
const result = await new Finder(client).emailFinder(
"stripe.com",
"Jane",
"Doe",
);
console.log(result.data);
} catch (error) {
if (!(error instanceof TombaException)) throw error;
const { errors } = error.response as ErrorBody;
console.log(errors.code, errors.type, errors.message);
}
```
`errors.code` is the HTTP status. Each `errors.type` is described in [Errors](/error-handling#error-types). For a failed request or a missing argument, `response` is empty and `message` holds the cause.
## Rate limits and retries
The SDK doesn't retry. `result.rateLimit` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) as `string | null` fields: `xSecondRateLimit`, `xMinuteRateLimit`, `xDailyRateLimit`, `xMinuteRequestLeft`, `xDailyRequestLeft`, `xMinuteResetSeconds`, `xDailyResetSeconds`, `retryAfter`, `rateLimitPolicy`, and `rateLimit`.
A `429` throws `TombaException` without the response headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
Create each service with the client, for example `new Finder(client)`.
| Method | Endpoint |
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `Account.getAccount()` | [`GET /me`](/api/account#get-account) |
| `Usage.getUsage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs.getLogs(page?, limit?)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Domain.domainSearch(domain, page?, limit?, department?, enrichMobile?, webhookUrl?)` | [`GET /domain-search`](/api/finder#domain-search) |
| `Finder.emailFinder(domain, firstName, lastName, webhookUrl?)` | [`GET /email-finder`](/api/finder#email-finder) |
| `Finder.authorFinder(url, webhookUrl?)` | [`GET /author-finder`](/api/finder#author-finder) |
| `Finder.linkedinFinder(url, webhookUrl?)` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Count.emailCount(domain)` | [`GET /email-count`](/api/finder#email-count) |
| `Verifier.emailVerifier(email, webhookUrl?)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources.emailSources(email)` | [`GET /email-sources`](/api/email#email-sources) |
| `Finder.phoneFinder(email, webhookUrl?)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `PhoneFinder.finder(email, webhookUrl?)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `PhoneFinder.validator(phone)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status.domainStatus(domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `Status.autoComplete(query)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Similar.websites(domain)` | [`GET /similar`](/api/domain#similar) |
| `Technology.list(domain)` | [`GET /technology`](/api/domain#technology) |
| `Keys.getKeys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Keys.createKey()` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Keys.resetKey(id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Keys.deleteKey(id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Leads.listLeads(page?, limit?, listId?, domain?)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Leads.getLead(id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Leads.createLead(data)` | [`POST /leads`](/api/leads#create-a-lead) |
| `Leads.updateLead(id, data)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Leads.deleteLead(id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `LeadsLists.getLists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `LeadsLists.createList(name)` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `LeadsLists.updateListId(id, name)` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `LeadsLists.deleteListId(id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
## Source
[github.com/tomba-io/deno](https://github.com/tomba-io/deno) (Apache-2.0), published as [@tomba/sdk](https://jsr.io/@tomba/sdk) on JSR. Release notes are in [CHANGELOG.md](https://github.com/tomba-io/deno/blob/master/CHANGELOG.md).
---
## Document: Dart SDK
Install and use the tomba package, the official Dart client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/dart-email-finder
# Dart SDK
The `tomba` package is the official Dart client for the [Tomba API](/introduction). Each API area is a service class, and each method returns a `Future` for one request. This page describes version 1.0.3, the current release on pub.dev.
## Requirements
- Dart 3, as declared in `pubspec.yaml` (`sdk: '>=3.0.0 <4.0.0'`).
- The [`http`](https://pub.dev/packages/http) package, installed as a dependency.
## Install
```bash
dart pub add tomba
```
In a Flutter app, run `flutter pub add tomba`. Everything is exported from `package:tomba/tomba.dart`.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `setKey()` and `setSecret()`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Don't ship your secret in a mobile or web app; call Tomba from your server. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Find the email address of a person at a company:
```dart
import 'dart:io';
import 'package:tomba/tomba.dart';
Future main() async {
final client = Client()
.setKey(Platform.environment['TOMBA_API_KEY']!)
.setSecret(Platform.environment['TOMBA_SECRET_KEY']!);
final result = await Finder(client).emailFinder(
domain: 'stripe.com',
firstName: 'Jane',
lastName: 'Doe',
);
print(result.data['data']['email']);
}
```
Every method returns a `Response` with two fields: `data`, the decoded JSON body, and `rateLimit`. The API wraps its payload in its own `data` object ([Response format](/introduction#response-format)), so the address is at `result.data['data']['email']`.
## Errors
The SDK throws `TombaException` when the API responds with status 400 or higher and when the request fails.
| Field | Value |
| ---------- | -------------------------------------------------------------------------------------- |
| `code` | The HTTP status. `null` when the error body isn't JSON or no response was received. |
| `message` | The API's `errors.message`, or the underlying error. |
| `response` | The decoded error body, `{'errors': {'type': …, 'message': …, 'code': …}}`, or `null`. |
```dart
import 'dart:io';
import 'package:tomba/tomba.dart';
Future main() async {
final client = Client()
.setKey(Platform.environment['TOMBA_API_KEY']!)
.setSecret(Platform.environment['TOMBA_SECRET_KEY']!);
try {
final result = await Finder(client).emailFinder(
domain: 'stripe.com',
firstName: 'Jane',
lastName: 'Doe',
);
print(result.data['data']['email']);
} on TombaException catch (e) {
print('${e.code} ${e.message}');
print(e.response?['errors']?['type']);
}
}
```
Each `errors.type` is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `result.rateLimit` holds the response's [rate limit headers](/rate-limits#rate-limit-headers) as `String?` fields: `xSecondRateLimit`, `xMinuteRateLimit`, `xDailyRateLimit`, `xMinuteRequestLeft`, `xDailyRequestLeft`, `xMinuteResetSeconds`, `xDailyResetSeconds`, `retryAfter`, `rateLimitPolicy`, and `rateLimit`.
A `429` throws `TombaException` with `code` 429, without the response headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds; the timeout isn't configurable.
## Method reference
Create each service with the client, for example `Finder(client)`. All arguments are named.
| Method | Endpoint |
| ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `Account.getAccount()` | [`GET /me`](/api/account#get-account) |
| `Usage.getUsage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs.getLogs({int? page, int? limit})` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Domain.domainSearch({required String domain, int? page, int? limit, String? department, bool? enrichMobile, String? webhookUrl})` | [`GET /domain-search`](/api/finder#domain-search) |
| `Finder.emailFinder({required String domain, required String firstName, required String lastName, String? webhookUrl})` | [`GET /email-finder`](/api/finder#email-finder) |
| `Finder.authorFinder({required String url, String? webhookUrl})` | [`GET /author-finder`](/api/finder#author-finder) |
| `Finder.linkedinFinder({required String url, String? webhookUrl})` | [`GET /linkedin`](/api/finder#linkedin-finder) |
| `Count.emailCount({required String domain})` | [`GET /email-count`](/api/finder#email-count) |
| `Verifier.emailVerifier({required String email, String? webhookUrl})` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources.emailSources({required String email})` | [`GET /email-sources`](/api/email#email-sources) |
| `Finder.phoneFinder({required String email, String? webhookUrl})` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `PhoneFinder.finder({required String email, String? webhookUrl})` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `PhoneFinder.validator({required String phone})` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status.domainStatus({required String domain})` | [`GET /domain-status`](/api/domain#domain-status) |
| `Status.autoComplete({required String query})` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Similar.websites({required String domain})` | [`GET /similar`](/api/domain#similar) |
| `Technology.list({required String domain})` | [`GET /technology`](/api/domain#technology) |
| `Keys.getKeys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Keys.createKey()` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Keys.resetKey({required String id})` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Keys.deleteKey({required String id})` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Leads.listLeads({int? page, int? limit, String? listId, String? domain})` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Leads.getLead({required String id})` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Leads.createLead({required Map data})` | [`POST /leads`](/api/leads#create-a-lead) |
| `Leads.updateLead({required String id, required Map data})` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Leads.deleteLead({required String id})` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `LeadsLists.getLists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `LeadsLists.createList({required String name})` | [`POST /leads_lists`](/api/lead-lists#create-leads-list) |
| `LeadsLists.updateListId({required String id, required String name})` | [`PUT /leads_lists/{id}`](/api/lead-lists#update-leads-list) |
| `LeadsLists.deleteListId({required String id})` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
## Source
[github.com/tomba-io/dart](https://github.com/tomba-io/dart) (Apache-2.0). Release notes are in [CHANGELOG.md](https://github.com/tomba-io/dart/blob/master/CHANGELOG.md).
---
## Document: C# SDK
Install and use the Tomba NuGet package, the official .NET client for the Tomba API: authentication, errors, rate limit data, and every method.
URL: /libraries/csharp-email-finder
# C# SDK
The `Tomba` NuGet package is the official .NET client for the [Tomba API](/introduction). Each API area is a service class, and each method sends one request and returns the raw `HttpResponseMessage`.
## Requirements
- .NET 10. Version 1.0.1 of the package targets `net10.0` only.
- [`Newtonsoft.Json`](https://www.nuget.org/packages/Newtonsoft.Json), installed as a dependency.
## Install
```bash
dotnet add package Tomba
```
The classes are in the `Tomba` namespace.
## Authenticate
Create an API key and copy your secret on the [API keys](https://app.tomba.io/api) page, then export both:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
The SDK doesn't read environment variables. Pass the values to `SetKey()` and `SetSecret()`, and the client sends them as the `X-Tomba-Key` and `X-Tomba-Secret` headers. Call each setter once per client: a second call throws `ArgumentException`. Key expiry and rotation are covered in [Authentication](/authentication).
## First request
Verify an email address.
```csharp
using Newtonsoft.Json.Linq;
using Tomba;
var client = new Client()
.SetKey(Environment.GetEnvironmentVariable("TOMBA_API_KEY"))
.SetSecret(Environment.GetEnvironmentVariable("TOMBA_SECRET_KEY"));
var response = await new Verifier(client).EmailVerifier("jane.doe@stripe.com");
var body = JObject.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(body["data"]?["email"]?["status"]);
```
The example is a `dotnet new console` program with top-level statements. Methods return the `HttpResponseMessage` unparsed, so read and parse the JSON body yourself. The API wraps its payload in a `data` object ([Response format](/introduction#response-format)); the verification status is at `data.email.status`.
## Errors
The SDK throws `TombaException` when the API responds with status 400 or higher and when the request fails. For API errors, the exception is wrapped: the outer `TombaException` has the API's `errors.message` as `Message`, and its `InnerException` is a `TombaException` whose `Code` is the HTTP status and whose `Response` is the raw response body.
```csharp
using Newtonsoft.Json.Linq;
using Tomba;
var client = new Client()
.SetKey(Environment.GetEnvironmentVariable("TOMBA_API_KEY"))
.SetSecret(Environment.GetEnvironmentVariable("TOMBA_SECRET_KEY"));
try
{
var response = await new Verifier(client).EmailVerifier("jane.doe@stripe.com");
var body = JObject.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine(body["data"]?["email"]?["status"]);
}
catch (TombaException e)
{
var inner = e.InnerException as TombaException;
Console.WriteLine($"{inner?.Code} {e.Message}");
Console.WriteLine(inner?.Response);
}
```
`inner` is `null` when the request failed before a response arrived. The raw `Response` is the JSON error body; each `errors.type` in it is described in [Errors](/error-handling#error-types).
## Rate limits and retries
The SDK doesn't retry. `Client.ParseRateLimit(response)` reads the [rate limit headers](/rate-limits#rate-limit-headers) of a successful response into a `RateLimit` with the `int?` properties `SecondRateLimit`, `MinuteRateLimit`, `DailyRateLimit`, `MinuteRequestLeft`, `DailyRequestLeft`, `MinuteResetSeconds`, `DailyResetSeconds`, and `RetryAfter`, and the `string` properties `RateLimitPolicy` and `RateLimitValue`.
A `429` throws `TombaException` without the response headers. Wait before retrying as described in [Handle 429 responses](/rate-limits#handle-429-responses).
Requests time out after 120 seconds.
## Method reference
Create each service with the client, for example `new Verifier(client)`. Every method returns `Task`.
| Method | Endpoint |
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `Account.GetAccount()` | [`GET /me`](/api/account#get-account) |
| `Usage.GetUsage()` | [`GET /usage`](/api/account#retrieve-api-usage) |
| `Logs.GetLogs(int? page = null, int? limit = null)` | [`GET /logs`](/api/account#retrieve-api-logs) |
| `Format.EmailFormatAsync(string domain)` | [`GET /email-format`](/api/finder#email-format) |
| `Count.EmailCount(string domain)` | [`GET /email-count`](/api/finder#email-count) |
| `Verifier.EmailVerifier(string email, string webhookUrl = null)` | [`GET /email-verifier`](/api/verifier#email-verifier) |
| `Sources.EmailSources(string email)` | [`GET /email-sources`](/api/email#email-sources) |
| `Phone.FinderAsync(Dictionary parameters, string webhookUrl = null)` | [`GET /phone-finder`](/api/phone#phone-finder) |
| `Phone.ValidatorAsync(string phone, string countryCode = null)` | [`GET /phone-validator`](/api/phone#phone-validator) |
| `Status.DomainStatus(string domain)` | [`GET /domain-status`](/api/domain#domain-status) |
| `Status.AutoComplete(string query)` | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `Keys.GetKeys()` | [`GET /keys`](/api/keys#retrieve-api-keys) |
| `Keys.GetKey(string id)` | [`GET /keys/{id}`](/api/keys#retrieve-a-single-api-key) |
| `Keys.CreateKey()` | [`POST /keys`](/api/keys#create-an-api-key) |
| `Keys.ResetKey(string id)` | [`PUT /keys/{id}`](/api/keys#reset-an-api-key) |
| `Keys.DeleteKey(string id)` | [`DELETE /keys/{id}`](/api/keys#delete-an-api-key) |
| `Leads.ListLeadsAsync(int? page = 1, int? limit = 10, string domain = null)` | [`GET /leads`](/api/leads#retrieve-leads) |
| `Leads.GetLeadAsync(string id)` | [`GET /leads/{id}`](/api/leads#retrieve-a-single-lead) |
| `Leads.CreateLeadAsync(Dictionary data)` | [`POST /leads`](/api/leads#create-a-lead) |
| `Leads.UpdateLeadAsync(string id, Dictionary data)` | [`PUT /leads/{id}`](/api/leads#update-a-lead) |
| `Leads.DeleteLeadAsync(string id)` | [`DELETE /leads/{id}`](/api/leads#delete-a-lead) |
| `LeadsLists.GetLists()` | [`GET /leads_lists`](/api/lead-lists#retrieve-leads-lists) |
| `LeadsLists.DeleteListId(string id)` | [`DELETE /leads_lists/{id}`](/api/lead-lists#delete-leads-list) |
| `Bulk.ListAsync(string type, Dictionary parameters = null)` | [`GET /bulk/{type}`](/api/bulks) |
| `Bulk.CreateAsync(string type, Dictionary data)` | [`POST /bulk/{type}`](/api/bulks) |
| `Bulk.GetAsync(string type, string id)` | [`GET /bulk/{type}/{id}`](/api/bulks) |
| `Bulk.ProgressAsync(string type, string id)` | [`GET /bulk/{type}/{id}/progress`](/api/bulks) |
| `Bulk.DownloadAsync(string type, string id)` | [`GET /bulk/{type}/{id}/download`](/api/bulks) |
| `Bulk.RenameAsync(string type, string id, string name)` | [`PUT /bulk/{type}/{id}/rename`](/api/bulks) |
`type` is the `{type}` segment listed in [Bulk types](/bulks#bulk-types). `CreateAsync` sends a JSON body, so it can't create jobs that need a CSV upload, such as [email finder](/bulks#email-finder) jobs.
## Source
[github.com/tomba-io/csharp](https://github.com/tomba-io/csharp) (Apache-2.0). Release notes are in [CHANGELOG.md](https://github.com/tomba-io/csharp/blob/master/CHANGELOG.md).
---
## Document: Zapier
Use Tomba's Zapier app to find, verify, and enrich email addresses, look up phone numbers, and create leads in your Zaps.
URL: /integrations/zapier
# Zapier
Tomba's Zapier app adds 12 Tomba actions to your Zaps: 9 searches and 3 creates. Tomba builds and maintains the app; it's listed at [zapier.com/apps/tomba](https://zapier.com/apps/tomba/integrations).
## Prerequisites
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
- A Zapier account.
## Install and connect
1. In the Zap editor, add an action step and search for **Tomba**. The app has no triggers, so start the Zap with another app's trigger, such as a new spreadsheet row or **Webhooks by Zapier**.
2. Choose an action event from the table below.
3. When Zapier asks for an account, enter your **API KEY** and **Secret Key**. Zapier tests the connection with `GET /v1/me`.
## Actions
Each action sends one request, so it's charged and throttled like a direct API call: see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
| Action | Type | Endpoint | Inputs |
| ---------------- | ------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Find Email | Search | [`GET /email-finder`](/api/finder#email-finder) | **domain** and **full name**, both required |
| Email Verifier | Search | [`GET /email-verifier`](/api/verifier#email-verifier) | **Email**, required |
| Domain Search | Search | [`GET /domain-search`](/api/finder#domain-search) | **Domain**, required. Returns the first 10 addresses. |
| Email Enrichment | Search | [`GET /enrich`](/api/finder#email-enrichment) | **Email**, required |
| Author Finder | Search | [`GET /author-finder`](/api/finder#author-finder) | **url**, required |
| LinkedIn Finder | Search | [`GET /linkedin`](/api/finder#linkedin-finder) | **LinkedIn URL**, required |
| Phone Finder | Search | [`GET /phone-finder`](/api/phone#phone-finder) | **Email**, **Domain**, or **LinkedIn URL**. Zapier marks none as required; fill in at least one. |
| Phone Validator | Search | [`GET /phone-validator`](/api/phone#phone-validator) | **Phone Number**, required; **Country Code**, optional |
| Similar Domains | Search | [`GET /similar`](/api/domain#similar) | **Domain**, required |
| Create Lead | Create | [`POST /leads`](/api/leads#create-a-lead) | **Email**, required; **First name** and **Last name**, optional |
| Count Email | Create | [`GET /email-count`](/api/finder#email-count) | **Domain**, required |
| Domain Status | Create | [`GET /domain-status`](/api/domain#domain-status) | **Domain**, required |
Count Email and Domain Status only read data, but Zapier lists them with the create actions.
Actions return the API response as is, so most fields appear under **Data**: for example, **Data Email** for Find Email and **Data Email Result** for Email Verifier. The response reference pages, such as [Person](/attributes/person) and [Email verifier attributes](/attributes/verifier), describe the fields.
## Example
Find and verify an address for each new spreadsheet row, and create a contact only when it's deliverable:
1. **Trigger:** Google Sheets, **New Spreadsheet Row**, on a sheet with a full name column and a company domain column.
2. **Tomba, Find Email:** map **domain** and **full name** from the row.
3. **Tomba, Email Verifier:** map **Email** to **Data Email** from step 2.
4. **Filter by Zapier:** continue only if **Data Email Result** exactly matches `deliverable`. [Result values](/attributes/verifier#result-values) lists the other values.
5. **Your CRM:** create the contact.
## Troubleshooting
| Symptom | Cause | Fix |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Connecting the account fails | The key or secret is wrong or malformed, or the key has expired. | Copy both values again from [API keys](https://app.tomba.io/api). Replace an expired key; see [Key expiry](/authentication#key-expiry). |
| Step fails with `402` | Not enough credits for the request. | See [Credits and usage](/usage-and-quotas). |
| Step fails with `429` | A rate limit was hit, or the email count balance is used up. | Space the steps with **Delay by Zapier**. See [Handle 429 responses](/rate-limits#handle-429-responses). |
| Step fails with `451` | The address, profile, or phone number is withheld at its owner's request. | Remove the record from your data. |
| Phone Finder fails with `422` | None of **Email**, **Domain**, or **LinkedIn URL** was filled in. | Map at least one of them. |
| "The app did not respond in-time. It may or may not have completed successfully." | Zapier stops waiting for an action after 30 seconds, even if Tomba completes the request. | Run the step again. Repeating the same request in the same usage window isn't charged again; see [Duplicate requests](/usage-and-quotas#duplicate-requests). |
| Tomba isn't offered as a trigger | The app provides actions only. | Start the Zap with another app's trigger. |
---
## Document: Steampipe
Query Tomba with SQL through the Tomba Steampipe plugin: installation, configuration, tables, and how queries are billed.
URL: /integrations/steampipe
# Steampipe
The Tomba plugin for [Steampipe](https://steampipe.io) exposes six Tomba endpoints as SQL tables. Tomba builds and maintains it; the source is at [github.com/tomba-io/steampipe-plugin-tomba](https://github.com/tomba-io/steampipe-plugin-tomba) and the listing at [hub.steampipe.io/plugins/tomba-io/tomba](https://hub.steampipe.io/plugins/tomba-io/tomba).
Every query that reads a Tomba table sends API requests: one for each value you give a table's required column, including each value in an `in (...)` list and each row a join supplies. Those requests are charged and throttled like direct API calls; see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
## Prerequisites
- [Steampipe](https://steampipe.io/downloads) installed.
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
## Install and connect
1. Install the plugin:
```bash
steampipe plugin install tomba-io/tomba
```
2. Put your credentials in `~/.steampipe/config/tomba.spc`:
```hcl
connection "tomba" {
plugin = "tomba-io/tomba"
key = "ta_xxxx"
secret = "ts_xxxx"
}
```
If `key` or `secret` is absent from the file, the plugin reads the `TOMBA_KEY` and `TOMBA_SECRET` environment variables. Values in the file take precedence.
## Tables
Each table requires the column listed, with an `=` or `in` condition, except `tomba_account`.
| Table | Required column | Endpoint | Columns |
| ---------------- | --------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `tomba_verifier` | `email` | [`GET /email-verifier`](/api/verifier#email-verifier) | `email`, `status`, `result`, `score`, `accept_all`, `block`, `registrar_name`, `created_date`, `referral_url` |
| `tomba_enrich` | `email` | [`GET /enrich`](/api/finder#email-enrichment) | `email`, `first_name`, `last_name`, `gender`, `company`, `website_url`, `country`, `twitter`, `linkedin`, `sources` |
| `tomba_linkedin` | `url` | [`GET /linkedin`](/api/finder#linkedin-finder) | `url` plus the `tomba_enrich` columns |
| `tomba_author` | `url` | [`GET /author-finder`](/api/finder#author-finder) | `url` plus the `tomba_enrich` columns |
| `tomba_count` | `domain` | [`GET /email-count`](/api/finder#email-count) | `domain`, `total`, `personal_emails`, `generic_emails`, `department`, `seniority` |
| `tomba_account` | — | [`GET /me`](/api/account#get-account) | `user_id`, `email`, `first_name`, `last_name`, `country`, `timezone`, `created_at`, `pricing`, `secret_token` |
`sources`, `department`, and `seniority` are JSON columns. `secret_token` holds your account's API secret, so restrict access to `tomba_account` when you share a Steampipe instance.
Steampipe caches query results for 300 seconds by default, so repeating a query within that time doesn't call Tomba again.
## Example
Verify a list of addresses and keep the deliverable ones. The join sends one verification request per row of `leads`:
```sql
with leads (email) as (
values
('jane.doe@stripe.com'),
('john.smith@stripe.com')
)
select
v.email,
v.result,
v.score,
v.accept_all
from
leads
join tomba_verifier as v on v.email = leads.email
where
v.result = 'deliverable';
```
`result` is one of the [result values](/attributes/verifier#result-values). Filter on `result` rather than `status`: `status` can be `valid` or `Valid`.
## Troubleshooting
| Symptom | Cause | Fix |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `key must be configured` or `secret must be configured` | The credential is in neither `tomba.spc` nor the environment. | Set `key` and `secret` in `tomba.spc`, or export `TOMBA_KEY` and `TOMBA_SECRET`. |
| An error says a required qual is missing | The query doesn't give the table's required column. | Add a `where` condition or join on the column listed above. |
| The error contains `"type":"quota_exceeded"` | Not enough credits for the requests. | See [Credits and usage](/usage-and-quotas). |
| The error contains `"type":"rate_limit"` | The query sent requests faster than your plan allows. | Query fewer values at a time. See [Rate limits](/rate-limits#limits-by-plan). |
| A lookup returns nothing for a URL or address that contains `&`, `#`, or `+` | The plugin doesn't URL-encode values. | Query the API directly for those values. |
---
## Document: Pipedream
Use the Tomba app on Pipedream to find, verify, and enrich contacts from workflow actions or Node.js code steps.
URL: /integrations/pipedream
# Pipedream
The Tomba app on Pipedream provides 20 actions and a connected account you can use in code steps. Pipedream maintains the app in its component registry ([source](https://github.com/PipedreamHQ/pipedream/tree/master/components/tomba)); it's listed at [pipedream.com/apps/tomba](https://pipedream.com/apps/tomba).
## Prerequisites
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
- A Pipedream account.
## Install and connect
1. In a workflow, add a step and search for **Tomba**.
2. Choose an action, or a Node.js code step that uses the Tomba app.
3. Connect your Tomba account with your API key and secret.
The app has no triggers. Start the workflow with an HTTP request, a schedule, or another app's event.
## Actions
Each action sends one request, so it's charged and throttled like a direct API call: see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
| Action | Endpoint | Inputs |
| ---------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Find Email | [`GET /email-finder`](/api/finder#email-finder) | Domain, First Name, Last Name |
| Verify Email | [`GET /email-verifier`](/api/verifier#email-verifier) | Email Address |
| Search Domain | [`GET /domain-search`](/api/finder#domain-search) | Domain; optional Page, Limit (10, 20, or 50), Country, Department |
| Enrich Email | [`GET /enrich`](/api/finder#email-enrichment) | Email Address |
| Find Author | [`GET /author-finder`](/api/finder#author-finder) | Blog Post URL |
| Find LinkedIn Email | [`GET /linkedin`](/api/finder#linkedin-finder) | LinkedIn URL |
| Find Phone | [`GET /phone-finder`](/api/phone#phone-finder) | Search Type (domain, email, or LinkedIn URL) and Search Input |
| Validate Phone | [`GET /phone-validator`](/api/phone#phone-validator) | Phone Number; optional Country |
| Find Similar Domains | [`GET /similar`](/api/domain#similar) | Domain |
| Get Technology | [`GET /technology`](/api/domain#technology) | Domain |
| Search Companies | [`POST /reveal/search`](/api/reveal#search-companies) | Search Query; optional Search Filters, Limit, Page |
| Get Email Count | [`GET /email-count`](/api/finder#email-count) | Domain |
| Get Email Format | [`GET /email-format`](/api/finder#email-format) | Domain |
| Get Email Sources | [`GET /email-sources`](/api/email#email-sources) | Email Address |
| Get Location | [`GET /location`](/api/finder#location) | Domain |
| Get Domain Status | [`GET /domain-status`](/api/domain#domain-status) | Domain |
| Get Domain Suggestions | [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) | Search Query |
| Get Account | [`GET /me`](/api/account#get-account) | — |
| Get Usage | [`GET /usage`](/api/account#retrieve-api-usage) | — |
| Get Logs | [`GET /logs`](/api/account#retrieve-api-logs) | Optional Limit, Page |
Check two actions before you rely on them:
- **Validate Phone** sends **Country** as a `country` parameter, which the API ignores. Write the number with its `+` country prefix instead.
- **Find Phone** searches by domain unless you change **Search Type**. A domain search is charged differently from an email or LinkedIn search; see [Credit costs](/usage-and-quotas#credit-costs).
## Example
Verify the email address posted to a workflow's HTTP trigger, in a Node.js code step. The `tomba` prop connects your Tomba account; its key and secret are available as `this.tomba.$auth.api_key` and `this.tomba.$auth.api_secret`.
```javascript
import { axios } from "@pipedream/platform";
export default defineComponent({
props: {
tomba: {
type: "app",
app: "tomba",
},
},
async run({ steps, $ }) {
const email = steps.trigger.event.body?.email;
if (!email) {
return $.flow.exit("No email in the request body");
}
const response = await axios($, {
url: "https://api.tomba.io/v1/email-verifier",
params: { email },
headers: {
"X-Tomba-Key": this.tomba.$auth.api_key,
"X-Tomba-Secret": this.tomba.$auth.api_secret,
},
timeout: 180000,
});
return {
email,
result: response.data.email.result,
status: response.data.email.status,
};
},
});
```
`axios` from `@pipedream/platform` returns the response body and throws on a non-2xx status. `result` is one of the [result values](/attributes/verifier#result-values).
## Troubleshooting
| Symptom | Cause | Fix |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Connecting the account fails, or a step returns `401` or `400` `authentication_failed` | The key or secret is wrong or malformed. | Copy both values again from [API keys](https://app.tomba.io/api) and reconnect the account. See [Authentication errors](/authentication#errors). |
| `this.tomba` is `undefined` in a code step | The step doesn't declare the `tomba` app prop. | Add the `props.tomba` block shown in the example and connect the account. |
| A step fails with `402` | Not enough credits for the request. | See [Credits and usage](/usage-and-quotas). |
| A step fails with `429` | A rate limit was hit, or the email count or sources balance is used up. | Send requests one at a time and wait `Retry-After` seconds before retrying. See [Handle 429 responses](/rate-limits#handle-429-responses). |
| A step fails with `451` | The address, profile, or phone number is withheld at its owner's request. | Remove the record from your data. |
---
## Document: Integrations overview
Every Tomba integration, with what it does, who maintains it, and where it's documented: automation platforms, data tools, AI agents, partners, and apps.
URL: /integrations/overview
# Integrations overview
Tomba connects to automation platforms, data tools, and AI agents, and partners offer it inside their own products. An integration that uses your API key and secret consumes your [credits](/usage-and-quotas#credit-costs) and counts toward your [rate limits](/rate-limits) like a direct API call.
| Integration | What it does | Maintained by | Docs |
| -------------------- | --------------------------------------------------------------------------------------------- | ------------- | ------------------------------------------------- |
| Zapier | Find, verify, and enrich addresses, look up phones, and create leads in Zaps | Tomba | [Zapier](/integrations/zapier) |
| Make | Find, verify, and enrich addresses, look up phones, and create leads in Make scenarios | Tomba | [Make](/integrations/make) |
| n8n | Tomba lookups plus lead, list, and key management in n8n workflows | Tomba | [n8n](/integrations/n8n) |
| Pipedream | Tomba actions and authenticated API calls from code steps in Pipedream workflows | Pipedream | [Pipedream](/integrations/pipedream) |
| Apify | Actors that run Tomba lookups over lists and store the results as Apify datasets | Tomba | [Apify](/integrations/apify/introduction) |
| Steampipe | SQL tables for six Tomba endpoints | Tomba | [Steampipe](/integrations/steampipe) |
| Maltego | Local transforms that add Tomba lookups to Maltego graphs | Tomba | [Maltego](/integrations/maltego) |
| Snowflake | A Snowflake Native App that finds, verifies, and enriches contacts in your tables | Tomba | [Tomba for Snowflake](/snowflake/introduction) |
| MCP servers | Tomba tools for Claude, ChatGPT, Cursor, and other MCP clients | Tomba | [Remote MCP server](/llm/remote-mcp/introduction) |
| ClawHub | An OpenClaw skill that guides an agent through Tomba's MCP tools | Tomba | [ClawHub](/integrations/clawhub) |
| Orthogonal | Tomba endpoints through Orthogonal's API gateway, paid to Orthogonal | Orthogonal | [Orthogonal](/integrations/orthogonal) |
| Google Sheets add-on | Find and verify addresses from a spreadsheet | Tomba | [tomba.io/sheets](https://tomba.io/sheets) |
| Excel add-in | Find, verify, and enrich addresses in Excel | Tomba | [tomba.io/excel](https://tomba.io/excel) |
| Airtable extension | Enrich Airtable bases with addresses and contact data | Tomba | [tomba.io/airtable](https://tomba.io/airtable) |
| Browser extension | Find addresses on websites and LinkedIn profiles, in Chrome, Firefox, Edge, Safari, and Opera | Tomba | [tomba.io/browsers](https://tomba.io/browsers) |
---
## Document: Orthogonal
Call Tomba endpoints through Orthogonal's API gateway, with an Orthogonal API key or per-call x402 payments in USDC.
URL: /integrations/orthogonal
# Orthogonal
[Orthogonal](https://orthogonal.com) is an API gateway for AI agents. It lists Tomba as a verified API and forwards requests to 20 Tomba endpoints with its own Tomba credentials. Orthogonal builds, prices, and supports this integration: you authenticate with an Orthogonal API key and pay Orthogonal, and the requests don't use your Tomba credits. Orthogonal's catalog page is [orthogonal.com/discover/tomba](https://orthogonal.com/discover/tomba).
## Prerequisites
- An Orthogonal API key, to call Tomba through Orthogonal's SDK or API.
- Or, to pay per call instead: a wallet holding USDC on Base, used through x402.
Setup, billing, and support questions go to Orthogonal.
## Call Tomba with an API key
Paths and parameters are Tomba's, so the [API reference](/api) applies. With Orthogonal's Node.js SDK (`npm install @orth/sdk`):
```javascript
import Orthogonal from "@orth/sdk";
const orthogonal = new Orthogonal({
apiKey: process.env.ORTHOGONAL_API_KEY,
});
const result = await orthogonal.run({
api: "tomba",
path: "/v1/domain-search",
query: { domain: "stripe.com" },
});
console.log(result);
```
## Endpoints
| Method | Path | Reference |
| ------ | ------------------------ | -------------------------------------------------------------------- |
| `GET` | `/v1/domain-search` | [Domain search](/api/finder#domain-search) |
| `GET` | `/v1/email-finder` | [Email finder](/api/finder#email-finder) |
| `GET` | `/v1/email-verifier` | [Email verifier](/api/verifier#email-verifier) |
| `GET` | `/v1/author-finder` | [Author finder](/api/finder#author-finder) |
| `GET` | `/v1/linkedin` | [LinkedIn finder](/api/finder#linkedin-finder) |
| `GET` | `/v1/phone-finder` | [Phone finder](/api/phone#phone-finder) |
| `GET` | `/v1/phone-validator` | [Phone validator](/api/phone#phone-validator) |
| `GET` | `/v1/email-count` | [Email count](/api/finder#email-count) |
| `GET` | `/v1/email-sources` | [Email sources](/api/email#email-sources) |
| `GET` | `/v1/email-format` | [Email format](/api/finder#email-format) |
| `GET` | `/v1/enrich` | [Email enrichment](/api/finder#email-enrichment) |
| `GET` | `/v1/people/find` | [Person enrichment](/api/enrichment#person-api) |
| `GET` | `/v1/companies/find` | [Company enrichment](/api/enrichment#company-api) |
| `GET` | `/v1/combined/find` | [Combined enrichment](/api/enrichment#combined-api) |
| `GET` | `/v1/technology` | [Technology](/api/domain#technology) |
| `GET` | `/v1/location` | [Location](/api/finder#location) |
| `GET` | `/v1/similar` | [Similar domains](/api/domain#similar) |
| `GET` | `/v1/domain-suggestions` | [Domain suggestions](/api/domain-suggestions#get-domain-suggestions) |
| `GET` | `/v1/domain-status` | [Domain status](/api/domain#domain-status) |
| `POST` | `/v1/reveal/search` | [Company search](/api/reveal#search-companies) |
## Pay per call with x402
Orthogonal also accepts per-call payment in USDC on Base over x402, without an Orthogonal API key. Send the request to `https://x402.orthogonal.com/tomba` followed by the Tomba path, with an x402 client such as `x402-fetch` (`npm install x402-fetch viem`):
```javascript
import { wrapFetchWithPayment } from "x402-fetch";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const fetchWithPayment = wrapFetchWithPayment(fetch, account);
const response = await fetchWithPayment(
"https://x402.orthogonal.com/tomba/v1/domain-search?domain=stripe.com",
);
console.log(await response.json());
```
`PRIVATE_KEY` is the key of a wallet holding USDC on Base. Anyone who has it
can spend the wallet's funds, so load it from a secrets manager or an
environment variable, never from source code.
To pay Tomba per request directly, without a Tomba account or a gateway, see [Agent payments (MPP)](/mpp/introduction).
---
## Document: n8n
Use Tomba's n8n community node to find, verify, and enrich contacts and manage leads in n8n workflows.
URL: /integrations/n8n
# n8n
The Tomba node, npm package `n8n-nodes-tomba`, adds 45 Tomba operations to n8n workflows. Tomba builds and maintains it; the source is at [github.com/tomba-io/n8n-nodes-tomba](https://github.com/tomba-io/n8n-nodes-tomba) and the listing at [n8n.io/integrations/tomba](https://n8n.io/integrations/tomba/).
## Prerequisites
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
- n8n Cloud or a self-hosted n8n instance, and an owner or admin account on it to install the node.
## Install and connect
Install the node in one of two ways:
- **From the nodes panel (n8n Cloud and self-hosted).** Open the nodes panel, search for **Tomba**, select the node under **More from the community**, and select **Install**. n8n verifies the node, so it's available wherever verified community nodes are enabled; on n8n Cloud, an owner enables them in the Cloud Admin Panel.
- **By package name (self-hosted only).** Go to **Settings > Community Nodes**, select **Install**, enter `n8n-nodes-tomba`, accept the risk notice, and select **Install**.
Either way, the node is installed once for the whole instance.
Then connect your account:
1. Open the Tomba node and leave **Authentication** set to **API Key**.
2. Under **Credential to connect with**, create a **Tomba API** credential with your **API Key** and **API Secret**. n8n tests the credential with `GET /v1/me`.
The **OAuth2** option needs an OAuth client ID and secret issued by Tomba. Use **API Key** unless Tomba has issued you a client.
## Actions
Pick a **Resource**, then an **Operation**. Each operation sends one request, so it's charged and throttled like a direct API call: see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
| Resource | Operations | Endpoints |
| -------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Domain | Domain Search, Domain Status, Autocomplete, Location, Similar, Technology | [`/domain-search`](/api/finder#domain-search), [`/domain-status`](/api/domain#domain-status), [`/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions), [`/location`](/api/finder#location), [`/similar`](/api/domain#similar), [`/technology`](/api/domain#technology) |
| Email | Email Finder, Email Verifier, Email Enrichment, Email Count, Email Sources, Email Format | [`/email-finder`](/api/finder#email-finder), [`/email-verifier`](/api/verifier#email-verifier), [`/enrich`](/api/finder#email-enrichment), [`/email-count`](/api/finder#email-count), [`/email-sources`](/api/email#email-sources), [`/email-format`](/api/finder#email-format) |
| Finder | Author Finder, LinkedIn Finder | [`/author-finder`](/api/finder#author-finder), [`/linkedin`](/api/finder#linkedin-finder) |
| Phone | Phone Finder, Phone Validator | [`/phone-finder`](/api/phone#phone-finder), [`/phone-validator`](/api/phone#phone-validator) |
| Company | Search Companies | [`POST /reveal/search`](/api/reveal#search-companies) |
| Enrichment | Person, Company, Combined | [`/people/find`](/api/enrichment#person-api), [`/companies/find`](/api/enrichment#company-api), [`/combined/find`](/api/enrichment#combined-api) |
| Lead | List, Get, Create, Update, Delete | [Leads](/api/leads#retrieve-leads) |
| Lead List | List, Get, Create, Update, Delete | [Lead lists](/api/lead-lists#retrieve-leads-lists) |
| Lead Attribute | List, Get, Create, Update, Delete | [Lead attributes](/api/lead-attributes#retrieve-lead-attributes) |
| Key | List, Get, Create, Reset, Delete | [Keys](/api/keys#retrieve-api-keys) |
| Account | Account, Usage, Logs | [`/me`](/api/account#get-account), [`/usage`](/api/account#retrieve-api-usage), [`/logs`](/api/account#retrieve-api-logs) |
| Flag | List, Create | [Flag](/api/flag#flag-incorrect-data) |
Operation details that differ from the API:
- **Domain Search** offers a **Limit** of 10, 20, or 50. **Return All** sends one request with `limit=100`, which is charged for 100 results (see [Credit costs](/usage-and-quotas#credit-costs)) and fails on the Free plan. **Only Emails** is on by default and returns only the addresses; turn it off to get the company data too.
- **Email Finder** needs **Domain** or **Company**, plus **Full Name** or both **First Name** and **Last Name**. The node doesn't mark these fields as required, so a request without them fails at the API with `422`.
- **Phone Finder** searches by one of email, domain, or LinkedIn URL, chosen in **Search Type**.
- **Lead > Create** requires **Email** and **List ID** in the node. The API itself requires only the email.
- **Email Finder**, **Email Verifier**, **Email Enrichment**, **Phone Finder**, **LinkedIn Finder**, **Author Finder**, **Domain Search**, and **Search Companies** accept a **Webhook URL**. The node still waits for the response; see [Per-request callbacks](/webhook#per-request-callbacks) for which endpoints send a callback.
The node waits up to 120 seconds for each response.
For an endpoint the node doesn't cover, use the **HTTP Request** node: set **Authentication** to **Predefined Credential Type**, choose **Tomba API**, and select your credential. n8n adds the `X-Tomba-Key` and `X-Tomba-Secret` headers.
## Example
Find an address for each incoming contact and keep only deliverable ones:
1. **Trigger:** a node that outputs `first_name`, `last_name`, and `domain`, such as a form or a spreadsheet row.
2. **Tomba > Email > Email Finder:** set **Domain** to `{{ $json.domain }}`, **First Name** to `{{ $json.first_name }}`, and **Last Name** to `{{ $json.last_name }}`. The output has the address in `email`.
3. **Tomba > Email > Email Verifier:** set **Email** to `{{ $json.email }}`. The output has the verification fields under `email`.
4. **If:** continue when `{{ $json.email.result }}` equals `deliverable`. [Email verifier attributes](/attributes/verifier#result-values) lists the other values.
## Troubleshooting
| Symptom | Cause | Fix |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Credential test fails | The key or secret is wrong or malformed, or the key has expired. | Copy both values again from [API keys](https://app.tomba.io/api). Replace an expired key; see [Key expiry](/authentication#key-expiry). |
| `402` error | Not enough credits for the request. | See [Credits and usage](/usage-and-quotas). |
| `429` error | A rate limit was hit, or the email count or sources balance is used up. | For per-second limits, turn on **Retry On Fail** in the node's settings. For per-minute limits, space items with a **Wait** node, because **Wait Between Tries** can't exceed 5 seconds. See [Handle 429 responses](/rate-limits#handle-429-responses). |
| `451` error | The address, profile, or phone number is withheld at its owner's request. | Remove the record from your data. |
| `422` error | A required parameter is missing or invalid. | Check the fields listed above. [Errors](/error-handling#error-types) lists every error type. |
| Created lead isn't in the list and has no name | The node sends **List ID** as text, and the API drops the list ID and the fields after it. | Create the lead with an **HTTP Request** node that sends `list_id` as a number, or add the lead to the list in the [dashboard](https://app.tomba.io/leads). |
---
## Document: Maltego
Run Tomba lookups as local Maltego transforms: installation, credentials, the ten transforms and their entity types, and how they're billed.
URL: /integrations/maltego
# Maltego
The Tomba transforms add ten Tomba lookups to Maltego graphs. They run as local transforms on your machine, using the Tomba Python SDK. Tomba builds and maintains them; the source is at [github.com/tomba-io/tomba-maltego](https://github.com/tomba-io/tomba-maltego).
## Prerequisites
- Maltego.
- Python 3.10 or later: the current `maltego-trx` release requires it.
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
## Install and connect
1. Clone the repository and install the dependencies:
```bash
git clone https://github.com/tomba-io/tomba-maltego.git
cd tomba-maltego
pip install -r requirements.txt
```
2. Create `settings.py` from the template:
```bash
cp settings.py.template settings.py
```
3. In `settings.py`, set `TOMBA_API_KEY` to your key and `TOMBA_SECRET_KEY` to your secret. The transforms read credentials only from this file.
4. In Maltego, use **Import Config** to import `tomba_transforms.mtz` from the repository.
5. In Maltego's transform settings, set each Tomba transform's working directory to the folder you cloned: the imported configuration points to another path. The transforms run `python3 project.py local `, so if you installed the dependencies in a virtual environment, also set the command to that environment's Python.
## Transforms
Each transform sends one request, so it's charged and throttled like a direct API call: see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
| Transform | Input entity | Output entities | Endpoint |
| ------------------------ | ------------- | ---------------------------------------- | ----------------------------------------------------- |
| Tomba - Domain Search | Website | Email Address, Person, Company | [`GET /domain-search`](/api/finder#domain-search) |
| Tomba - Email Verifier | Email Address | Email Address | [`GET /email-verifier`](/api/verifier#email-verifier) |
| Tomba - Email Enrichment | Email Address | Email Address, Person | [`GET /enrich`](/api/finder#email-enrichment) |
| Tomba - Author Finder | URL | Email Address, Person | [`GET /author-finder`](/api/finder#author-finder) |
| Tomba - LinkedIn Finder | URL | Email Address, Person | [`GET /linkedin`](/api/finder#linkedin-finder) |
| Tomba - Phone Finder | Email Address | Email Address carrying the phone details | [`GET /phone-finder`](/api/phone#phone-finder) |
| Tomba - Phone Validator | Phone Number | Phone Number | [`GET /phone-validator`](/api/phone#phone-validator) |
| Tomba - Similar Websites | Website | Website | [`GET /similar`](/api/domain#similar) |
| Tomba - Technology | Domain | One Domain entity per technology | [`GET /technology`](/api/domain#technology) |
| Tomba - Account Info | Phrase | Phrase | [`GET /me`](/api/account#get-account) |
The import also adds **Tomba - Base Transform**, which does nothing; don't run it.
**Tomba - Domain Search** requests 50 addresses and is charged for that limit. To request fewer, add a `tomba.limit` property with the value `10` or `20` to the Website entity before you run the transform; on the Free plan, a limit above 10 fails (see [Free plan limits](/usage-and-quotas#free-plan-limits)). The transform also reads an optional `tomba.department` property.
## Example
Map the people at a company:
1. Add a **Website** entity with the value `stripe.com`.
2. Run **Tomba - Domain Search**. Maltego adds an Email Address entity for each address found, a Person entity for each named person, and a Company entity.
3. Select the Email Address entities and run **Tomba - Email Verifier** to check them.
## Troubleshooting
| Symptom | Cause | Fix |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| "Please configure Tomba.io API credentials" | `settings.py` is missing or doesn't contain your key and secret. | Create it from the template and set both values. |
| The transform fails to start | The working directory or Python command in the transform settings is wrong, or the dependencies aren't installed for that Python. | Point the settings at your clone and the Python that has `maltego-trx` and `tomba-io` installed. |
| A transform reports a Tomba error | The request failed: invalid credentials, too few credits, a rate limit, or a withheld record. | Look up the error in [Errors](/error-handling#error-types). |
---
## Document: Make
Use the Tomba Email Finder app on Make to find, verify, and enrich email addresses, look up phone numbers, and create leads in scenarios.
URL: /integrations/make
# Make
The Tomba Email Finder app adds 12 Tomba modules to Make scenarios. Make lists it as a verified app from the official vendor at [make.com/en/integrations/tomba](https://www.make.com/en/integrations/tomba).
## Prerequisites
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
- A Make account.
## Install and connect
1. In a scenario, add a module and search for **Tomba Email Finder**.
2. Choose a module from the table below.
3. Create a connection with your **API Key** and **Secret key**.
The app has no triggers. Start the scenario with another app's module, a webhook, or a schedule.
## Actions
Each module sends one request, so it's charged and throttled like a direct API call: see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
| Module | Type | Endpoint |
| ---------------------- | ------ | ------------------------------------------------------ |
| Search email address | Search | [`GET /email-finder`](/api/finder#email-finder) |
| Email verifier | Search | [`GET /email-verifier`](/api/verifier#email-verifier) |
| Search domain emails | Search | [`GET /domain-search`](/api/finder#domain-search) |
| Search email | Search | [`GET /enrich`](/api/finder#email-enrichment) |
| Search author | Search | [`GET /author-finder`](/api/finder#author-finder) |
| Search LinkedIn email | Search | [`GET /linkedin`](/api/finder#linkedin-finder) |
| Search phone | Search | [`GET /phone-finder`](/api/phone#phone-finder) |
| Phone validator | Search | [`GET /phone-validator`](/api/phone#phone-validator) |
| Search similar domains | Search | [`GET /similar`](/api/domain#similar) |
| Search emails | Search | [`GET /email-count`](/api/finder#email-count) |
| Create a lead | Action | [`POST /leads`](/api/leads#create-a-lead) |
| Make an API Call | Action | Any Tomba endpoint, using the connection's credentials |
Parameter rules come from the API:
- **Search domain emails:** the API reference documents a `limit` of 10, 20, or 50. On the Free plan, a limit above 10 fails; see [Free plan limits](/usage-and-quotas#free-plan-limits).
- **Search email** enriches an email address only. To look up a company by domain, call [`GET /companies/find`](/api/enrichment#company-api) with **Make an API Call**.
- **Search emails** returns the number of addresses for a domain, not the addresses.
## Example
Find and verify an address for each new spreadsheet row, and add deliverable addresses to your CRM:
1. **Google Sheets:** a module that watches for new rows in a sheet with first name, last name, and company domain columns.
2. **Tomba Email Finder, Search email address:** map the name and domain columns.
3. **Tomba Email Finder, Email verifier:** map the address from step 2.
4. **Filter:** continue only when the verifier's `result` is `deliverable`. [Result values](/attributes/verifier#result-values) lists the other values.
5. **Your CRM:** create the contact.
## Troubleshooting
| Symptom | Cause | Fix |
| ------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| The connection fails | The key or secret is wrong or malformed, or the key has expired. | Copy both values again from [API keys](https://app.tomba.io/api). Replace an expired key; see [Key expiry](/authentication#key-expiry). |
| A module fails with `402` | Not enough credits for the request. | See [Credits and usage](/usage-and-quotas). |
| A module fails with `429` | A rate limit was hit, or the email count balance is used up. | Process fewer bundles per run, or add an error handler that retries later. See [Handle 429 responses](/rate-limits#handle-429-responses). |
| A module fails with `451` | The address, profile, or phone number is withheld at its owner's request. | Remove the record from your data. |
| A module fails with `422` | A parameter is missing or has an invalid value. | Check the parameter in the [API reference](/api). [Errors](/error-handling#error-types) lists every error type. |
---
## Document: ClawHub
Install the Tomba Contact Intelligence skill from ClawHub so OpenClaw agents choose the right Tomba MCP tools for contact and company research.
URL: /integrations/clawhub
# ClawHub
The Tomba Contact Intelligence skill tells an OpenClaw agent which Tomba MCP tools to use for contact discovery, verification, enrichment, and company research, and in what order. The skill makes no requests itself: the agent calls the tools of a connected Tomba MCP server. Tomba maintains the skill; the source is in [tomba-io/tomba-mcp-server](https://github.com/tomba-io/tomba-mcp-server/tree/main/skills/tomba-contact-intelligence) and the listing on [ClawHub](https://clawhub.ai/benemohamed/skills/tomba-contact-intelligence).
## Prerequisites
- OpenClaw.
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
- A Tomba MCP server connected to OpenClaw: the [local server](/llm/local-mcp/introduction), which reads the same environment variables as the skill, or the [remote server](/llm/remote-mcp/introduction).
## Install and connect
1. Install the skill:
```bash
openclaw skills install @benemohamed/tomba-contact-intelligence
```
Or copy the `skills/tomba-contact-intelligence` folder from the source repository to `~/.openclaw/skills/tomba-contact-intelligence/`.
2. Set the environment variables the skill declares as required:
```bash
export TOMBA_API_KEY="ta_xxxx"
export TOMBA_SECRET_KEY="ts_xxxx"
```
3. Restart the OpenClaw gateway or start a new session so the skill is loaded.
## Actions
The skill expects these 12 tools from the MCP server: `domain_search`, `email_finder`, `email_verifier`, `email_enrichment`, `author_finder`, `linkedin_finder`, `phone_finder`, `phone_validator`, `email_count`, `similar_finder`, `technology_finder`, and `companies_search`. [MCP tools](/llm/remote-mcp/tools) describes their parameters.
It directs the agent to:
- Find a person's address with `email_finder`, then check it with `email_verifier` before calling it confirmed.
- Research a company with `domain_search`, `email_count`, `technology_finder`, and `similar_finder`.
- Build a company list with `companies_search`, then look up contacts for the best matches.
- Say whether an address was verified and separate confirmed data from inferred data.
Each tool call is a Tomba API request, charged and throttled like a direct call: see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
## Example
Ask the agent:
```text
Find the email address of Jane Doe at stripe.com and verify it.
```
The skill leads the agent to call `email_finder` with the name and domain, then `email_verifier` on the result, and to report the address with its verification result.
## Troubleshooting
| Symptom | Cause | Fix |
| ------------------------------------------ | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| The agent doesn't use the skill | The skill isn't loaded. | Check that `SKILL.md` is in `~/.openclaw/skills/tomba-contact-intelligence/`, then restart the gateway or start a new session. |
| The agent reports missing Tomba tools | No Tomba MCP server is connected, or it's missing some of the 12 tools. | Connect a Tomba MCP server and check its tool list. |
| Tool calls fail with authentication errors | The MCP server has no valid key and secret. | Set `TOMBA_API_KEY` and `TOMBA_SECRET_KEY`, or fix the remote server's credentials; see [Authentication](/authentication#errors). |
---
## Document: Data updates
Log of Tomba data refreshes: when each refresh's data was collected and published, and the email and company counts it reported.
URL: /data/updates
# Data updates
This log lists each refresh of Tomba's email and company data, newest first, under the month its data was
collected, with the counts the refresh reported. For sources and methodology, see [Data](/data). Changes to the API itself are in the [API changelog](/changelog).
## September 2026
Data collected through 2026-09-17. Published 2026-09-29.
- Unique emails collected: 49.8 million
## August 2026
Data collected through 2026-08-20. Published 2026-09-05.
- Unique emails collected: 49.2 million
## July 2026
Data collected through 2026-07-23. Published 2026-08-07.
- Unique emails collected: 46.0 million
## June 2026
Data collected through 2026-06-18. Published 2026-07-01.
- Unique emails collected: 48.7 million
## May 2026
Data collected through 2026-05-21. Published 2026-06-02.
- Unique emails collected: 50.3 million
## April 2026
Data collected through 2026-04-21. Published 2026-05-22.
- Unique emails collected: 35.2 million
## March 2026
Data collected through 2026-03-17. Published 2026-04-01.
- Unique emails collected: 47.8 million
## February 2026
Data collected through 2026-02-19. Published 2026-03-09.
- Unique emails collected: 48.8 million
## January 2026
Data collected through 2026-01-25. Published 2026-02-09.
- Unique emails collected: 49.5 million
## December 2025
Data collected through 2025-12-17. Published 2026-01-08.
- Unique emails collected: 44.9 million
## November 2025
Data collected through 2025-11-19.
- Emails imported or updated: 45.4 million
- Company profiles updated: 18.5 million
## October 2025
Data collected through 2025-10-19.
- Emails imported or updated: 49.4 million
- Company profiles updated: 10.5 million
## September 2025
Data collected through 2025-09-18.
- Emails imported or updated: 46.5 million
- Company profiles updated: 12.0 million
---
## Document: Slack slash commands
Run Tomba lookups from Slack with slash commands served by your own tomba serve instance: prerequisites, proxy setup, app manifest, and commands.
URL: /cli/slack
# Slack slash commands
Slash commands such as `/checker jane.doe@stripe.com` run Tomba lookups from Slack. They're served by your own [`tomba serve`](/cli/http) instance: you create a Slack app from the manifest on this page and point its commands at your server.
## How it works
When someone runs a command, Slack sends the text after the command to the command's request URL. With `?slack=true` in the URL, `tomba serve` uses that text as the input, calls the Tomba API with the account the CLI is signed in to, and answers with a Slack message. Slack shows the reply only to the person who ran the command.
Every command is a Tomba API request billed to that account, as described in [Credit costs](/usage-and-quotas#credit-costs). The server doesn't verify Slack's request signatures, so anyone who knows the URL can run lookups on your credits.
## Prerequisites
- A server with the CLI installed and signed in. `tomba login --api-key` suits a server; see [Authentication](/cli/authentication).
- A public HTTPS URL for that server, with a valid certificate. Slack sends slash commands only to HTTPS URLs.
- Permission to create apps in your Slack workspace.
## Expose the server
`tomba serve` has no authentication and exposes routes that return your API secret and manage your keys, so don't publish its port. Run it behind a reverse proxy that terminates HTTPS and forwards only the routes the commands use, and block port 3000 at your firewall.
An nginx server block that does this:
```text
server {
listen 443 ssl;
server_name tomba-slack.example.com;
ssl_certificate /etc/ssl/certs/tomba-slack.example.com.pem;
ssl_certificate_key /etc/ssl/private/tomba-slack.example.com.key;
location ~ ^/(search|verify|enrich|author|linkedin)$ {
limit_except POST { deny all; }
proxy_pass http://127.0.0.1:3000;
}
location / {
return 404;
}
}
```
Then start the server on the same host:
```bash
tomba serve --port 3000
```
## Create the Slack app
1. Replace `https://tomba-slack.example.com` in the manifest below with your server's URL.
2. Open [api.slack.com/apps](https://api.slack.com/apps), choose **Create New App**, then **From a manifest**, select your workspace, and paste the manifest.
3. Install the app to your workspace.
```json
{
"display_information": {
"name": "Tomba",
"description": "Find and verify email addresses with Tomba."
},
"features": {
"bot_user": {
"display_name": "tomba",
"always_online": false
},
"slash_commands": [
{
"command": "/domain",
"url": "https://tomba-slack.example.com/search?slack=true",
"description": "Find email addresses at a domain.",
"usage_hint": "stripe.com",
"should_escape": false
},
{
"command": "/checker",
"url": "https://tomba-slack.example.com/verify?slack=true",
"description": "Verify an email address.",
"usage_hint": "jane.doe@stripe.com",
"should_escape": false
},
{
"command": "/enrich",
"url": "https://tomba-slack.example.com/enrich?slack=true",
"description": "Look up the person behind an email address.",
"usage_hint": "jane.doe@stripe.com",
"should_escape": false
},
{
"command": "/author",
"url": "https://tomba-slack.example.com/author?slack=true",
"description": "Find the email address of an article's author.",
"usage_hint": "https://stripe.com/blog/example-article",
"should_escape": false
},
{
"command": "/linkedin",
"url": "https://tomba-slack.example.com/linkedin?slack=true",
"description": "Find the email address for a LinkedIn profile.",
"usage_hint": "https://www.linkedin.com/in/jane-doe-example",
"should_escape": false
}
]
},
"oauth_config": {
"scopes": {
"bot": ["commands"]
}
},
"settings": {
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}
```
## Commands
| Command | Route | Input | Reply |
| ----------- | ----------- | ---------------------- | --------------------------------------------------------------------- |
| `/domain` | `/search` | A domain | The addresses found, with name, position, location, and profiles |
| `/checker` | `/verify` | An email address | Format, mail server, and deliverability status, with WHOIS details |
| `/enrich` | `/enrich` | An email address | The person's name, position, company, location, profiles, and sources |
| `/author` | `/author` | An article URL | The author's email address and profile |
| `/linkedin` | `/linkedin` | A LinkedIn profile URL | The person's email address and profile |
For example:
```text
/domain stripe.com
/checker jane.doe@stripe.com
```
## Troubleshooting
| Problem | Cause and fix |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Slack reports that the command failed | The server answered with an error status: the lookup found nothing, the input was invalid, or the API call failed, for example on a rate limit. Slack doesn't show the reason. Check the server's log, and run the same lookup with the CLI, such as `tomba verify --target jane.doe@stripe.com`, to see the API's status. |
| Slack reports a timeout | Slack waits 3 seconds for a reply, and some lookups take longer. The lookup still runs and uses credits as usual. |
| `/checker` fails for a Gmail or other webmail address | The server answers with an error for addresses at webmail and disposable email domains, after the verification has run. |
---
## Document: Skills
Chain Tomba lookups into one command with tomba skill: the built-in skills, running a skill, and creating, importing, or exporting your own in YAML.
URL: /cli/skill
# Skills
A skill runs several Tomba lookups as one command and passes results from one step to the next, for example finding an address and then verifying it. The CLI includes eight skills, and you can add your own as YAML files.
The CLI's output calls this feature ClawHub. CLI skills are local files that the CLI runs itself; they're separate from the Tomba skill published on [ClawHub](/integrations/clawhub) for AI agents.
## Built-in skills
| Skill | Steps | Inputs |
| ---------------- | ------------------------------------------ | -------------------------------------------- |
| `find-email` | finder, verify | `domain`, `first_name`, `last_name` |
| `company-emails` | count, search | `domain` |
| `enrich-verify` | enrich, verify, sources | `email` |
| `company-intel` | status, count, search, technology, similar | `domain` |
| `lead-gen` | search, filtered by department | `domain`, `department` (default `executive`) |
| `linkedin-intel` | linkedin, enrich, verify | `url` |
| `domain-check` | status, count | `domain` |
| `author-verify` | author, verify | `url` |
Each step is a Tomba API request, billed as described in [Credit costs](/usage-and-quotas#credit-costs). The steps use the same endpoints as the [commands](/cli/commands) of the same name.
## List and inspect skills
```bash
tomba skill list
tomba skill info find-email
```
`tomba skill list`, or `tomba skill` alone, lists the built-in and custom skills. `tomba skill info` shows a skill's inputs, each step with its parameters, and an example command.
## Run a skill
```text
tomba skill run --target [= ...]
```
`--target` sets the skill's first input; pass the others as `name=value` arguments. The CLI prompts for any required input you leave out.
```bash
tomba skill run find-email --target stripe.com first_name=Jane last_name=Doe
```
Steps run in order. When a step fails, the CLI reports it and moves on to the next step; a step that needs an email address from an earlier step fails if that step found none. After the last step, the CLI prints each step's result.
## Add your own skills
| Command | What it does |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `tomba skill create` | Asks for a name, description, inputs, steps, and output settings, then saves the skill. |
| `tomba skill import ` | Checks that the file has a name and at least one step, then saves it, replacing a custom skill of the same name. |
| `tomba skill export ` | Prints a skill, built-in or custom, as YAML. |
| `tomba skill remove ` | Deletes a custom skill. Built-in skills can't be removed. |
Custom skills are saved in `~/.tomba/skills/` as `.yaml`, and the CLI also loads any `.yaml` or `.yml` file you copy there. A custom skill with the same name as a built-in skill is never run.
To start from a built-in skill, export it, edit the file, and import it under a new name:
```bash
tomba skill export find-email > my-find-email.yaml
tomba skill import my-find-email.yaml
```
## Skill file format
This skill searches a domain and verifies the first address found:
```yaml
name: verify-top-contact
description: Search a domain and verify the first address found
inputs:
- name: domain
description: Company domain
type: domain
required: true
steps:
- id: search
name: Search emails
action: search
params:
domain: "{{input.domain}}"
limit: "10"
- id: verify
name: Verify first address
action: verify
params:
email: "{{steps.search.data.emails.0.email}}"
output:
title: Verified contact
```
| Field | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `name` | Skill name, used on the command line and as the file name. Required. |
| `description` | Shown by `list` and `info`. |
| `inputs` | Each input's `name`, `description`, `type` (`string`, `email`, `domain`, `url`, or `linkedin`), `required`, and `default`. |
| `steps` | Each step's `id`, `name`, `action`, and `params`. At least one step is required. |
| `output.title` | Heading printed above the results. |
`author`, `version`, `tags`, and `output.fields` appear only in `tomba skill info`. `output.format`, `save_as`, `condition`, and `variables` are accepted but have no effect.
### Actions
| `action` | `params` |
| ----------------- | --------------------------------------------------------- |
| `search` | `domain`, `limit`, `page`, `department` |
| `finder` | `domain`, and `first_name` and `last_name` or `full_name` |
| `enrich` | `email` |
| `verify` | `email` |
| `count` | `domain` |
| `status` | `domain` |
| `sources` | `email` |
| `similar` | `domain` |
| `technology` | `domain` |
| `linkedin` | `url` |
| `author` | `url` |
| `phone-finder` | `email` or `domain` |
| `phone-validator` | `phone`, `country_code` |
`tomba skill create` also offers `reveal`, but a step with that action fails with `unknown action`.
### Templates
Parameter values can include templates:
- `{{input.}}` is replaced with an input's value.
- `{{steps..}}` is replaced with a field from an earlier step's response. Use dots for nested fields and `0` for the first item of a list, as in `{{steps.search.data.emails.0.email}}`.
If a template doesn't resolve, an `enrich`, `verify`, or `sources` step fails with `no email available from previous step`; other steps send the template text as written.
---
## Document: CLI installation
Install the Tomba CLI on macOS, Linux, or Windows, verify the download, set up shell completion, and update or remove it.
URL: /cli/installation
# CLI installation
The Tomba CLI, `tomba`, is a single binary for macOS, Linux, and Windows. After you install it, sign in as described in [Authentication](/cli/authentication).
## Install
Release builds are available for macOS (amd64, arm64), Linux (386, amd64, armv6, arm64, ppc64), and Windows (386, amd64, arm64).
### Install script (macOS and Linux)
```bash
curl -sSL https://releases.tomba.io/install.sh | sh
```
The script downloads the latest release for your platform from [GitHub Releases](https://github.com/tomba-io/tomba/releases), checks its SHA-256 checksum, installs `tomba` to `/usr/local/bin` (with `sudo` if that directory isn't writable), and sets up completion for bash, zsh, or fish. If it can't download the checksum file or find `sha256sum` or `shasum`, it prints a warning and installs without checking; to check the archive yourself, see [Verify the download](#verify-the-download).
To pin a version or install to another directory, pass `--version` or `--dir`:
```bash
curl -sSL https://releases.tomba.io/install.sh | sh -s -- --version 1.2.3 --dir "$HOME/.local/bin"
```
### PowerShell (Windows)
```powershell
irm https://releases.tomba.io/install.ps1 | iex
```
The script checks the SHA-256 checksum, installs `tomba.exe` to `%LOCALAPPDATA%\tomba`, adds that folder to your user `PATH`, and adds PowerShell completion to your profile.
### Homebrew
```bash
brew install --cask tomba-io/tap/tomba
```
### Scoop (Windows)
```powershell
scoop bucket add tomba https://github.com/tomba-io/scoop-bucket.git
scoop install tomba
```
### Snap (Linux)
```bash
sudo snap install tomba
```
### Linux packages
Each release includes `.deb`, `.rpm`, `.apk`, and Arch Linux (`.pkg.tar.zst`) packages. For example, on Debian or Ubuntu:
```bash
curl -LO https://github.com/tomba-io/tomba/releases/latest/download/tomba_amd64.deb
sudo apt install --no-install-recommends ./tomba_amd64.deb
```
The packages depend on `git` and `systemd` and recommend `golang`; `--no-install-recommends` skips the Go toolchain. They install shell completion, a man page, and a systemd unit, `tomba.service`, which runs [`tomba serve`](/cli/http) as `www-data` and is enabled at boot. If you don't use the local server, disable the unit:
```bash
sudo systemctl disable --now tomba
```
### Go
Requires Go 1.25 or later.
```bash
go install github.com/tomba-io/tomba@latest
```
The binary goes to `$(go env GOPATH)/bin`, which must be on your `PATH`. Builds from `go install` report their version as `dev`.
## Verify the download
Every release publishes `tomba_checksums.txt` with the SHA-256 checksum of each archive and package. To install from an archive and check it first:
```bash
VERSION=1.2.3
curl -LO "https://github.com/tomba-io/tomba/releases/download/v${VERSION}/tomba_linux_amd64.tar.gz"
curl -LO "https://github.com/tomba-io/tomba/releases/download/v${VERSION}/tomba_checksums.txt"
sha256sum --ignore-missing -c tomba_checksums.txt
tar -xzf tomba_linux_amd64.tar.gz tomba
sudo install -m 0755 tomba /usr/local/bin/tomba
```
On macOS, use `shasum -a 256 --ignore-missing -c tomba_checksums.txt` and the `tomba_darwin_arm64` or `tomba_darwin_amd64` archive. The check prints `OK` next to the archive name; any other result means the file doesn't match the release, so don't install it.
## Verify the installation
```bash
tomba version
```
```text
tomba 1.2.3
built: 2026-09-27T14:29:29Z
commit: 0949b31
go: go1.27.1
os/arch: linux/amd64
```
`tomba --version` prints the version on one line (`tomba version 1.2.3`).
## Shell completion
The install scripts, Homebrew, and the Linux packages set up completion for you. To load it yourself for the current session:
```bash
# bash (requires the bash-completion package)
source <(tomba completion bash)
# zsh
source <(tomba completion zsh)
# fish
tomba completion fish | source
```
```powershell
tomba completion powershell | Out-String | Invoke-Expression
```
To load completion in every new session, follow the steps that `tomba completion --help` prints for your shell.
## Update
```bash
tomba update
```
`tomba update` (alias `upgrade`) looks up the latest release on GitHub, downloads the archive for your platform, checks its SHA-256 checksum when the checksum file is available, and replaces the `tomba` binary in place. If the binary is in a directory you can't write to, the command fails with `Cannot rename current binary (may need sudo)`; run it with `sudo`.
If you installed with a package manager, update with the same tool so it keeps track of the version:
| Installed with | Update with |
| -------------- | ------------------------------------------------ |
| Homebrew | `brew upgrade --cask tomba` |
| Scoop | `scoop update tomba` |
| Snap | `sudo snap refresh tomba` |
| Linux package | Download the new package and install it as above |
| Go | `go install github.com/tomba-io/tomba@latest` |
## Uninstall
| Installed with | Remove with |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| Install script | `sudo rm /usr/local/bin/tomba`, or remove `tomba` from the directory you passed to `--dir` |
| PowerShell script | Delete `%LOCALAPPDATA%\tomba`, remove it from your user `PATH`, and remove the `tomba.completion.ps1` line from your PowerShell profile |
| Homebrew | `brew uninstall --cask tomba` |
| Scoop | `scoop uninstall tomba` |
| Snap | `sudo snap remove tomba` |
| Debian or Ubuntu package | `sudo apt remove tomba` |
| RPM package | `sudo dnf remove tomba` |
| Alpine package | `sudo apk del tomba` |
| Arch Linux package | `sudo pacman -R tomba` |
| Go | `rm "$(go env GOPATH)/bin/tomba"` |
The CLI keeps saved credentials in `~/.tomba.json` and custom skills in `~/.tomba/skills/`. Removing the binary leaves them in place; delete them with:
```bash
rm -rf ~/.tomba.json ~/.tomba
```
---
## Document: Local HTTP server
Run tomba serve to call Tomba through a local HTTP server: start it, send requests, the routes it exposes, its responses, and how to secure it.
URL: /cli/http
# Local HTTP server
`tomba serve` runs an HTTP server that forwards each request to the Tomba API with the credentials the CLI is signed in with. Use it to call Tomba from local scripts and tools, or to back [Slack slash commands](/cli/slack). CLI versions before 1.2.3 name this command `tomba http`.
The server has no authentication, listens on all IPv4 interfaces, and
accepts cross-origin requests from any website. Anyone who can reach the
port, including web pages open in your browser, can spend your credits, read
your API secret from `GET /whoami`, and list, create, or delete your API
keys through `/keys`. Block the port at your firewall and stop the server
when you're not using it. To expose routes to the internet, put a reverse
proxy in front that forwards only the routes you need, as shown in [Slack
slash commands](/cli/slack#expose-the-server).
## Start the server
```bash
tomba serve
tomba serve --port 8080
```
| Flag | Short | Type | Default | Description |
| -------- | ----- | ---- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
| `--port` | `-p` | int | `3000` | Port to listen on. The server listens on all IPv4 interfaces (`0.0.0.0`); there is no option to listen on `localhost` only. |
The server reads your credentials once, at startup, in the order described in [Authentication](/cli/authentication#pass-credentials-per-command). It runs in the foreground and logs each request to standard output with the time, client IP, status, latency, method, path, and query string. Stop it with `Ctrl+C`. If the port is already in use, `tomba serve` exits immediately without an error message; choose another port with `--port`.
Each request to the server makes one API request, billed and rate-limited as described in [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits).
The [Linux packages](/cli/installation#linux-packages) install a systemd unit, `tomba.service`, that runs `tomba serve` as `www-data` and is enabled at boot.
## Send a request
```bash
curl -X POST http://localhost:3000/verify \
-H "Content-Type: application/json" \
-d '{"email": "jane.doe@stripe.com"}'
```
The lookup routes accept a JSON body or form fields. The `/people/find`, `/companies/find`, and `/combined/find` routes, and the lead, lead list, attribute, key, and flag routes, accept JSON only.
## Routes
### Lookups
| Method and path | Body fields | Calls |
| ----------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `POST /search` | `domain` | [`GET /v1/domain-search`](/api/finder#domain-search), first page |
| `POST /finder` | `domain`, plus `first_name` and `last_name` or `full_name`; `enrich_mobile` | [`GET /v1/email-finder`](/api/finder#email-finder) |
| `POST /author` | `url` | [`GET /v1/author-finder`](/api/finder#author-finder) |
| `POST /enrich` | `email`, `enrich_mobile` | [`GET /v1/enrich`](/api/finder#email-enrichment) |
| `POST /linkedin` | `url`, `enrich_mobile` | [`GET /v1/linkedin`](/api/finder#linkedin-finder) |
| `POST /verify` | `email` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `POST /sources` | `email` | [`GET /v1/email-sources`](/api/email#email-sources) |
| `POST /count` | `domain` | [`GET /v1/email-count`](/api/finder#email-count) |
| `POST /status` | `domain` | [`GET /v1/domain-status`](/api/domain#domain-status) |
| `POST /format` | `domain` | [`GET /v1/email-format`](/api/finder#email-format) |
| `POST /location` | `domain` | [`GET /v1/location`](/api/finder#location) |
| `POST /autocomplete` | `query` | [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) |
| `POST /similar` | `domain` | [`GET /v1/similar`](/api/domain#similar) |
| `POST /technology` | `domain` | [`GET /v1/technology`](/api/domain#technology) |
| `POST /phone-finder` | One of `email`, `domain`, or `linkedin`; `full` | [`GET /v1/phone-finder`](/api/phone#phone-finder) |
| `POST /phone-validator` | `phone`, `country_code` | [`GET /v1/phone-validator`](/api/phone#phone-validator) |
| `POST /reveal` | `query`, or the arrays `country`, `industry`, and `size`; `page` | [`POST /v1/reveal/search`](/api/reveal#search-companies) |
| `POST /people/find` | `email` | [`GET /v1/people/find`](/api/enrichment#person-api) |
| `POST /companies/find` | `domain` | [`GET /v1/companies/find`](/api/enrichment#company-api) |
| `POST /combined/find` | `email` | [`GET /v1/combined/find`](/api/enrichment#combined-api) |
For example, a company search with filters:
```bash
curl -X POST http://localhost:3000/reveal \
-H "Content-Type: application/json" \
-d '{"country": ["US", "GB"], "size": ["51-200"]}'
```
### Account and data
| Method and path | Calls |
| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /whoami` | [`GET /v1/me`](/api/account#get-account). The response includes your API secret. |
| `GET /usage` | [`GET /v1/usage`](/api/account#retrieve-api-usage) |
| `GET /logs` | [`GET /v1/logs`](/api/account#retrieve-api-logs), first page |
| `GET /keys`, `POST /keys` | [List](/api/keys#retrieve-api-keys) or [create](/api/keys#create-an-api-key) API keys |
| `GET /keys/{id}`, `DELETE /keys/{id}` | [Get](/api/keys#retrieve-a-single-api-key) or [delete](/api/keys#delete-an-api-key) a key |
| `PUT /keys/{id}` | Calls `PUT /v1/keys/{id}/reset`, which the API doesn't have, so it returns `500`. [Rotate keys](/authentication#rotate-keys) with the API instead. |
| `GET /flag`, `POST /flag` | [List](/api/flag#list-my-flags) or [create](/api/flag#flag-incorrect-data) flags |
| `GET /leads`, `POST /leads` | [List](/api/leads#retrieve-leads) or [create](/api/leads#create-a-lead) leads |
| `GET /leads/{id}`, `PUT /leads/{id}`, `DELETE /leads/{id}` | [Get](/api/leads#retrieve-a-single-lead), [update](/api/leads#update-a-lead), or [delete](/api/leads#delete-a-lead) a lead |
| `GET /leads_lists`, `POST /leads_lists` | [List](/api/lead-lists#retrieve-leads-lists) or [create](/api/lead-lists#create-leads-list) lead lists |
| `GET /leads_lists/{id}`, `PUT /leads_lists/{id}`, `DELETE /leads_lists/{id}` | [Get](/api/lead-lists#retrieve-a-single-leads-list), [update](/api/lead-lists#update-leads-list), or [delete](/api/lead-lists#delete-leads-list) a lead list |
| `GET /attributes`, `POST /attributes` | [List](/api/lead-attributes#retrieve-lead-attributes) or [create](/api/lead-attributes#create-a-lead-attribute) lead attributes |
| `GET /attributes/{id}`, `PUT /attributes/{id}`, `DELETE /attributes/{id}` | [Get](/api/lead-attributes#retrieve-a-single-lead-attribute), [update](/api/lead-attributes#update-a-lead-attribute), or [delete](/api/lead-attributes#delete-a-lead-attribute) an attribute |
`GET /` redirects to the Tomba website.
## Responses
A successful request returns status `200` with the Tomba API's response. Errors use the server's own format, not the API's:
| Status | Body | When |
| ------ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | `{"status": "error", "message": "..."}` | A lookup's input is missing or malformed. |
| `401` | `{"status": "error", "message": "invalid KEY or SECRET"}` | The API call failed on `/whoami`, `/usage`, `/logs`, or a lookup route not listed under `500`, for any reason: this includes rate limits (`429`) and exhausted credits (`402`), not only bad credentials. |
| `404` | `{"status": "error", "message": "..."}` | `search`, `finder`, `author`, `enrich`, `linkedin`, or `count` found nothing, or `verify` got an address at a webmail or disposable email domain. |
| `222` | `{"status": "error", "message": "..."}` | `verify` couldn't complete the check because the recipient's mail server responded unexpectedly. |
| `500` | `{"error": "error: , status code: "}` | The API call failed on `/format`, `/location`, `/autocomplete`, `/people/find`, `/companies/find`, `/combined/find`, or a lead, lead list, attribute, key, or flag route. |
Because a `401` hides the API's status, run the same lookup with the CLI, such as `tomba verify --target jane.doe@stripe.com`, to see it; see [Status codes](/error-handling#status-codes).
---
## Document: CLI commands
Reference for the tomba lookup and account commands: the API endpoint each one calls, its flags, examples, and output.
URL: /cli/commands
# CLI commands
This reference describes CLI version 1.2.3. Each command on this page calls the Tomba API with your credentials, so it uses credits and counts toward your limits exactly like the endpoint it calls: see [Credit costs](/usage-and-quotas#credit-costs) and [Rate limits](/rate-limits). Commands with their own pages: [`tomba bulk`](/cli/bulk), [`tomba chat`](/cli/chat), [`tomba serve`](/cli/http), [`tomba skill`](/cli/skill), [`tomba login` and `tomba logout`](/cli/authentication), and [`tomba version`, `tomba completion`, and `tomba update`](/cli/installation).
## Global flags
Every command accepts these flags.
| Flag | Short | Type | Default | Description |
| ------------ | ----- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `--target` | `-t` | string | — | The command's input: a domain, email address, URL, or text, depending on the command. |
| `--json` | `-j` | bool | `false` | Print the response as JSON. |
| `--yaml` | `-y` | bool | `false` | Print the response as YAML. Overrides `--json`. |
| `--csv` | — | bool | `false` | Print the response's `data` as CSV. |
| `--output` | `-o` | string | — | Also write the response as JSON to this file. For `tomba bulk` and `tomba chat --file`, the path of the output CSV. |
| `--key` | `-k` | string | — | API key. See [Pass credentials per command](/cli/authentication#pass-credentials-per-command). |
| `--secret` | `-s` | string | — | API secret. |
| `--no-color` | — | bool | `false` | Disable colored output. Setting the `NO_COLOR` environment variable to any value does the same. |
| `--port` | `-p` | int | `3000` | Port for [`tomba serve`](/cli/http). Other commands ignore it. |
| `--verbose` | — | bool | `false` | Accepted, but has no effect. |
| `--help` | `-h` | — | — | Show help for any command. |
## Output and errors
By default, commands print a formatted summary. `--json` prints the response as JSON, syntax-colored only when the output is a terminal. `--yaml` output always contains terminal color codes, even with `--no-color`, so use `--json` when you redirect or parse the output. `--csv` suits flat responses; nested objects aren't flattened.
The list subcommands of `lead`, `leads-list`, `attribute`, `key`, and `flag` number the rows in their text output but don't show IDs. Run them with `--json` to get the `id` values that `get`, `update`, and `delete` take.
When a request fails, or when `search`, `finder`, `author`, `enrich`, `linkedin`, `verify`, `sources`, or `count` has no result, the CLI prints a short message instead of the response, even with `--json`. API errors appear as `error: , status code: `, such as `error: 429 Too Many Requests, status code: 429`; the response body isn't shown. See [Status codes](/error-handling#status-codes). The exit status is `0` in all of these cases; only usage errors, such as an unknown or missing flag, exit with `1`. In scripts, parse the output rather than the exit status, and add `--no-color`.
## Aliases
| Alias | Runs |
| --------- | ----------------------- |
| `a` | `tomba author` |
| `ai` | `tomba chat` |
| `b` | `tomba bulk` |
| `c` | `tomba count` |
| `e` | `tomba enrich` |
| `l` | `tomba linkedin` |
| `pf` | `tomba phone-finder` |
| `pv` | `tomba phone-validator` |
| `r` | `tomba reveal` |
| `s` | `tomba finder` |
| `sim` | `tomba similar` |
| `sk` | `tomba skill` |
| `t` | `tomba status` |
| `tech` | `tomba technology` |
| `upgrade` | `tomba update` |
| `w` | `tomba whoami` |
The help for `tomba search` and `tomba sources` lists the alias `s`, and the help for `tomba verify` lists `t`, but those aliases run `tomba finder` and `tomba status`.
## `tomba search`
Find the email addresses Tomba has for a domain. Calls [`GET /v1/domain-search`](/api/finder#domain-search).
```text
tomba search --target [--limit 10|20|50] [--page ]
```
| Flag | Short | Type | Default | Description |
| -------------- | ----- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `--target` | `-t` | string | — | Domain to search. Required. |
| `--limit` | — | int | `10` | Addresses per page: `10`, `20`, or `50`. The CLI rejects other values. The value affects the [cost](/usage-and-quotas#credit-costs). |
| `--page` | — | int | `1` | Page of results. |
| `--department` | — | string | — | Has no effect: the CLI doesn't send it to the API. |
```bash
tomba search --target stripe.com
tomba search --target stripe.com --limit 50 --page 2
tomba search --target stripe.com --json --output stripe.json
```
The text output shows the organization, then a table of addresses with name, position, department, and confidence score. The Free plan restricts `--limit` and `--page`; see [Free plan limits](/usage-and-quotas#free-plan-limits).
## `tomba finder`
Find a person's email address from their name and company domain. Calls [`GET /v1/email-finder`](/api/finder#email-finder).
```text
tomba finder --target --first --last [--enrich-mobile]
tomba finder --target --full "" [--enrich-mobile]
```
| Flag | Short | Type | Default | Description |
| ----------------- | ----- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `--target` | `-t` | string | — | Company domain. Required. |
| `--first` | `-f` | string | — | First name. Use with `--last`. |
| `--last` | `-l` | string | — | Last name. Use with `--first`. |
| `--full` | — | string | — | Full name, instead of `--first` and `--last`. Ignored when both are set. |
| `--enrich-mobile` | — | bool | `false` | Also return the person's phone number. Phone data changes the [cost](/usage-and-quotas#credit-costs). |
```bash
tomba finder --target stripe.com --first Jane --last Doe
tomba finder --target stripe.com --full "Jane Doe" --enrich-mobile
```
## `tomba author`
Find the email address of an article's author. Calls [`GET /v1/author-finder`](/api/finder#author-finder).
```text
tomba author --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ----------------------------- |
| `--target` | `-t` | string | — | URL of the article. Required. |
```bash
tomba author --target "https://stripe.com/blog/example-article"
```
## `tomba enrich`
Look up the person behind an email address: name, position, company, social profiles, and sources. Calls [`GET /v1/enrich`](/api/finder#email-enrichment). For separate person and company profiles, use [`tomba enrichment`](#tomba-enrichment).
```text
tomba enrich --target [--enrich-mobile]
```
| Flag | Short | Type | Default | Description |
| ----------------- | ----- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `--target` | `-t` | string | — | Email address. Required. |
| `--enrich-mobile` | — | bool | `false` | Also return the person's phone number. Phone data changes the [cost](/usage-and-quotas#credit-costs). |
```bash
tomba enrich --target jane.doe@stripe.com
tomba enrich --target jane.doe@stripe.com --enrich-mobile --json
```
## `tomba enrichment`
Get a person profile, a company profile, or both. Each subcommand takes its input with `--target` and has no other flags.
```text
tomba enrichment person --target
tomba enrichment company --target
tomba enrichment combined --target
```
### `tomba enrichment person`
Get the profile of the person who owns an email address. Calls [`GET /v1/people/find`](/api/enrichment#person-api).
```bash
tomba enrichment person --target jane.doe@stripe.com
```
### `tomba enrichment company`
Get the profile of the company behind a domain. Calls [`GET /v1/companies/find`](/api/enrichment#company-api).
```bash
tomba enrichment company --target stripe.com
```
### `tomba enrichment combined`
Get the person and company profiles for an email address in one request. Calls [`GET /v1/combined/find`](/api/enrichment#combined-api).
```bash
tomba enrichment combined --target jane.doe@stripe.com --json
```
## `tomba verify`
Check whether an email address is deliverable. Calls [`GET /v1/email-verifier`](/api/verifier#email-verifier). For what the `status` and `result` values mean, see [Email verifier response](/attributes/verifier).
```text
tomba verify --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ------------------------ |
| `--target` | `-t` | string | — | Email address. Required. |
```bash
tomba verify --target jane.doe@stripe.com
tomba verify --target jane.doe@stripe.com --json
```
For an address at a webmail or disposable email domain, the CLI prints a warning instead of the result, even with `--json`. The verification still runs and uses credits as usual.
## `tomba linkedin`
Find the email address for a LinkedIn profile. Calls [`GET /v1/linkedin`](/api/finder#linkedin-finder).
```text
tomba linkedin --target [--enrich-mobile]
```
| Flag | Short | Type | Default | Description |
| ----------------- | ----- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `--target` | `-t` | string | — | LinkedIn profile URL. Required. |
| `--enrich-mobile` | — | bool | `false` | Also return the person's phone number. Phone data changes the [cost](/usage-and-quotas#credit-costs). |
```bash
tomba linkedin --target "https://www.linkedin.com/in/jane-doe-example"
```
## `tomba sources`
List the public web pages where Tomba found an email address. Calls [`GET /v1/email-sources`](/api/email#email-sources).
```text
tomba sources --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ------------------------ |
| `--target` | `-t` | string | — | Email address. Required. |
```bash
tomba sources --target jane.doe@stripe.com
```
## `tomba count`
Count the email addresses Tomba has for a domain, by department and seniority. Calls [`GET /v1/email-count`](/api/finder#email-count).
```text
tomba count --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ----------------- |
| `--target` | `-t` | string | — | Domain. Required. |
```bash
tomba count --target stripe.com
```
## `tomba status`
Check whether a domain belongs to a webmail or disposable email provider. Calls [`GET /v1/domain-status`](/api/domain#domain-status).
```text
tomba status --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ----------------- |
| `--target` | `-t` | string | — | Domain. Required. |
```bash
tomba status --target gmail.com
```
## `tomba format`
Get the email address patterns a domain uses, such as `{first}.{last}`. Calls [`GET /v1/email-format`](/api/finder#email-format).
```text
tomba format --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ----------------- |
| `--target` | `-t` | string | — | Domain. Required. |
```bash
tomba format --target stripe.com
```
## `tomba location`
Get a company's employee counts by location. Calls [`GET /v1/location`](/api/finder#location).
```text
tomba location --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ----------------- |
| `--target` | `-t` | string | — | Domain. Required. |
```bash
tomba location --target stripe.com
```
## `tomba autocomplete`
Suggest companies, with their domains and email counts, from part of a name. Calls [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions).
```text
tomba autocomplete --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ------------------------------------------------------ |
| `--target` | `-t` | string | — | Company name or domain, complete or partial. Required. |
```bash
tomba autocomplete --target strip
```
## `tomba phone-finder`
Find phone numbers from an email address, a domain, or a LinkedIn profile. Calls [`GET /v1/phone-finder`](/api/phone#phone-finder).
```text
tomba phone-finder --email [--full]
tomba phone-finder --domain [--full]
tomba phone-finder --linkedin [--full]
```
| Flag | Short | Type | Default | Description |
| ------------ | ----- | ------ | ------- | ------------------------------------------------------- |
| `--email` | — | string | — | Email address to look up. |
| `--domain` | — | string | — | Domain to look up. |
| `--linkedin` | — | string | — | LinkedIn profile URL to look up. |
| `--full` | — | bool | `false` | Return every phone number found instead of one. |
| `--target` | `-t` | string | — | Used as `--email` when none of the three inputs is set. |
Pass one input. If you pass more than one, the CLI sends only the first of `--email`, `--domain`, and `--linkedin`. The input type affects the [cost](/usage-and-quotas#credit-costs).
```bash
tomba phone-finder --email jane.doe@stripe.com
tomba phone-finder --domain stripe.com --full
tomba phone-finder --linkedin "https://www.linkedin.com/in/jane-doe-example"
```
## `tomba phone-validator`
Check whether a phone number is valid, and get its formats, line type, and carrier. Calls [`GET /v1/phone-validator`](/api/phone#phone-validator).
```text
tomba phone-validator --phone [--country-code ]
```
| Flag | Short | Type | Default | Description |
| ---------------- | ----- | ------ | ------- | ------------------------------------------------------------------------------------------------- |
| `--phone` | — | string | — | Number to validate, in international format or, with `--country-code`, in local format. Required. |
| `--country-code` | — | string | — | Two-letter country code for parsing a local number, such as `US`. |
```bash
tomba phone-validator --phone "+14155552671"
tomba phone-validator --phone 4155552671 --country-code US
```
## `tomba reveal`
Search for companies with a natural-language query or with filters. Calls [`POST /v1/reveal/search`](/api/reveal#search-companies).
```text
tomba reveal --query "" [--page ]
tomba reveal [filter flags] [--page ]
```
Use `--query` or filter flags, not both: the API rejects a request that has both. Each filter flag takes a comma-separated list, or you can repeat the flag; the CLI sends the values as `include` filters. Accepted values are listed in [Search Companies](/api/reveal#search-companies).
| Flag | Short | Type | Default | Description |
| ---------------- | ----- | ------- | ------- | ---------------------------------------------------------------------------- |
| `--query` | `-q` | string | — | Description of the companies to find, such as `Fintech companies in France`. |
| `--page` | — | int | `1` | Page of results. |
| `--country` | — | strings | — | Country codes (ISO 3166-1 alpha-2), such as `US,GB`. |
| `--state` | — | strings | — | States or regions. |
| `--city` | — | strings | — | Cities. |
| `--industry` | — | strings | — | Industries. |
| `--size` | — | strings | — | Employee-count ranges, such as `51-200`. |
| `--type` | — | strings | — | Company types. |
| `--keywords` | — | strings | — | Keywords. |
| `--founded` | — | strings | — | Founding-year ranges. |
| `--technologies` | — | strings | — | Technologies the company uses. |
| `--similar` | — | strings | — | Domains of companies to find look-alikes of. |
| `--revenue` | — | strings | — | Revenue ranges. |
| `--sic` | — | strings | — | SIC codes. |
| `--naics` | — | strings | — | NAICS codes. |
```bash
tomba reveal --query "Fintech companies in France"
tomba reveal --country US,GB --size 51-200,201-500
tomba reveal --country FR --size 51-200 --page 2 --json
```
On the Free plan, company search returns only the first page and ignores some filters; see [Free plan limits](/usage-and-quotas#free-plan-limits).
## `tomba similar`
Find companies similar to a domain. Calls [`GET /v1/similar`](/api/domain#similar).
```text
tomba similar --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ----------------- |
| `--target` | `-t` | string | — | Domain. Required. |
```bash
tomba similar --target stripe.com
```
## `tomba technology`
List the technologies detected on a domain's website. Calls [`GET /v1/technology`](/api/domain#technology).
```text
tomba technology --target
```
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ----------------- |
| `--target` | `-t` | string | — | Domain. Required. |
```bash
tomba technology --target stripe.com
```
## `tomba flag`
Report data from Tomba that's wrong, such as an address that bounced, and list your reports. Some confirmed reports return a credit; see [Refunds for incorrect data](/usage-and-quotas#refunds-for-incorrect-data).
### `tomba flag create`
Report an item. Calls [`POST /v1/flag`](/api/flag#flag-incorrect-data).
```text
tomba flag create --type --value --reason [--comment ]
```
| Flag | Short | Type | Default | Description |
| ----------- | ----- | ------ | ------- | ------------------------------------------------------------------------------------------------------ |
| `--type` | — | string | — | Kind of item: `email`, `organization`, `phone`, `author_url`, or `website`. Required. |
| `--value` | — | string | — | The item: an email address, domain, phone number, or URL. Required. |
| `--reason` | — | string | — | Why it's wrong. The valid reasons depend on `--type`; `tomba flag create --help` lists them. Required. |
| `--comment` | — | string | — | Optional details. |
```bash
tomba flag create --type email --value jane.doe@stripe.com --reason hard_bounce
tomba flag create --type phone --value "+14155552671" --reason wrong_phone --comment "Number disconnected"
```
### `tomba flag list`
List your reports and their review status. Calls [`GET /v1/flag`](/api/flag#list-my-flags). The text output leaves the reported item blank; use `--json`.
| Flag | Short | Type | Default | Description |
| --------- | ----- | ---- | ------- | ----------------- |
| `--page` | — | int | `1` | Page of results. |
| `--limit` | — | int | `10` | Reports per page. |
```bash
tomba flag list --json
```
## `tomba lead`
Manage the leads saved in your account. Get lead IDs from `tomba lead list --json`.
```text
tomba lead list [--page ] [--limit ]
tomba lead get --id
tomba lead create --email --list-id [--first-name ] [--last-name ]
tomba lead update --id [--email ] [--first-name ] [--last-name ]
tomba lead delete --id
```
### `tomba lead list`
List leads, newest first. Calls [`GET /v1/leads`](/api/leads#retrieve-leads).
| Flag | Short | Type | Default | Description |
| ---------- | ----- | ------ | ------- | ------------------------------------------------------ |
| `--page` | — | int | `1` | Page of results. |
| `--limit` | — | int | `10` | Leads per page. |
| `--domain` | — | string | — | Has no effect: the API doesn't filter leads by domain. |
### `tomba lead get`
Get one lead. Calls [`GET /v1/leads/{id}`](/api/leads#retrieve-a-single-lead). Requires `--id`.
### `tomba lead create`
Save a lead. Calls [`POST /v1/leads`](/api/leads#create-a-lead).
| Flag | Short | Type | Default | Description |
| -------------- | ----- | ------ | ------- | -------------------------------------------------------------- |
| `--email` | — | string | — | Email address. Required. |
| `--list-id` | — | string | — | Lead list ID. Required by the CLI, but not applied: see below. |
| `--first-name` | — | string | — | First name. |
| `--last-name` | — | string | — | Last name. |
The CLI sends `--list-id` as a string, which the API doesn't accept as a list ID, so the lead is saved without a list. To add a lead to a list, use the dashboard or [`POST /v1/leads`](/api/leads#create-a-lead) with a numeric `list_id`.
```bash
tomba lead create --email jane.doe@stripe.com --list-id 1 --first-name Jane --last-name Doe
```
### `tomba lead update`
Change a lead's email address or name. Calls [`PUT /v1/leads/{id}`](/api/leads#update-a-lead).
| Flag | Short | Type | Default | Description |
| -------------- | ----- | ------ | ------- | ------------------ |
| `--id` | — | string | — | Lead ID. Required. |
| `--email` | — | string | — | New email address. |
| `--first-name` | — | string | — | New first name. |
| `--last-name` | — | string | — | New last name. |
### `tomba lead delete`
Delete a lead. Calls [`DELETE /v1/leads/{id}`](/api/leads#delete-a-lead). Requires `--id`.
## `tomba leads-list`
Manage lead lists. Get list IDs from `tomba leads-list list --json`.
```text
tomba leads-list list
tomba leads-list get --id
tomba leads-list create --name
tomba leads-list update --id --name
tomba leads-list delete --id
```
### `tomba leads-list list`
List your lead lists. Calls [`GET /v1/leads_lists`](/api/lead-lists#retrieve-leads-lists). Takes no flags.
### `tomba leads-list get`
Get one list. Calls [`GET /v1/leads_lists/{id}`](/api/lead-lists#retrieve-a-single-leads-list). Requires `--id`.
### `tomba leads-list create`
Create a list. Calls [`POST /v1/leads_lists`](/api/lead-lists#create-leads-list). Requires `--name`.
```bash
tomba leads-list create --name "Q3 prospects"
```
### `tomba leads-list update`
Rename a list. Calls [`PUT /v1/leads_lists/{id}`](/api/lead-lists#update-leads-list). Requires `--id` and `--name`.
### `tomba leads-list delete`
Delete a list. Calls [`DELETE /v1/leads_lists/{id}`](/api/lead-lists#delete-leads-list). Requires `--id`.
## `tomba attribute`
Manage custom lead attributes. Get attribute IDs from `tomba attribute list --json`.
```text
tomba attribute list
tomba attribute get --id
tomba attribute create --name --type
tomba attribute delete --id
```
### `tomba attribute list`
List your custom attributes. Calls [`GET /v1/attributes`](/api/lead-attributes#retrieve-lead-attributes). Takes no flags.
### `tomba attribute get`
Get one attribute. Calls [`GET /v1/attributes/{id}`](/api/lead-attributes#retrieve-a-single-lead-attribute). Requires `--id`.
### `tomba attribute create`
Create an attribute. Calls [`POST /v1/attributes`](/api/lead-attributes#create-a-lead-attribute). Requires `--name` and `--type`; the accepted types are listed in [Create a Lead Attribute](/api/lead-attributes#create-a-lead-attribute).
```bash
tomba attribute create --name "Company size" --type string
```
### `tomba attribute update`
Calls [`PUT /v1/attributes/{id}`](/api/lead-attributes#update-a-lead-attribute) with `--id` and `--name`. The API also requires the attribute's `type`, which the CLI doesn't send, so this subcommand fails with `error: 422 Unprocessable Entity`. Rename attributes in the dashboard or with the API.
### `tomba attribute delete`
Delete an attribute. Calls [`DELETE /v1/attributes/{id}`](/api/lead-attributes#delete-a-lead-attribute). Requires `--id`.
## `tomba key`
Manage the API keys of your workspace. For how keys expire and how to replace them, see [Key expiry](/authentication#key-expiry) and [Rotate keys](/authentication#rotate-keys). Get key IDs from `tomba key list --json`.
```text
tomba key list
tomba key get --id
tomba key create
tomba key delete --id
```
### `tomba key list`
List the keys of every member of your workspace. Calls [`GET /v1/keys`](/api/keys#retrieve-api-keys). The text output shortens each key; `--json` shows full values, IDs, and expiry dates.
### `tomba key get`
Get one key. Calls [`GET /v1/keys/{id}`](/api/keys#retrieve-a-single-api-key). Requires `--id`.
### `tomba key create`
Create a key with a generated name and the default lifetime. Calls [`POST /v1/keys`](/api/keys#create-an-api-key). The text output shows only the new key's ID; run `tomba key create --json` to get the key itself, in `data.message`. The command takes no flags; to set a name or a shorter lifetime, use the dashboard or the API.
### `tomba key delete`
Delete a key. Calls [`DELETE /v1/keys/{id}`](/api/keys#delete-an-api-key). Requires `--id`. Requests that use the key fail from then on, including the CLI's own if you logged in with that key.
### `tomba key reset`
Calls `PUT /v1/keys/{id}/reset`, a route the API doesn't have, so it fails with `error: 404 Not Found`. To rotate a key, use the dashboard or [`PUT /v1/keys/{id}`](/api/keys#reset-an-api-key).
## `tomba whoami`
Show the account the CLI is signed in to. Calls [`GET /v1/me`](/api/account#get-account).
```bash
tomba whoami
tomba whoami --json
```
The text output shows your account details and plan name. `--json` adds your search and verification allowances and usage, in `requests.domains` and `requests.verifications` ([Check usage](/usage-and-quotas#check-usage)), and your API secret, in `secret_token`. Don't share `--json` or `--output` output from this command.
## `tomba usage`
Show your daily usage. Calls [`GET /v1/usage`](/api/account#retrieve-api-usage).
```bash
tomba usage
```
The CLI keeps only the verification counts from the response; search and other counts show as `0`. For your balances, use `tomba whoami --json`, the [Usage](https://app.tomba.io/usage) page of the dashboard, or [`GET /v1/usage`](/api/account#retrieve-api-usage).
## `tomba logs`
Show your 20 most recent logged requests, newest first. Calls [`GET /v1/logs`](/api/account#retrieve-api-logs). Only charged lookups are logged, including repeats charged `0`; see [Duplicate requests](/usage-and-quotas#duplicate-requests). The text output lists each request's method, URI, type, and date; `--json` adds its cost.
```bash
tomba logs
tomba logs --json
```
The command takes no paging flags. To page through older entries, call [`GET /v1/logs`](/api/account#retrieve-api-logs) with `page` and `limit`.
---
## Document: AI chat
Ask for Tomba lookups in plain language with tomba chat, which lets an OpenAI model call Tomba for you: setup, data sent to OpenAI, and batch mode.
URL: /cli/chat
# AI chat
`tomba chat` takes requests in plain language, such as "Find the head of sales at Stripe and verify their email", and lets an OpenAI model decide which Tomba lookups to run. It uses your own OpenAI API key.
## Requirements
- The CLI, signed in to Tomba ([Authentication](/cli/authentication)).
- An OpenAI API key in the `OPENAI_API_KEY` environment variable. OpenAI bills its usage to that key.
## Data and credits
The CLI sends your messages, its instructions to the model, the list of available lookups, and the full response of every lookup it runs to OpenAI's Chat Completions API (`https://api.openai.com/v1/chat/completions`), from your machine and with your key. In batch mode, it also sends values from each CSV row.
Lookup results sent to OpenAI include personal data, such as people's names,
email addresses, and phone numbers.
The model chooses the lookups, and one request can lead to several of them. Each is a Tomba API request billed as described in [Credit costs](/usage-and-quotas#credit-costs), and the CLI doesn't ask before running it.
An interactive chat saves nothing locally: the conversation is kept in memory until you exit. Batch mode writes one CSV file.
## Start a chat
```bash
export OPENAI_API_KEY="sk-..."
tomba chat
```
Type a request at the `You:` prompt. The CLI prints the lookups the model ran, such as `workflow: domain_search -> email_finder -> email_verifier`, followed by the model's answer. Type `quit`, `exit`, or `q` to leave.
| Flag | Type | Default | Description |
| ---------------- | ------ | ---------------------- | ----------------------------------------------------------- |
| `--model` | string | `gpt-4o` | OpenAI model to use. It must support tool calling. |
| `--file` | string | — | CSV file to process in batch mode. |
| `--type` | string | `enrich` | Batch operation: `enrich`, `verify`, `finder`, or `search`. |
| `--output`, `-o` | string | `_enriched.csv` | Output file in batch mode. |
The help for `--type` also lists `linkedin` and `author`, but the CLI builds no request for those types, so their rows fail.
## Lookups the model can run
| Tool | Calls |
| ------------------ | -------------------------------------------------------- |
| `domain_search` | [`GET /v1/domain-search`](/api/finder#domain-search) |
| `email_finder` | [`GET /v1/email-finder`](/api/finder#email-finder) |
| `email_verifier` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `email_enrichment` | [`GET /v1/enrich`](/api/finder#email-enrichment) |
| `email_count` | [`GET /v1/email-count`](/api/finder#email-count) |
| `email_sources` | [`GET /v1/email-sources`](/api/email#email-sources) |
| `linkedin_finder` | [`GET /v1/linkedin`](/api/finder#linkedin-finder) |
| `author_finder` | [`GET /v1/author-finder`](/api/finder#author-finder) |
| `domain_status` | [`GET /v1/domain-status`](/api/domain#domain-status) |
| `phone_finder` | [`GET /v1/phone-finder`](/api/phone#phone-finder) |
## Batch mode
```bash
tomba chat --file contacts.csv --type verify
```
For each row, the CLI sends the model one request built from the row:
| `--type` | Request sent for each row |
| ------------------ | ------------------------------------------------------------------------------------------------------- |
| `enrich`, `verify` | The value of the column named `email`, `e-mail`, `mail`, or `email_address`; without one, the whole row |
| `search` | The value of the column named `domain`, `company_domain`, or `website`; without one, the whole row |
| `finder` | Every non-empty column, as `column=value` pairs |
Rows are processed one at a time. When all rows are done, the CLI writes the output file, with your input columns followed by the same result columns as [`tomba bulk`](/cli/bulk), and prints a table of the first 20 rows. It doesn't resume an interrupted run. The input must be a comma-separated file with a header row, and every row must have the same number of fields.
To run a single lookup per row without OpenAI, use [`tomba bulk`](/cli/bulk).
## Examples
```bash
# Interactive chat with another model
tomba chat --model gpt-4o-mini
# Enrich every address in a file
tomba chat --file contacts.csv --type enrich
# Find addresses from name and company columns, writing to a chosen file
tomba chat --file prospects.csv --type finder --output prospects_found.csv
```
---
## Document: Bulk CSV processing
Process a CSV file row by row with tomba bulk: one API request per row from your machine, with column mapping, resume, and CSV output.
URL: /cli/bulk
# Bulk CSV processing
`tomba bulk` reads a CSV file, sends one API request per row from your machine, and writes the results to a new CSV file. It doesn't use the [Bulk API](/bulks), so every row is billed and rate-limited as a separate request.
## How it differs from the Bulk API
| | `tomba bulk` | [Bulk API](/bulks) |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| Where it runs | On your machine: one request per row | On Tomba's servers: one job for the whole list |
| Credits | Each row, like a single request ([Credit costs](/usage-and-quotas#credit-costs)); the [duplicate-request rule](/usage-and-quotas#duplicate-requests) applies to each row | Per job, when you first download the results ([Billing](/bulks#billing)) |
| Rate limits | Each row counts toward your [rate limits](/rate-limits) | The job limits in [Limits](/bulks#limits) |
| Results | A CSV file written as rows complete | A CSV file you download when the job completes |
## Usage
```text
tomba bulk --file --type [column flags] [--output ]
```
The input must be a comma-separated file with a header row. Rows shorter than the header are padded, and rows whose input cell is empty are skipped without a request.
## Types
| `--type` | Calls | Input columns |
| ------------------ | -------------------------------------------------------- | -------------------------------------------- |
| `enrich` (default) | [`GET /v1/enrich`](/api/finder#email-enrichment) | Email |
| `verify` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) | Email |
| `finder` | [`GET /v1/email-finder`](/api/finder#email-finder) | Domain, and first and last name or full name |
| `search` | [`GET /v1/domain-search`](/api/finder#domain-search) | Domain |
| `author` | [`GET /v1/author-finder`](/api/finder#author-finder) | Article URL |
| `linkedin` | [`GET /v1/linkedin`](/api/finder#linkedin-finder) | LinkedIn profile URL |
| `phone` | [`GET /v1/phone-finder`](/api/phone#phone-finder) | Email, domain, or LinkedIn profile URL |
| `phone-validator` | [`GET /v1/phone-validator`](/api/phone#phone-validator) | Phone number, and optionally a country code |
| `company` | [`GET /v1/companies/find`](/api/enrichment#company-api) | Domain |
| `similar` | [`GET /v1/similar`](/api/domain#similar) | Domain |
| `sources` | [`GET /v1/email-sources`](/api/email#email-sources) | Email |
## Flags
| Flag | Type | Default | Description |
| -------------------- | ------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `--file` | string | — | Input CSV file. Required. |
| `--type` | string | `enrich` | Lookup to run for each row. See [Types](#types). |
| `--column` | string | — | Email column. For `phone-validator`, also used as the phone column when no phone column is found. |
| `--domain-col` | string | — | Domain column. |
| `--first-col` | string | — | First-name column, for `finder`. |
| `--last-col` | string | — | Last-name column, for `finder`. |
| `--full-name-col` | string | — | Full-name column, for `finder`, instead of first and last name. |
| `--url-col` | string | — | URL column, for `author`, `linkedin`, and LinkedIn URLs with `phone`. |
| `--phone-col` | string | — | Phone column, for `phone-validator`. |
| `--country-code-col` | string | — | Country-code column, for `phone-validator`. |
| `--enrich-mobile` | bool | `false` | Also return phone numbers, for `enrich`, `finder`, and `linkedin`. Phone data changes the [cost](/usage-and-quotas#credit-costs). |
| `--full` | bool | `false` | Return up to 10 phone numbers per row, for `phone`. |
| `--concurrency` | int | `0` | Number of parallel workers. `0` picks a number based on your plan. The maximum is 60. |
| `--no-resume` | bool | `false` | Overwrite the output file and process every row. |
| `--output`, `-o` | string | `_enriched.csv` | Output file. |
The [global flags](/cli/commands#global-flags) `--key`, `--secret`, and `--no-color` also apply.
## Map columns
Column names are matched without regard to case. If you don't pass a column flag, the CLI looks for these header names:
| Input | Header names |
| ------------ | ----------------------------------------------------------------------- |
| Email | `email`, `e-mail`, `email_address`, `emailaddress`, `mail` |
| Domain | `domain`, `company_domain`, `website`, `company_website` |
| First name | `first_name`, `firstname`, `first`, `fname` |
| Last name | `last_name`, `lastname`, `last`, `lname` |
| Full name | `full_name`, `fullname`, `full name` |
| URL | `url`, `link`, `linkedin`, `linkedin_url`, `article_url`, `profile_url` |
| Phone | `phone`, `phone_number`, `phonenumber`, `tel`, `telephone` |
| Country code | `country_code`, `countrycode`, `country` |
`--column` sets the email column, not the domain column. For types that take a domain, pass `--domain-col`: with `--column domain`, the CLI treats that column as email addresses and asks for the domain column.
When it can't find a required column, the CLI asks you to pick one from a list. Without an interactive terminal, such as in CI, it exits with `Could not find ... column`; pass the column flags explicitly there.
## Output file
The output has your input columns followed by the result columns for the type. `search` and `similar` write one row per address or company found, repeating the input columns; `search` gets the first page of results, at the API's default `limit`. Results are written as they arrive from the workers, so the output rows can be in a different order than the input.
When the run ends, the CLI prints a summary with the number of rows found, not found, skipped, and failed with client (4xx and network) or server (5xx) errors.
## Resume a run
By default, if the output file already exists with the expected columns, the CLI skips the rows it already contains and appends the rest. A row counts as done when a row with the same input values is in the output file.
Rows that fail, for example with `429` or `402`, are written with empty result columns, so a resumed run treats them as done and doesn't retry them. To retry them, delete those rows from the output file and run the command again, or rerun everything with `--no-resume`. The CLI doesn't retry or slow down after a `429`; if the summary shows client errors, lower `--concurrency` and see [Handle 429 responses](/rate-limits#handle-429-responses).
If the output file exists but its columns don't match, for example because
you changed `--type`, the CLI starts a fresh run and overwrites the file.
The default output name doesn't include the type, so pass `--output` to keep
the results of each type in a separate file.
## Examples
```bash
# Verify the addresses in the "Email Address" column
tomba bulk --file leads.csv --type verify --column "Email Address"
# Find addresses from domain and name columns
tomba bulk --file prospects.csv --type finder --domain-col company_domain --first-col first --last-col last
# Search each domain and write the results to a chosen file
tomba bulk --file domains.csv --type search --domain-col domain --output domain-search.csv
# Validate phone numbers, using a country-code column for local numbers
tomba bulk --file phones.csv --type phone-validator --phone-col phone --country-code-col country
```
---
## Document: CLI authentication
Sign in to the Tomba CLI with your browser or an API key and secret, pass credentials per command for CI, and sign out.
URL: /cli/authentication
# CLI authentication
Commands that call the Tomba API need credentials. Sign in once with `tomba login`, or pass a key and secret with each command.
## Log in
```bash
tomba login
```
`tomba login` signs you in through your browser with the OAuth 2.0 device flow. The CLI prints a URL and a one-time code and tries to open the URL; approve the request in Tomba, and the CLI saves the session and refreshes it when needed. You can open the URL on any device, so this also works over SSH. The CLI requests the `read`, `search`, `verify`, `enrich`, `leads`, and `account` scopes ([OAuth 2.0](/authentication#oauth-20)).
After a browser login, each command first exchanges the session for your API secret (`GET /v1/me`) and the first key that `GET /v1/keys` returns, then calls the API with them. Your account therefore needs at least one API key, and these two requests count toward your [rate limits](/rate-limits) on every run. For scripts and frequent runs, sign in with a key instead.
To sign in with an API key and secret:
```bash
tomba login --api-key
```
The CLI prompts for the key and the secret, checks them with `GET /v1/me`, and saves them. Get both from [Get your key and secret](/authentication#get-your-key-and-secret). `tomba login` doesn't accept `--key` and `--secret`; passing them prints `Please logout to login.`
Credentials are saved in `~/.tomba.json`. Run `tomba whoami` to confirm which account the CLI uses.
`~/.tomba.json` stores your secret, or your OAuth tokens, in plain text, and
by default other users of the machine can read it. Restrict it with `chmod
600 ~/.tomba.json`.
## Pass credentials per command
For CI jobs and one-off runs, pass the key and secret as flags:
```bash
tomba verify --target jane.doe@stripe.com --key "$TOMBA_API_KEY" --secret "$TOMBA_SECRET_KEY"
```
The CLI doesn't read credentials from environment variables; `-k`/`--key` and `-s`/`--secret` are the only way to pass them. It picks credentials in this order:
1. A browser session saved by `tomba login`.
2. A key and secret saved by `tomba login --api-key`.
3. The `--key` and `--secret` flags.
The flags are ignored while credentials are saved. To use different credentials, run `tomba logout` first.
## Log out
```bash
tomba logout
```
`tomba logout` clears the saved key, secret, and OAuth tokens from `~/.tomba.json`. It doesn't revoke anything on the server: revoke the CLI's browser session under [Connected apps](https://app.tomba.io/settings/connected-apps), and replace a key you no longer trust as described in [Rotate keys](/authentication#rotate-keys).
## Troubleshooting
| Message | Cause and fix |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `please sign in to your account, not logged in` | No saved credentials and no `--key` and `--secret` flags. Run `tomba login`, or pass the flags. |
| `Please logout to login.` | Credentials are already saved, or you passed `--key` and `--secret` to `tomba login`. Run `tomba logout`, then log in again. |
| `OAuth token expired. Please run 'tomba login' to re-authenticate.` | The CLI couldn't refresh the saved session. Run `tomba logout`, then `tomba login`. |
| `Could not retrieve API credentials: no API keys found` | Your account has no API key. Create one on the [API keys](https://app.tomba.io/api) page, or log in with `tomba login --api-key`. |
| `error: 400 Bad Request` or `error: 401 Unauthorized` after a browser login | The first key that `GET /v1/keys` returns is the oldest key in your workspace. If it has expired or belongs to a teammate, the API rejects it with your secret. Log in with `tomba login --api-key` and your own key. |
| `error: 400 Bad Request` with saved or passed credentials | The key may have expired ([Key expiry](/authentication#key-expiry)). |
| `error: 401 Unauthorized` with saved or passed credentials | The key doesn't exist or doesn't match the secret, or the account can't use the API. See [Status codes](/error-handling#status-codes). |
---
## Document: Email verifier response
Fields returned by the Tomba Email Verifier, and every status and result value with what it means.
URL: /attributes/verifier
# Email verifier response
Returned by [`GET /email-verifier`](/api/verifier#email-verifier). The checks are in `data.email`, the public pages where the address appears are in `data.sources`, and phone numbers are in `data.phone_data` when you set `enrich_mobile=true`.
## Fields
| Field | Type | Description |
| --------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `email` | string | The verified address. Empty when the domain is disposable. |
| `status` | string | The detailed outcome. See [Status values](#status-values). |
| `result` | string | The deliverability verdict. See [Result values](#result-values). |
| `score` | integer | Confidence score. Higher means more likely to be deliverable. |
| `smtp_provider` | string or null | Mail provider detected from the domain's MX records, such as `Google`. |
| `mx.records` | array of strings or null | MX host names of the domain. |
| `mx_check` | boolean | The domain has MX records. |
| `smtp_check` | boolean | `true` when the domain has MX records, the same as `mx_check`. It doesn't show whether the mailbox exists; use `status`. |
| `smtp_server` | boolean | Same value as `smtp_check`. |
| `accept_all` | boolean | The domain's mail server accepts mail for any address (catch-all), so a mailbox can't be confirmed. |
| `greylisted` | boolean | The mail server temporarily deferred the check. |
| `block` | boolean | The mail server blocked or refused the check. Usually comes with status `unknown`. |
| `gibberish` | boolean | The part before `@` looks randomly generated. |
| `disposable` | boolean | The domain belongs to a disposable email provider. |
| `webmail` | boolean | The domain belongs to a webmail provider, such as Gmail or Outlook. |
| `regex` | boolean | The address is syntactically valid. |
| `whois` | object | Domain registration: `registrar_name`, `referral_url`, `created_date`. Each can be null. |
`data.sources` is described in [Sources](/attributes/sources). When a valid address has no public source, it contains one entry that points to the address's page on Tomba. `data.phone_data` is described in [Phone](/attributes/phone).
## Status values
| `status` | `result` | Meaning |
| ------------ | --------------- | --------------------------------------------------------------------------------------- |
| `valid` | `deliverable` | The mailbox exists and accepts mail. |
| `accept_all` | `risky` | The server accepts mail for any address, so this mailbox can't be confirmed. |
| `invalid` | `undeliverable` | The mailbox doesn't exist. |
| `unknown` | `risky` | The check was inconclusive: the server blocked it (`block` is `true`) or didn't answer. |
| `disposable` | empty | The domain is a disposable email provider. `email` and `result` are empty strings. |
Compare `status` case-insensitively: some responses return `Valid`. Webmail addresses don't have their own status; check the `webmail` field.
## Result values
| `result` | Meaning | Suggested action |
| --------------- | ------------------------------------------------------------------------- | -------------------------------------- |
| `deliverable` | Mail to this address should be delivered. | Send. |
| `undeliverable` | Mail to this address will bounce. | Remove it from your list. |
| `risky` | Delivery can't be confirmed: a catch-all domain or an inconclusive check. | Send with care, or verify again later. |
| empty | The address is on a disposable domain. | Treat it as undeliverable. |
## How results are produced
- A request that returns `disposable` or `unknown` isn't charged; see [Credits and usage](/usage-and-quotas#credit-costs).
- When the mailbox check fails, the request returns `408` with type `proxy_error`; see [Errors](/error-handling#error-types).
## Example
```json
{
"data": {
"email": {
"email": "jane.doe@wordpress.org",
"result": "deliverable",
"status": "valid",
"score": 99,
"smtp_provider": "Google",
"mx": { "records": ["aspmx.l.google.com"] },
"mx_check": true,
"smtp_server": true,
"smtp_check": true,
"accept_all": false,
"greylisted": false,
"block": false,
"gibberish": false,
"disposable": false,
"webmail": false,
"regex": true,
"whois": {
"registrar_name": "MarkMonitor Inc.",
"referral_url": "http://www.markmonitor.com",
"created_date": "2003-01-11T00:00:00Z"
}
},
"sources": [
{
"uri": "https://wordpress.org/about/",
"website_url": "wordpress.org",
"extracted_on": "2023-03-15",
"last_seen_on": "2025-01-10",
"still_on_page": true
}
]
}
}
```
---
## Document: Technology response
Fields returned by the technology endpoint: the technologies a website uses, with their categories and vendor websites.
URL: /attributes/technology
# Technology response
Returned by [`GET /technology`](/api/domain#technology) at `data`, with one entry per technology detected on the domain's website. The response also repeats the `domain` you asked about.
## Fields
| Field | Type | Description |
| ------------- | ------ | -------------------------------------------------------------------------------------------------------------- |
| `slug` | string | Identifier of the technology, such as `intercom`. |
| `name` | string | Name of the technology. |
| `icon` | string | File name of the technology's icon, such as `Intercom.svg`. It isn't a URL. |
| `website` | string | Website of the technology's vendor. |
| `description` | string | Short description. Left out when empty. |
| `categories` | array | Categories the technology belongs to, each with an `id`, a `slug`, and a `name`. Left out when there are none. |
`data` is `null` when Tomba has no technology data for the domain. When none is stored, Tomba detects the technologies live before it responds, except on the Free plan; see [Free plan limits](/usage-and-quotas#free-plan-limits).
## Example
`GET /technology?domain=stripe.com`:
```json
{
"domain": "stripe.com",
"data": [
{
"slug": "intercom",
"name": "Intercom",
"icon": "Intercom.svg",
"website": "https://www.intercom.com",
"description": "Intercom is an American software company that produces a messaging platform which allows businesses to communicate with prospective and existing customers within their app, on their website, through social media, or via email.",
"categories": [
{ "id": 52, "slug": "live-chat", "name": "Live chat" },
{ "id": 53, "slug": "crm", "name": "CRM" }
]
}
]
}
```
---
## Document: Sources response
Fields that describe where Tomba found an email address: the page URL, when the address was first and last seen, and whether it's still there.
URL: /attributes/sources
# Sources response
Returned by [`GET /email-sources`](/api/email#email-sources) at `data`, and in the `sources` array of [person](/attributes/person) and [email verifier](/attributes/verifier) results. Each entry is a public web page where the address appears.
## Fields
| Field | Type | Description |
| --------------- | ------- | ------------------------------------------------------------------- |
| `uri` | string | URL of the page where the address was found. |
| `website_url` | string | Host name of that page. |
| `extracted_on` | string | When Tomba first found the address on the page, in RFC 3339 format. |
| `last_seen_on` | string | When Tomba last saw the address on the page, in RFC 3339 format. |
| `still_on_page` | boolean | `true` if the address was on the page when Tomba last checked it. |
An address has at most 20 sources.
When an address has no public source, results can contain one entry that isn't a published page:
- A link to the address's page on Tomba, with `website_url` `tomba.io`, when the address was verified as valid.
- A Google search link, with `website_url` `google.com`, when Tomba matched the person to a LinkedIn profile.
For these entries, `extracted_on` and `last_seen_on` are the time of your request.
`GET /email-sources` also returns the address you asked about in `email`. When Tomba has no sources for it, the response has status `201` and an empty `data` array.
## Example
`GET /email-sources?email=jane.doe@stripe.com`:
```json
{
"data": [
{
"uri": "https://stripe.com/blog/engineering",
"website_url": "stripe.com",
"extracted_on": "2024-06-01T09:14:07+01:00",
"last_seen_on": "2025-01-10T17:02:44+01:00",
"still_on_page": true
}
],
"email": "jane.doe@stripe.com"
}
```
---
## Document: Similar domains response
Fields returned by the similar endpoint: companies similar to a domain, with their name and industry, 25 per page.
URL: /attributes/similar
# Similar domains response
Returned by [`GET /similar`](/api/domain#similar) at `data`, with paging information in `meta`.
## Fields
| Field | Type | Description |
| -------------------- | -------------- | --------------------------------------------------------------------------------- |
| `data[].website_url` | string | Domain of a similar company. |
| `data[].name` | string or null | Company name. |
| `data[].industries` | string or null | Industry, in the same form as the [company response](/attributes/company#values). |
| `meta.total` | integer | Number of similar companies Tomba has for the domain. |
| `meta.pageSize` | integer | Results per page. Always 25. |
| `meta.current` | integer | Current page. Request another page with `page`. |
| `meta.total_pages` | integer | Number of pages. |
`meta` also contains a `params` object whose values are always empty; ignore it.
`data` is `null` when Tomba has no similar companies for the domain. When none are stored, Tomba looks them up live before it responds, except on the Free plan; see [Free plan limits](/usage-and-quotas#free-plan-limits).
## Example
`GET /similar?domain=stripe.com`:
```json
{
"data": [
{
"website_url": "adyen.com",
"name": "Adyen",
"industries": "financial services"
},
{
"website_url": "checkout.com",
"name": "Checkout.com",
"industries": "financial services"
}
],
"meta": {
"total": 2,
"pageSize": 25,
"current": 1,
"total_pages": 1
}
}
```
---
## Document: SIC codes
Where Standard Industrial Classification (SIC) codes appear in Tomba responses and company search, and where to find the code list.
URL: /attributes/sic
# SIC codes
Tomba classifies companies with U.S. Standard Industrial Classification (SIC) codes. The code list holds four-digit codes: the first two digits give the major group, and the last two narrow the industry.
## Where codes appear
| Field or filter | Endpoint | Content |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `category.sicCode` | [`GET /companies/find`](/api/enrichment#company-api), and `data.company.category` in [`GET /combined/find`](/api/enrichment#combined-api) | SIC code of the company's industry. |
| `category.sic4Codes` | Same | Four-digit SIC codes. |
| `filters.sic.include`, `filters.sic.exclude` | [`POST /reveal/search`](/api/reveal#search-companies) | Up to 50 codes each. A code matches a company when it's in the company's `sic4Codes`. |
The company search filter matches whole four-digit codes: `7372` matches, and a shorter prefix such as `73` doesn't.
## Code list
The U.S. Occupational Safety and Health Administration publishes the SIC manual at [osha.gov/data/sic-manual](https://www.osha.gov/data/sic-manual).
---
## Document: Phone response
Fields returned by the phone finder and phone validator, and in phone_data: number formats, line type, carrier, region, and time zones.
URL: /attributes/phone
# Phone response
Returned by [`GET /phone-finder`](/api/phone#phone-finder) and [`GET /phone-validator`](/api/phone#phone-validator) at `data`. The same object makes up the `phone_data` array of [person](/attributes/person) and [email verifier](/attributes/verifier) results when you set `enrich_mobile=true`.
## Fields
| Field | Type | Description |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `valid` | boolean | `true` if the number is valid in its country's numbering plan. |
| `e164_format` | string | E.164 format, such as `+14155550132`. |
| `intl_format` | string | International format, such as `+1 415-555-0132`. |
| `local_format` | string | National format, such as `(415) 555-0132`. |
| `rfc3966_format` | string | RFC 3966 URI, such as `tel:+1-415-555-0132`. |
| `country_code` | string | Two-letter country code (ISO 3166-1 alpha-2) of the number. |
| `line_type` | string | See [Line types](#line-types). |
| `carrier` | string | Carrier the number was assigned to. Empty when unknown. |
| `region` | object or null | State or province of the number: `name` and ISO 3166-2 `code`, such as `California` and `CA`. `null` when the number doesn't identify one. |
| `timezones` | array of strings | IANA time zones of the number, such as `America/Los_Angeles`. |
| `email` | string | Phone finder only. The address the number was found for. |
| `domain` | string | Phone finder only. The domain the number was found for. |
| `linkedin` | string | Phone finder only. The LinkedIn profile URL the number was found for. |
The phone finder returns one number in `data`, preferring a valid one. With `full=true`, `data` is an array of every number found. When nothing is found, `data` is an empty array and the response repeats the `email`, `domain`, or `linkedin` you sent.
The phone validator returns `data` only for a valid number. For an invalid number, `data` is an empty array and the response repeats your input in `phone`.
## Line types
| `line_type` | Meaning |
| ---------------------- | ------------------------------------------------------------------------------- |
| `MOBILE` | Mobile number. |
| `FIXED_LINE` | Landline. |
| `FIXED_LINE_OR_MOBILE` | The numbering plan doesn't distinguish landlines from mobiles for this number. |
| `VOIP` | Internet (VoIP) number. |
| `TOLL_FREE` | Toll-free number. |
| `PREMIUM_RATE` | Premium-rate number. |
| `SHARED_COST` | Shared-cost number. |
| `PERSONAL_NUMBER` | Personal number that forwards to a mobile or landline. |
| `PAGER` | Pager. |
| `UAN` | Universal access number: one company number that routes to several offices. |
| `UNKNOWN` | The type can't be determined. Voicemail numbers are also reported as `UNKNOWN`. |
For US, Canadian, and Mexican numbers, Tomba uses the numbering plan's allocation data to replace `FIXED_LINE_OR_MOBILE` with `MOBILE`, `FIXED_LINE`, `VOIP`, or `TOLL_FREE` when the number's block is classified, and to fill in `carrier` and `region`.
## Example
`GET /phone-finder?email=jane.doe@stripe.com`:
```json
{
"data": {
"email": "jane.doe@stripe.com",
"domain": "stripe.com",
"valid": true,
"local_format": "(415) 555-0132",
"intl_format": "+1 415-555-0132",
"e164_format": "+14155550132",
"rfc3966_format": "tel:+1-415-555-0132",
"country_code": "US",
"line_type": "MOBILE",
"carrier": "AT&T",
"region": { "name": "California", "code": "CA" },
"timezones": ["America/Los_Angeles"]
}
}
```
---
## Document: Person response
Fields returned for a person by the email finder, domain search, email enrichment, LinkedIn finder, and author finder, and their values.
URL: /attributes/person
# Person response
Returned by [`GET /email-finder`](/api/finder#email-finder), [`GET /enrich`](/api/finder#email-enrichment), [`GET /linkedin`](/api/finder#linkedin-finder), and [`GET /author-finder`](/api/finder#author-finder) at `data`, and by [`GET /domain-search`](/api/finder#domain-search) at `data.emails[]`. With `full=true`, the LinkedIn finder returns an array of people in `data`.
## Fields
When a finder or enrichment request finds no address, `data.email` is `null` and the other fields are `null`, `false`, or `0`. Domain search returns an empty `data.emails` array instead.
| Field | Type | Returned by | Description |
| ----------------------- | --------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email` | string or null | All | The email address. |
| `first_name` | string or null | All | First name. |
| `last_name` | string or null | All | Last name. |
| `full_name` | string or null | All | First and last name. |
| `gender` | string or null | All | `male` or `female`. |
| `country` | string or null | All | Two-letter country code (ISO 3166-1 alpha-2). |
| `position` | string or null | All | Job title. |
| `department` | string or null | All | See [Departments](#departments). Empty or `null` when unknown. |
| `type` | string or null | All | `personal` or `generic`. See [Email types](#email-types). |
| `twitter` | string or null | All | Twitter (X) profile URL. |
| `linkedin` | string or null | All | LinkedIn profile URL. |
| `phone_number` | boolean | All | `true` when Tomba has a phone number for the person. |
| `score` | integer | All | Confidence from 0 to 100 that the address is correct, based on its last verification result and the number of public sources that list it. |
| `verification.status` | string or null | All | Result of Tomba's last check of the address: `valid`, `accept_all`, `invalid`, or `unknown`, with the meanings in [Status values](/attributes/verifier#status-values). `null` if Tomba hasn't checked it. |
| `verification.date` | string or null | All | When Tomba last checked the address, in RFC 3339 format. |
| `sources` | array | All | Public pages where the address appears. See [Sources response](/attributes/sources). |
| `phone_data` | array | All but author finder | Phone numbers, when you set `enrich_mobile=true`. See [Phone response](/attributes/phone). |
| `seniority` | string or null | Domain search | `junior`, `senior`, or `executive`. |
| `website_url` | string or null | Finders, enrichment | Company domain. |
| `company` | string or null | Finders, enrichment | Company name. |
| `pattern` | string or null | Finders, enrichment | Format of the address, such as `{first}.{last}`. See [Patterns](/attributes/format#patterns). |
| `accept_all` | boolean or null | Finders, enrichment | `true` when the domain accepts mail for any address (catch-all). |
| `disposable`, `webmail` | boolean | Email enrichment | Whether the address's domain is a disposable or webmail provider. |
| `info` | object | Author finder | The article and its author candidates. See [Author finder](#author-finder). |
## Values
### Email types
| `type` | Meaning |
| ---------- | ----------------------------------------------------------------- |
| `personal` | The address belongs to one person, such as `jane.doe@stripe.com`. |
| `generic` | A role address that a team shares, such as `support@stripe.com`. |
### Departments
`accounting`, `administrative`, `communication`, `diversity`, `education`, `engineering`, `executive`, `facilities`, `finance`, `healthcare`, `hr`, `it`, `legal`, `management`, `marketing`, `operations`, `pr`, `sales`, `security`, `software`, `support`, `warehouse`.
Domain search accepts the same values in its `department` parameter.
## Author finder
[`GET /author-finder`](/api/finder#author-finder) returns the fields above for the article's author, plus `data.info` with the article and every author candidate found on the page. `info` is also returned when no address is found, as long as the page could be read.
| Field | Type | Description |
| --------------------- | ------ | --------------------------------------------------------- |
| `info.url` | string | The article URL. |
| `info.title` | string | Article title. |
| `info.description` | string | Article description. |
| `info.og_image` | string | URL of the article's Open Graph image. |
| `info.article_domain` | string | Domain that hosts the article. |
| `info.publisher_name` | string | Name of the publication. |
| `info.publish_date` | string | Publication date, as it appears on the page. |
| `info.candidates` | array | People named as the author on the page, best match first. |
Each candidate has these fields. Fields other than the names, `name_confidence`, `score`, and `count` are left out when they're empty.
| Field | Type | Description |
| ------------------------------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------------------ |
| `full_name`, `first_name`, `last_name` | string | The candidate's name. |
| `email` | string | Address published on the page for the candidate. |
| `job_title` | string | Job title from the page. |
| `bio` | string | Biography from the page. |
| `image` | string | URL of the candidate's picture. |
| `gravatar` | boolean | `true` when `image` comes from Gravatar. |
| `author_url` | string | The candidate's author page on the publication. |
| `website` | string | The candidate's own website. |
| `linkedin`, `twitter`, `facebook`, `github`, `instagram`, `youtube`, `mastodon`, `bluesky` | string | Handles of the candidate's profiles found on the page. |
| `sources` | array of strings | Where on the page the name was found, such as `json-ld`, `meta-author`, or `byline`. |
| `name_confidence` | number | Confidence from 0 to 1 that the text is a person's name. |
| `score` | number | Ranking score from 0 to 1. Candidates are sorted by it. |
| `count` | integer | How many times the name appears on the page. |
## Example
`GET /email-finder?domain=stripe.com&first_name=Jane&last_name=Doe`:
```json
{
"data": {
"email": "jane.doe@stripe.com",
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"website_url": "stripe.com",
"company": "Stripe",
"pattern": "{first}.{last}",
"department": "engineering",
"type": "personal",
"position": "Software Engineer",
"country": "US",
"gender": "female",
"twitter": null,
"linkedin": null,
"score": 100,
"accept_all": false,
"phone_number": false,
"verification": {
"date": "2025-01-15T10:32:00+01:00",
"status": "valid"
},
"sources": [
{
"uri": "https://stripe.com/blog/engineering",
"website_url": "stripe.com",
"extracted_on": "2024-06-01T09:14:07+01:00",
"last_seen_on": "2025-01-10T17:02:44+01:00",
"still_on_page": true
}
],
"phone_data": []
}
}
```
---
## Document: NAICS codes
Where North American Industry Classification System (NAICS) codes appear in Tomba responses and company search, and where to find the code list.
URL: /attributes/naics
# NAICS codes
Tomba classifies companies with North American Industry Classification System (NAICS) codes. The code lists hold six-digit codes: the first two digits give the sector, and each further digit narrows the industry.
## Where codes appear
| Field or filter | Endpoint | Content |
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `category.naicsCode` | [`GET /companies/find`](/api/enrichment#company-api), and `data.company.category` in [`GET /combined/find`](/api/enrichment#combined-api) | NAICS code of the company's industry. |
| `category.naics6Codes` | Same | Six-digit NAICS codes. |
| `category.naics6Codes2022` | Same | Six-digit codes from the 2022 edition of NAICS. |
| `filters.naics.include`, `filters.naics.exclude` | [`POST /reveal/search`](/api/reveal#search-companies) | Up to 50 codes each. A code matches a company when it's in the company's `naics6Codes` or `naics6Codes2022`. |
The company search filter matches whole six-digit codes: `522320` matches, and a shorter prefix such as `52` doesn't.
## Code list
The U.S. Census Bureau publishes the NAICS manual and a code search at [census.gov/naics](https://www.census.gov/naics/).
---
## Document: Location response
Fields returned by the location endpoint: how many of a domain's email addresses Tomba has in each country.
URL: /attributes/location
# Location response
Returned by [`GET /location`](/api/finder#location) at `data`: for each country, the number of the domain's email addresses that belong to people in that country. To list the addresses for one country, pass the same code to [domain search](/api/finder#domain-search) in `country`.
## Fields
| Field | Type | Description |
| ------- | ------- | ------------------------------------------------------- |
| `name` | string | Two-letter country code (ISO 3166-1 alpha-2). |
| `total` | integer | Number of the domain's email addresses in that country. |
Entries are in no particular order. `data` is an empty array when Tomba has no location data for the domain.
## Example
`GET /location?domain=stripe.com`:
```json
{
"data": [
{ "name": "US", "total": 450 },
{ "name": "IE", "total": 120 },
{ "name": "SG", "total": 35 }
]
}
```
---
## Document: Email format response
Fields returned by the email format endpoint, and the tokens that make up email patterns such as {first}.{last}.
URL: /attributes/format
# Email format response
Returned by [`GET /email-format`](/api/finder#email-format) at `data`: the formats that a domain's email addresses follow, with the share of known addresses that use each one. The same pattern syntax is used by the `pattern` field of [company](/attributes/company) and [person](/attributes/person) responses.
## Fields
| Field | Type | Description |
| ------------ | ------- | --------------------------------------------------------------------------- |
| `format` | string | The pattern. See [Patterns](#patterns). |
| `percentage` | integer | Share of the domain's known addresses that use this pattern, from 0 to 100. |
`data` holds up to 20 formats, in no particular order; sort by `percentage` to find the most common. It's an empty array when Tomba has no formats for the domain.
## Patterns
A pattern combines these tokens, either joined directly or separated by `.`, `_`, or `-`:
| Token | Replaced with | For Jane Doe |
| --------- | ------------- | ------------ |
| `{first}` | First name | `jane` |
| `{last}` | Last name | `doe` |
| `{f}` | First initial | `j` |
| `{l}` | Last initial | `d` |
For example, `{first}.{last}` gives `jane.doe`, `{f}{last}` gives `jdoe`, and `{last}_{f}` gives `doe_j`.
## Example
`GET /email-format?domain=stripe.com`:
```json
{
"data": [
{ "format": "{first}", "percentage": 66 },
{ "format": "{first}.{last}", "percentage": 34 }
]
}
```
---
## Document: Clearbit-compatible enrichment
Fields returned by the person, company, and combined enrichment endpoints, which use Clearbit's response format, including fields that are always null.
URL: /attributes/enrichment
# Clearbit-compatible enrichment
Returned by [`GET /people/find`](/api/enrichment#person-api), [`GET /companies/find`](/api/enrichment#company-api), and [`GET /combined/find`](/api/enrichment#combined-api). These endpoints use Clearbit's field names; the [Clearbit migration guide](/migration/clearbit/introduction) maps them. For Tomba's own format, use [email enrichment](/attributes/person).
When there's no match, these endpoints return status `200` with an `errors` object instead of `data`: type `not_found` when Tomba has no record, and `params_invalid` for a disposable or webmail address or domain. Check for `errors` before you read `data`; see [Errors](/error-handling#error-format).
## Person fields
`GET /people/find` returns these fields at `data`, and the address you looked up at `meta.email`.
| Field | Type | Description |
| ---------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
| `email` | string | The email address. |
| `name.fullName`, `name.givenName`, `name.familyName` | string | Full, first, and last name. |
| `gender` | string or null | `male` or `female`. |
| `location` | string or null | Two-letter country code. Clearbit returns a full location here. |
| `geo.country` | string | Country name, such as `United States`. |
| `geo.countryCode` | string or null | Two-letter country code. |
| `employment.domain` | string | Employer's domain. |
| `employment.name` | string or null | Employer's name. |
| `employment.title` | string or null | Job title. |
| `employment.role` | string or null | Department, such as `engineering`. See [Departments](/attributes/person#departments). |
| `linkedin.handle` | string or null | LinkedIn profile URL. Clearbit returns a handle here. |
| `twitter.handle` | string or null | Twitter (X) profile URL. Clearbit returns a handle here. |
| `phone` | boolean | `true` when Tomba has a phone number for the person. |
| `verification.status`, `verification.date` | string or null | Tomba's last check of the address, as in the [person response](/attributes/person#fields). Not a Clearbit field. |
| `indexedAt` | string | When Tomba last updated the record, in RFC 3339 format. |
Always `null`: `timeZone`, `utcOffset`, `bio`, `site`, `avatar`, `fuzzy`, `emailProvider`, `activeAt`, `inactiveAt`, `geo.city`, `geo.state`, `geo.stateCode`, `geo.lat`, `geo.lng`, `employment.subRole`, `employment.seniority`, `facebook.handle`, every `github` field, every `gravatar` field, and every `twitter` field except `handle`.
## Company fields
`GET /companies/find` returns these fields at `data`, and the domain you looked up at `meta.domain`.
| Field | Type | Description |
| ------------------------------------------------------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | string or null | Company name. `legalName` has the same value. |
| `domain` | string | The domain. |
| `description` | string or null | Short description of the company. |
| `foundedYear` | string or null | Year founded. |
| `type` | string or null | Company type, in the same form as `company_type` in the [company response](/attributes/company#values). |
| `tags` | array of strings or null | Keywords that describe the company. |
| `location` | string or null | Two-letter country code of the headquarters. |
| `geo.country` | string | Country name. |
| `geo.countryCode` | string or null | Two-letter country code. |
| `geo.city`, `geo.state`, `geo.postalCode` | string or null | City, state or region, and postal code. `geo.stateCode` repeats `geo.state`. |
| `geo.streetAddress` | string or null | Street address. `geo.streetNumber` and `geo.streetName` repeat it. |
| `site.phoneNumbers` | array of strings or null | The company's phone number, if Tomba has one. |
| `site.emailAddresses` | array of strings or null | Generic (role) addresses at the domain. |
| `category.sicCode`, `category.sic4Codes` | string or null, array or null | SIC codes. See [SIC codes](/attributes/sic). |
| `category.naicsCode`, `category.naics6Codes`, `category.naics6Codes2022` | string or null, array or null | NAICS codes. See [NAICS codes](/attributes/naics). |
| `metrics.employees` | string or null | Employee count range, as in `company_size`. |
| `metrics.annualRevenue` | string or null | Estimated annual revenue range. `metrics.estimatedAnnualRevenue` has the same value. |
| `metrics.trafficRank` | string or null | Popularity rank of the domain on the web, as a number in a string. Lower is more popular. Clearbit returns a label such as `very_high` here. |
| `facebook.handle`, `linkedin.handle`, `twitter.handle` | string or null | Handles of the company's profiles. |
| `emailProvider` | string or null | Mail provider detected from the domain's MX records, such as `Google`. |
| `tech` | array of strings or null | Names of the technologies detected on the website. See [Technology response](/attributes/technology). |
| `techCategories` | array of strings or null | Categories of those technologies. |
| `whois` | object | Domain registration, with snake_case keys. See [WHOIS](/attributes/company#whois). Not a Clearbit field. |
| `indexedAt` | string | When Tomba last updated the record, in RFC 3339 format. |
Always `null`: `domainAliases`, `timeZone`, `utcOffset`, `logo`, `ticker`, `identifiers.usEIN`, `category.sector`, `category.industryGroup`, `category.industry`, `category.subIndustry`, `geo.subPremise`, `geo.lat`, `geo.lng`, `facebook.likes`, every `twitter` field except `handle`, `metrics.alexaUsRank`, `metrics.alexaGlobalRank`, `metrics.marketCap`, `metrics.raised`, `metrics.fiscalYearEnd`, `parent.domain`, and `ultimateParent.domain`. For the industry, use `industries` from [domain search](/attributes/company); for the logo, use the [Logo API](/logo-api).
## Combined fields
`GET /combined/find` returns the person fields at `data.person`, the company fields for the address's domain at `data.company`, and the address at `meta.email`. When Tomba has no record for the domain, every `company` field is `null` and `meta.email` is empty.
## Example
`GET /people/find?email=jane.doe@stripe.com`:
```json
{
"data": {
"name": {
"fullName": "Jane Doe",
"givenName": "Jane",
"familyName": "Doe"
},
"email": "jane.doe@stripe.com",
"location": "US",
"gender": "female",
"timeZone": null,
"utcOffset": null,
"geo": {
"city": null,
"state": null,
"stateCode": null,
"country": "United States",
"countryCode": "US",
"lat": null,
"lng": null
},
"bio": null,
"site": null,
"avatar": null,
"employment": {
"domain": "stripe.com",
"name": "Stripe",
"title": "Software Engineer",
"role": "engineering",
"subRole": null,
"seniority": null
},
"facebook": { "handle": null },
"github": {
"handle": null,
"id": null,
"avatar": null,
"company": null,
"blog": null,
"followers": null,
"following": null
},
"twitter": {
"handle": null,
"id": null,
"bio": null,
"followers": null,
"following": null,
"statuses": null,
"favorites": null,
"location": null,
"site": null,
"avatar": null
},
"linkedin": { "handle": null },
"gravatar": {
"handle": null,
"urls": null,
"avatar": null,
"avatars": null
},
"verification": {
"date": "2025-01-15T10:32:00+01:00",
"status": "valid"
},
"phone": false,
"fuzzy": null,
"emailProvider": null,
"indexedAt": "2025-04-03T13:23:24+02:00",
"activeAt": null,
"inactiveAt": null
},
"meta": { "email": "jane.doe@stripe.com" }
}
```
`GET /companies/find?domain=stripe.com`:
```json
{
"data": {
"name": "Stripe",
"legalName": "Stripe",
"domain": "stripe.com",
"domainAliases": null,
"site": {
"phoneNumbers": ["+1 415-555-0100"],
"emailAddresses": ["support@stripe.com"]
},
"category": {
"sector": null,
"industryGroup": null,
"industry": null,
"subIndustry": null,
"sicCode": null,
"sic4Codes": ["7372"],
"naicsCode": null,
"naics6Codes": ["522320"],
"naics6Codes2022": ["522320"]
},
"tags": ["payments", "billing", "fintech"],
"description": "Stripe is a financial infrastructure platform for businesses.",
"foundedYear": "2010",
"location": "US",
"timeZone": null,
"utcOffset": null,
"geo": {
"streetNumber": "354 Oyster Point Blvd",
"streetName": "354 Oyster Point Blvd",
"subPremise": null,
"streetAddress": "354 Oyster Point Blvd",
"city": "South San Francisco",
"postalCode": "94080",
"state": "California",
"stateCode": "California",
"country": "United States",
"countryCode": "US",
"lat": null,
"lng": null
},
"logo": null,
"facebook": { "handle": "StripeHQ", "likes": null },
"linkedin": { "handle": "stripe" },
"twitter": {
"handle": "stripe",
"id": null,
"bio": null,
"followers": null,
"following": null,
"location": null,
"site": null,
"avatar": null
},
"whois": {
"registrar_name": "SafeNames Ltd.",
"created_date": "1995-09-12T04:00:00Z",
"referral_url": "http://www.safenames.net"
},
"emailProvider": "Google",
"type": "private",
"ticker": null,
"identifiers": { "usEIN": null },
"metrics": {
"alexaUsRank": null,
"alexaGlobalRank": null,
"trafficRank": "310",
"employees": "5K-10K",
"marketCap": null,
"raised": null,
"annualRevenue": "$1B-$10B",
"estimatedAnnualRevenue": "$1B-$10B",
"fiscalYearEnd": null
},
"indexedAt": "2025-04-03T13:23:24+02:00",
"tech": ["Intercom", "Stripe"],
"techCategories": ["Live chat", "CRM", "Payment processors"],
"parent": { "domain": null },
"ultimateParent": { "domain": null }
},
"meta": { "domain": "stripe.com" }
}
```
---
## Document: Email count response
Fields returned by the email count endpoint: how many addresses Tomba has for a domain, by email type, department, and seniority.
URL: /attributes/email-count
# Email count response
Returned by [`GET /email-count`](/api/finder#email-count) at `data`. The counts cover the addresses Tomba has for the domain. With `type=personal` or `type=generic`, `total` and the department and seniority counts include only that type.
## Fields
| Field | Type | Description |
| ------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `total` | integer | Number of addresses of the requested `type`. |
| `personal_emails` | integer | Number of personal addresses. |
| `generic_emails` | integer | Number of generic (role) addresses. |
| `department.` | integer | Number of addresses in each department. Every [department](/attributes/person#departments) is present, with `0` when there are none. |
| `seniority.junior`, `seniority.senior`, `seniority.executive` | integer | Number of addresses at each seniority level. |
The response also has a top-level `website_id`, Tomba's internal ID for the domain.
An unknown, webmail, or disposable domain returns `0` in every count.
## Example
`GET /email-count?domain=stripe.com`:
```json
{
"data": {
"total": 1250,
"personal_emails": 1100,
"generic_emails": 150,
"department": {
"engineering": 410,
"finance": 35,
"hr": 22,
"it": 18,
"marketing": 64,
"operations": 51,
"management": 88,
"sales": 120,
"legal": 12,
"support": 47,
"communication": 9,
"executive": 15,
"accounting": 6,
"administrative": 4,
"diversity": 0,
"facilities": 2,
"pr": 5,
"security": 14,
"software": 30,
"warehouse": 0,
"healthcare": 0,
"education": 1
},
"seniority": {
"junior": 180,
"senior": 420,
"executive": 37
}
},
"website_id": 1024
}
```
---
## Document: Domain suggestions response
Fields returned by the domain suggestions endpoint: up to five companies whose name or domain matches your query.
URL: /attributes/domain-suggestions
# Domain suggestions response
Returned by [`GET /domain-suggestions`](/api/domain-suggestions#get-domain-suggestions): the companies whose name or domain matches the `query` you send, so you can turn a company name into a domain. The endpoint returns at most five suggestions and has no parameter to change that.
## Fields
| Field | Type | Description |
| -------------------- | ------- | --------------------------------------------------- |
| `data[].name` | string | Company name. |
| `data[].domain` | string | Company domain. |
| `data[].email_count` | integer | Number of email addresses Tomba has for the domain. |
| `meta.query` | string | Your query, in lowercase. |
| `meta.limit` | integer | Maximum number of suggestions. Always 5. |
`data` is an empty array when nothing matches, and `null` if the lookup fails.
## Example
`GET /domain-suggestions?query=stripe`:
```json
{
"data": [{ "name": "Stripe", "domain": "stripe.com", "email_count": 1250 }],
"meta": {
"query": "stripe",
"limit": 5
}
}
```
---
## Document: Company response
Company fields returned by domain search at data.organization, including location, social links, and WHOIS registration data.
URL: /attributes/company
# Company response
Returned by [`GET /domain-search`](/api/finder#domain-search) at `data.organization`. The [WHOIS](#whois) object is also returned by the email verifier and by company enrichment.
## Fields
| Field | Type | Description |
| --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `website_url` | string | The domain. |
| `organization` | string or null | Company name. When Tomba has no name for the company, it's derived from the domain. |
| `description` | string or null | Short description of the company. |
| `industries` | string or null | Industry. See [Values](#values). |
| `company_size` | string or null | Employee count range. See [Values](#values). |
| `company_type` | string or null | Company type. See [Values](#values). |
| `revenue` | string or null | Estimated annual revenue range. See [Values](#values). |
| `founded` | string or null | Year the company was founded. |
| `keywords` | array of strings or null | Keywords that describe the company. |
| `ranking` | integer or null | Popularity rank of the domain on the web. Lower is more popular. |
| `pattern` | string or null | Email format used at the domain, such as `{first}.{last}`. [`GET /email-format`](/api/finder#email-format) lists every format with its share. See [Patterns](/attributes/format#patterns). |
| `accept_all` | boolean | `true` when the domain's mail server accepts mail for any address (catch-all). |
| `disposable` | boolean | `true` when the domain belongs to a disposable email provider. The response then contains no addresses. |
| `webmail` | boolean | `true` when the domain belongs to a webmail provider, such as Gmail. The response then contains no addresses. |
| `phone_number` | boolean | `true` when Tomba has a phone number for the company. Get it from the [phone finder](/api/phone#phone-finder) with `domain`. |
| `total_similar` | integer or null | Number of similar companies Tomba has for the domain. List them with [`GET /similar`](/api/domain#similar). |
| `location.country` | string or null | Two-letter country code (ISO 3166-1 alpha-2) of the headquarters. |
| `location.state` | string or null | State or region. |
| `location.city` | string or null | City. |
| `location.street_address` | string or null | Street address. |
| `location.postal_code` | string or null | Postal code. |
| `social_links.twitter_url`, `facebook_url`, `linkedin_url`, `instagram_url`, `github_url`, `youtube_url`, `pinterest_url`, `tiktok_url` | string or null | URLs of the company's social profiles. |
| `whois` | object | Domain registration. See [WHOIS](#whois). |
| `last_updated` | string | When Tomba last updated the company record, in RFC 3339 format. |
## Values
`industries`, `company_size`, `company_type`, and `revenue` come from several data sources and don't follow one fixed list, so treat them as display text rather than as enums. Typical values:
| Field | Examples |
| -------------- | ------------------------------------------------- |
| `industries` | `financial services`, `marketing and advertising` |
| `company_size` | `1-10`, `11-50` |
| `company_type` | `private`, `Public Company` |
| `revenue` | `$1M-$10M`, `$10M-$50M` |
## WHOIS
Registration data for the domain, from its WHOIS record. Each field is `null` when Tomba has no WHOIS data for the domain.
| Field | Type | Description |
| ---------------- | -------------- | --------------------------------------------------- |
| `registrar_name` | string or null | Name of the registrar. |
| `referral_url` | string or null | URL of the registrar. |
| `created_date` | string or null | When the domain was registered, in RFC 3339 format. |
The same object is returned at `data.organization.whois` by domain search, at `data.email.whois` by the [email verifier](/attributes/verifier), and at `data.whois` by [company enrichment](/attributes/enrichment).
## Example
`data.organization` from `GET /domain-search?domain=stripe.com`:
```json
{
"website_url": "stripe.com",
"organization": "Stripe",
"location": {
"country": "US",
"city": "South San Francisco",
"state": "California",
"street_address": "354 Oyster Point Blvd",
"postal_code": "94080"
},
"social_links": {
"twitter_url": "https://twitter.com/stripe",
"facebook_url": "https://www.facebook.com/StripeHQ",
"linkedin_url": "https://www.linkedin.com/company/stripe",
"instagram_url": null,
"github_url": "https://www.github.com/stripe",
"youtube_url": null,
"pinterest_url": null,
"tiktok_url": null
},
"disposable": false,
"webmail": false,
"phone_number": true,
"industries": "financial services",
"founded": "2010",
"company_size": "5K-10K",
"company_type": "private",
"revenue": "$1B-$10B",
"accept_all": false,
"description": "Stripe is a financial infrastructure platform for businesses.",
"pattern": "{first}",
"total_similar": 25,
"keywords": ["payments", "billing", "fintech"],
"ranking": 310,
"whois": {
"registrar_name": "SafeNames Ltd.",
"referral_url": "http://www.safenames.net",
"created_date": "1995-09-12T04:00:00Z"
},
"last_updated": "2025-04-03T13:23:24+02:00"
}
```
---
## Document: Migrate from RocketReach
Map RocketReach person lookups to Tomba Phone Finder, Email Finder, and LinkedIn Finder, with the parameters, fields, and errors that change.
URL: /migration/phone-finder/rocketreach
# Migrate from RocketReach
This guide maps RocketReach's People Lookup, People Lookup Status, Bulk People Lookup, and Account endpoints to Tomba; the Universal API endpoints (`/universal/*`) aren't covered. RocketReach API behavior checked on 2026-09-30 against the [RocketReach API reference](https://docs.rocketreach.co/reference/people-lookup-api).
## 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.rocketreach.co/api/v2` with `https://api.tomba.io/v1`. RocketReach's person lookup takes any identifier; Tomba has one endpoint per identifier:
| RocketReach | Tomba | Notes |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `GET /person/lookup` with `linkedin_url` | [`GET /v1/linkedin`](/api/finder#linkedin-finder) with `enrich_mobile=true` | Returns the email address and the person's numbers in `data.phone_data`. |
| `GET /person/lookup` with `linkedin_url`, phones only | [`GET /v1/phone-finder`](/api/phone#phone-finder) with `linkedin` | Set `full=true` to get every number. |
| `GET /person/lookup` with `name`, `current_employer` | [`GET /v1/email-finder`](/api/finder#email-finder) with `enrich_mobile=true` | Returns the email address and the person's numbers in `data.phone_data`. |
| `GET /person/lookup` with `email` | [`GET /v1/enrich`](/api/finder#email-enrichment) with `enrich_mobile=true`, or [`GET /v1/phone-finder`](/api/phone#phone-finder) with `email` | Email Enrichment returns the person; Phone Finder returns only numbers. |
| `GET /person/lookup` with `phone` | No equivalent | Tomba doesn't look up a person from a number. [`GET /v1/phone-validator`](/api/phone#phone-validator) checks the number itself. |
| `GET /person/lookup` with `id` or `npi_number` | No equivalent | Store the LinkedIn URL, email address, or name and company instead. |
| `GET /person/checkStatus` | No equivalent | Tomba returns the final result in the lookup response. Remove the polling. |
| `POST /bulkLookup` | Bulk jobs | See [Bulk and async jobs](#bulk-and-async-jobs). |
| `GET /account/` | [`GET /v1/me`](/api/account#get-account) | See [Check usage](/usage-and-quotas#check-usage). |
## Authentication changes
RocketReach takes the key in the `Api-Key` header, or in the deprecated `api_key` query parameter. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string:
```bash
# RocketReach
curl "https://api.rocketreach.co/api/v2/person/lookup?linkedin_url=https://www.linkedin.com/in/jane-doe" \
-H "Api-Key: $ROCKETREACH_API_KEY"
# Tomba
curl "https://api.tomba.io/v1/linkedin?url=https://www.linkedin.com/in/jane-doe&enrich_mobile=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
| RocketReach | Tomba | Notes |
| ---------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `linkedin_url`, `linkedin_ext_url` | `url` on LinkedIn Finder, `linkedin` on Phone Finder | Send a full profile URL, such as `https://www.linkedin.com/in/jane-doe`. |
| `name` | `full_name` on Email Finder | Or send `first_name` and `last_name`. |
| `current_employer` | `company` on Email Finder | Send `domain` instead when you have the company's domain. |
| `email` | `email` on Email Enrichment or Phone Finder | |
| `title` | No equivalent | The response carries `position`; compare it in your code. |
| `phone`, `id`, `npi_number` | No equivalent | |
| `lookup_type` | `enrich_mobile`, or the choice of endpoint | Set `enrich_mobile=true` on Email Finder, LinkedIn Finder, or Email Enrichment to get numbers, at an extra cost; see [Credit costs](/usage-and-quotas#credit-costs). |
| `return_cached_emails` | No equivalent | Tomba doesn't return partial results. |
| `webhook_id` | `webhook_url` | Pass the callback URL on each request instead of an ID registered in the dashboard; see [Webhooks](/webhook#per-request-callbacks). |
| `metadata` | No equivalent | Keep your own IDs and tags alongside the request. |
## Response field mapping
RocketReach returns the profile at the top level. Tomba returns the person under `data`, and phone numbers under `data.phone_data[]` on the finders or `data` on Phone Finder.
### Person and email fields
| RocketReach | Tomba | Notes |
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `name` | `data.full_name` | Also `first_name` and `last_name`. |
| `current_title` | `data.position` | |
| `current_employer` | `data.company` | |
| `current_employer_domain` | `data.website_url` | |
| `linkedin_url` | `data.linkedin` | |
| `country_code` | `data.country` | Tomba uses ISO 3166-1 alpha-2 codes. |
| `recommended_email`, `recommended_professional_email`, `current_work_email` | `data.email` | `null` when no address is found; the status is still `200`. |
| `emails[].email` | `data.email` | Tomba returns one address. LinkedIn Finder returns up to two with `full=true`, without `phone_data`. |
| `emails[].smtp_valid` | `data.verification.status` | See [Status mapping](#status-mapping). |
| `emails[].last_validation_check` | `data.verification.date` | |
| `emails[].type` `role-based` | `data.type` `generic` | Tomba's `personal` means an address that belongs to one person, not a personal mailbox. |
| `emails[].grade` | `data.score` | A 0 to 100 confidence, computed differently. Recalibrate any threshold you apply to it. |
| `recommended_personal_email`, `current_personal_email` | No equivalent | |
| `current_employer_website`, `current_employer_linkedin_url`, `current_employer_industry` | [`GET /v1/companies/find`](/api/enrichment#company-api) | Call it with `data.website_url`. |
| `city`, `region`, `location`, `region_latitude`, `region_longitude` | No equivalent | |
| `job_history`, `education`, `skills`, `birth_year`, `npi_data`, `connections`, `links`, `tags`, `profile_list`, `metadata` | No equivalent | |
| `id`, `status` | No equivalent | Tomba has no lookup IDs or lookup states. |
Tomba also returns `department`, `gender`, `twitter`, `accept_all`, and `sources`; see [Person response](/attributes/person).
### Phone fields
| RocketReach | Tomba | Notes |
| ---------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `phones[].number` | `intl_format` | Tomba also returns `local_format` and `rfc3966_format`. |
| `phones[].e164` | `e164_format` | |
| `phones[].country_code` | No equivalent | RocketReach returns the calling code, such as `1`. Tomba's `country_code` is the ISO 3166-1 alpha-2 country, such as `US`. |
| `phones[].type` | `line_type` | See [Status mapping](#status-mapping). |
| `phones[].grade`, `validity` | `valid` | `valid` only says whether the number fits its country's numbering plan; there's no identity-match grade. |
| `phones[].recommended` | Phone Finder without `full` | The single number Phone Finder returns prefers a valid one. |
| `phones[].extension` | No equivalent | |
| No equivalent | `carrier`, `region`, `timezones` | See [Phone response](/attributes/phone#fields). |
On the finders, `data.phone_number` is `true` when Tomba has a number for the person; `data.phone_data` is filled only with `enrich_mobile=true`. When Phone Finder finds nothing, the status is `200` and `data` is an empty array.
## Status mapping
RocketReach's lookup `status` (`complete`, `failed`, `waiting`, `searching`, `progress`) has no equivalent: every Tomba response is final. Email and phone statuses map as follows:
| RocketReach `emails[].smtp_valid` | Tomba `data.verification.status` |
| --------------------------------- | -------------------------------- |
| `valid` | `valid` |
| `invalid` | `invalid` |
| `accept-all` | `accept_all` |
| `unknown` | `unknown` |
Tomba can also return `disposable`, or `null` when the address hasn't been verified. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
| RocketReach `phones[].type` | Tomba `line_type` |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `mobile` | `MOBILE` |
| `direct dial`, `other` | No direct equivalent. Tomba reports the line itself: `FIXED_LINE`, `VOIP`, `TOLL_FREE`, and others. |
Every value is listed in [Line types](/attributes/phone#line-types).
## Error mapping
RocketReach returns an error body with `detail` or `message`. Tomba returns an `errors` object with `type`, `message`, and `code`:
| RocketReach | Tomba | What to do |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `400` malformed or missing parameters | `422 params_invalid` | Fix the parameter named in the message. |
| `401` missing or 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. |
| `403` out of lookup credits | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `403` key lacks permission | `403` | Your plan doesn't include the feature; see [Errors](/error-handling#error-types). |
| `404` profile doesn't exist | `200` with `data.email` `null`, or an empty `data` array on Phone Finder | Check the body, not the status. |
| `429` with `Retry-After` | `429 rate_limit` with `Retry-After` | Retry after the delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `500` | `500 api_error` | Retry with backoff. |
Also handle `451 claimed_email` or `451 claimed_linkedin` 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). RocketReach's `RR-Request-ID` header corresponds to Tomba's request ID; see [Request IDs](/going-to-production#request-ids).
## Bulk and async jobs
RocketReach's `POST /bulkLookup` takes 1 to 100 queries and delivers results to a webhook. Tomba runs lists as bulk jobs, which can send an event to a `webhook_url` when they end:
| RocketReach query | Tomba bulk type |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `linkedin_url`, for phones | `phone-finder`: a JSON `list` of LinkedIn URLs, or a CSV file with a `linkedin` column. See [Phone finder](/bulks#phone-finder). |
| `email`, for phones | `phone-finder`: a JSON `list` of email addresses, or a CSV file with an `email` column. See [Phone finder](/bulks#phone-finder). |
| `linkedin_url`, for emails | `linkedin`: a JSON `list` of profile URLs. See [LinkedIn finder](/bulks#linkedin-finder). |
| `name` and `current_employer` | `finder`: a CSV file with name and company columns. See [Email finder](/bulks#email-finder). |
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). Limits are in [Limits](/bulks#limits).
## Code example
This function replaces a call to RocketReach's `GET /person/lookup` and returns RocketReach's field names, or `null` when no person is found. It needs Node.js 18 or later.
```js title="person-lookup.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
const SMTP_VALID = {
valid: "valid",
invalid: "invalid",
accept_all: "accept-all",
unknown: "unknown",
};
// Accepts RocketReach's linkedin_url, email, or name + current_employer.
async function personLookup(params) {
let path;
const query = { enrich_mobile: "true" };
if (params.linkedin_url) {
path = "linkedin";
query.url = params.linkedin_url;
} else if (params.email) {
path = "enrich";
query.email = params.email;
} else if (params.name && params.current_employer) {
path = "email-finder";
query.full_name = params.name;
query.company = params.current_employer;
} else {
throw new Error(
"Send linkedin_url, email, or name and current_employer",
);
}
const url = new URL(`https://api.tomba.io/v1/${path}`);
for (const [key, value] of Object.entries(query)) {
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}`,
);
}
const person = body.data;
const phones = person.phone_data ?? [];
if (!person.email && phones.length === 0) return null;
const status = person.verification?.status?.toLowerCase();
return {
status: "complete",
name: person.full_name,
current_title: person.position,
current_employer: person.company,
current_employer_domain: person.website_url,
linkedin_url: person.linkedin,
country_code: person.country,
recommended_professional_email: person.email,
emails: person.email
? [
{
email: person.email,
smtp_valid: SMTP_VALID[status] ?? null,
type:
person.type === "generic"
? "role-based"
: "professional",
last_validation_check: person.verification?.date ?? null,
},
]
: [],
phones: phones.map((phone) => ({
number: phone.intl_format,
e164: phone.e164_format,
type: phone.line_type === "MOBILE" ? "mobile" : "other",
})),
};
}
console.log(
await personLookup({
linkedin_url: "https://www.linkedin.com/in/jane-doe",
}),
);
```
## 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 identifier: LinkedIn URLs to `/v1/linkedin`, names and companies to `/v1/email-finder`, email addresses to `/v1/enrich`, and phone-only lookups to `/v1/phone-finder`. Add `enrich_mobile=true` where you need numbers.
3. Remove the `/person/checkStatus` polling and the lookup webhook handler, or pass `webhook_url` per request.
4. Update response parsing: read fields under `data`, numbers from `data.phone_data`, and treat `data.email` `null` or an empty `data` array as not found instead of `404`.
5. Swap error handling: `402` replaces RocketReach's out-of-credits `403`, `422` replaces `400`, and `errors` is an object.
6. Move `/bulkLookup` batches to `phone-finder`, `linkedin`, and `finder` bulk jobs.
7. Run the same sample of people through both services and compare the results before you switch production traffic.
---
## Document: Migrate from Lusha
Map Lusha person enrichment phone lookups, from GET and POST /v2/person and v3 contact enrichment, to the Tomba phone finder and email finder.
URL: /migration/phone-finder/lusha
# 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.
---
## Document: Migrate from Kaspr
Map Kaspr's LinkedIn profile API to Tomba Phone Finder and LinkedIn Finder, with the parameters, fields, and errors that change.
URL: /migration/phone-finder/kaspr
# 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.
---
## Document: Migrate from a phone finder
Guides for moving phone and mobile lookups from Lusha, ContactOut, Kaspr, or RocketReach to Tomba, and what every migration changes.
URL: /migration/phone-finder/introduction
# Migrate from a phone finder
Each guide maps one provider's phone and mobile lookups to the Tomba [Phone Finder](/api/phone#phone-finder) and [Phone Validator](/api/phone#phone-validator).
## Guides
| Provider | Guide |
| ----------- | --------------------------------------------------------------- |
| Lusha | [Migrate from Lusha](/migration/phone-finder/lusha) |
| ContactOut | [Migrate from ContactOut](/migration/phone-finder/contactout) |
| Kaspr | [Migrate from Kaspr](/migration/phone-finder/kaspr) |
| RocketReach | [Migrate from RocketReach](/migration/phone-finder/rocketreach) |
## What changes in every migration
| Area | Change |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Authentication | Every request sends `X-Tomba-Key` and `X-Tomba-Secret` headers. Tomba doesn't read keys from the query string. See [Authentication](/authentication). |
| Inputs | [`GET /v1/phone-finder`](/api/phone#phone-finder) takes an `email`, a LinkedIn profile URL in `linkedin`, or a company `domain`, alone or combined; send at least one. A `domain` returns the company's number, not a person's. The phone finder doesn't take a name: look up a person by name and company with the email finder and `enrich_mobile=true`. |
| Phones on finder endpoints | Set `enrich_mobile=true` on [`GET /v1/email-finder`](/api/finder#email-finder), [`GET /v1/linkedin`](/api/finder#linkedin-finder), [`GET /v1/enrich`](/api/finder#email-enrichment), [`GET /v1/domain-search`](/api/finder#domain-search), or [`GET /v1/email-verifier`](/api/verifier#email-verifier) to get the person's numbers in `phone_data` with the rest of the result. |
| Results | The phone finder returns one number in `data`, preferring a valid one. Set `full=true` to get an array of every number found. When nothing is found, the response is `200` with `data` as an empty array. |
| Response | Each number comes in E.164, international, national, and RFC 3966 formats, with its country, line type, carrier, region, and time zones. See [Phone response](/attributes/phone). |
| Validation | Check a number you already have with [`GET /v1/phone-validator`](/api/phone#phone-validator). A number that isn't valid returns `200` with an empty `data` array. |
| Withheld data | A claimed email address or LinkedIn profile sent to the phone finder returns `451 claimed_email` or `451 claimed_linkedin`, and claimed numbers are left out of its results. The phone validator returns `451 claimed_phone` for a claimed number. See [Status codes](/error-handling#status-codes). |
| Billing | The phone finder's cost depends on whether you send `domain`, and phone numbers returned with `enrich_mobile=true` cost extra. See [Credit costs](/usage-and-quotas#credit-costs). |
| Lists | Run a bulk job: `phone-finder` takes email addresses, LinkedIn URLs, or domains, and `phone-validator` takes phone numbers. Create the job, launch it, poll its progress, and download a CSV. See [Phone finder](/bulks#phone-finder) and [Lifecycle](/bulks#lifecycle). |
---
## Document: Migrate from ContactOut
Map ContactOut phone lookups from LinkedIn URLs, email addresses, and People Enrich, plus its batch and phone checker APIs, to Tomba.
URL: /migration/phone-finder/contactout
# Migrate from ContactOut
This guide maps the phone lookups of ContactOut's Contact Info API (single, bulk v1, and bulk v2), LinkedIn Profile API (from a LinkedIn URL and from an email address), People Enrich API, and Phone Number Checker to Tomba. ContactOut API behavior checked on 2026-09-30 against the [ContactOut API reference](https://api.contactout.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).
- 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.contactout.com` with `https://api.tomba.io`.
| ContactOut | Tomba | Notes |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /v1/people/linkedin` with `include_phone=true` | [`GET /v1/phone-finder`](/api/phone#phone-finder) with `linkedin` | For email addresses, call [`GET /v1/linkedin`](/api/finder#linkedin-finder). |
| `GET /v1/linkedin/enrich` | [`GET /v1/phone-finder`](/api/phone#phone-finder) with `linkedin` | Returns phone numbers only. For email addresses and profile data, call [`GET /v1/linkedin`](/api/finder#linkedin-finder). |
| `GET /v1/email/enrich` | [`GET /v1/enrich`](/api/finder#email-enrichment) with `enrich_mobile=true` | Returns the person and their numbers in `data.phone_data`. For numbers only, call `/v1/phone-finder` with `email`. |
| `POST /v1/people/enrich` with `linkedin_url` or `email` | [`GET /v1/phone-finder`](/api/phone#phone-finder) | See [Parameter mapping](#parameter-mapping). |
| `POST /v1/people/enrich` with a name and `company` or `company_domain` | [`GET /v1/email-finder`](/api/finder#email-finder) with `enrich_mobile=true` | Numbers are in `data.phone_data`. |
| `POST /v1/people/enrich` with `phone` | No equivalent | Tomba doesn't look people up by phone number. [`GET /v1/phone-validator`](/api/phone#phone-validator) checks the number itself. |
| `GET /v1/people/linkedin/phone_status` | [`GET /v1/linkedin`](/api/finder#linkedin-finder), `data.phone_number` | `true` when Tomba has a number for the person. Unlike ContactOut's checker, this request can be charged; see [Credit costs](/usage-and-quotas#credit-costs). |
| `POST /v1/people/linkedin/batch`, `POST /v2/people/linkedin/batch` | A [bulk job](#bulk-and-async-jobs) | |
## Authentication changes
ContactOut reads the key from the `token` header. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# ContactOut
curl -G "https://api.contactout.com/v1/people/linkedin" \
--data-urlencode "profile=https://www.linkedin.com/in/jane-doe" \
--data-urlencode "include_phone=true" \
-H "token: $CONTACTOUT_API_TOKEN"
# Tomba
curl -G "https://api.tomba.io/v1/phone-finder" \
--data-urlencode "linkedin=https://www.linkedin.com/in/jane-doe" \
--data-urlencode "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
| ContactOut | Tomba | Notes |
| --------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `profile`, `linkedin_url` | `linkedin` | On `/v1/phone-finder`. On `/v1/linkedin` the parameter is `url`. |
| `email` | `email` | On `/v1/phone-finder` or `/v1/enrich`. |
| `include_phone=true`, `include` with `phone` | Not needed | `/v1/phone-finder` returns only phone numbers. On the finder endpoints, `enrich_mobile=true` adds them. |
| `full_name`, `first_name`, `last_name` | `full_name`, `first_name`, `last_name` | On `/v1/email-finder` with `enrich_mobile=true`. |
| `company` | `company` | On `/v1/email-finder`. Tomba takes one company per request; ContactOut takes an array. |
| `company_domain` | `domain` | On `/v1/email-finder`. One domain per request. On `/v1/phone-finder`, `domain` returns the company's number. |
| No equivalent | `full` | `full=true` returns every number found as an array; without it, `data` holds one number, preferring a valid one. |
| `email_type`, `profile_only`, `phone`, `job_title`, `location`, `education` | No equivalent | |
## Response field mapping
| ContactOut | Tomba | Notes |
| --------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------- |
| `status_code` in the body | HTTP status | Tomba doesn't repeat the status in the body. |
| `profile.phone[]` (array of numbers) | `data[].e164_format` with `full=true` | Also in `intl_format`, `local_format`, and `rfc3966_format`. |
| `profile.phone` (a string, from `/v1/email/enrich`) | `data.phone_data[].e164_format` | On `/v1/enrich` with `enrich_mobile=true`. |
| `profile.url` | `data.linkedin` | Only on a number found for the LinkedIn URL you sent. |
| `profile.email`, `work_email`, `personal_email` | No equivalent on the phone finder | Call `/v1/linkedin` or `/v1/enrich` for the email address. |
| `profile.github`, `twitter`, and profile details | No equivalent on the phone finder | For profile data, see the [Person API](/api/enrichment#person-api). |
| Phone checker `profile.phone` | `data.phone_number` on `/v1/linkedin` | |
Tomba also returns `valid`, `country_code`, `line_type`, `carrier`, `region`, and `timezones` for each number. Every field is described in [Phone response](/attributes/phone).
## Status mapping
| ContactOut | Tomba |
| ----------------------------------- | --------------------------------------------------------------------------------------------- |
| `404` with `"message": "Not Found"` | `200` with `data` as an empty array |
| Empty `phone` or `phones` array | `200` with `data` as an empty array |
| No equivalent | `valid` is `false`: the number isn't valid in its country's numbering plan |
| No equivalent | `line_type`, such as `MOBILE` or `FIXED_LINE`; see [Line types](/attributes/phone#line-types) |
With `full=true`, filter on `valid` if you only want valid numbers.
## Error mapping
ContactOut returns `status_code` and `message`. Tomba returns an `errors` object with `type`, `message`, and `code`:
| ContactOut | Tomba | What to do |
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `400` or `401` bad credentials or invalid headers | `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. |
| `400` or `401` bad request or invalid input | `422 params_invalid` | Fix the parameter named in the message. Sending none of `email`, `domain`, and `linkedin` to `/v1/phone-finder` also returns `422`. |
| `403` out of credits | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `404` not found | `200` with `data` as an empty array | Treat as no match. |
| `429` rate limit, with `retry-after` | `429 rate_limit`, with `Retry-After` | Retry after the delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| No equivalent | `451 claimed_email` or `451 claimed_linkedin` | The person asked Tomba to stop processing their data. Remove the record from your data. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
ContactOut's bulk v1 endpoint answers up to 100 LinkedIn URLs synchronously, and bulk v2 returns a `job_id` to poll or a `callback_url` to notify. A Tomba bulk job runs in the background: create it, launch it, poll its progress, and download the results as CSV.
| ContactOut | Tomba bulk type |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `profiles` with `include_phone: true` | `phone-finder`: a JSON `list` or CSV file of LinkedIn URLs or email addresses. See [Phone finder](/bulks#phone-finder). |
| `GET /v2/people/linkedin/batch/{job_id}` | Poll the job's progress, then download its results. See [Lifecycle](/bulks#lifecycle). |
| `callback_url` | `webhook_url`, which receives a signed event when the job ends. See [Bulk job events](/webhook#bulk-job-events). |
ContactOut keys batch results by profile URL. A Tomba JSON `list` is de-duplicated and reordered, so join the CSV rows back to your records on the LinkedIn URL, not on position. A job is charged when you first download its results; see [Billing](/bulks#billing).
## Code example
This function replaces a call to `GET /v1/people/linkedin` with `include_phone=true` and returns ContactOut's `profile.url` and `profile.phone` fields, or `null` when no number is found. 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,
};
// Accepts ContactOut's profile parameter: a LinkedIn profile URL.
async function linkedinPhones(profile) {
const url = new URL("https://api.tomba.io/v1/phone-finder");
url.searchParams.set("linkedin", profile);
url.searchParams.set("full", "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}`,
);
}
if (body.data.length === 0) return null;
return {
url: profile,
phone: body.data.map((phone) => phone.e164_format),
};
}
console.log(await linkedinPhones("https://www.linkedin.com/in/jane-doe"));
```
## 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 `token`.
2. Route each lookup by its identifier: LinkedIn URL or email address to `/v1/phone-finder`, name and company to `/v1/email-finder` with `enrich_mobile=true`. Drop lookups by phone number.
3. Read numbers from `data` (with `full=true`) or `data.phone_data` instead of `profile.phone`, and use the HTTP status instead of `status_code`.
4. Treat an empty `data` array as no match, in place of `404`.
5. Swap error handling: `402` replaces the out-of-credits `403`, `422` covers invalid input, and `451` carries a `claimed_*` type.
6. Move batch lookups to Tomba bulk jobs and poll their progress instead of waiting for `callback_url`.
7. Run the same sample of profiles through both services and compare the numbers before you switch production traffic.
---
## Document: Migrate from ZeroBounce
Map ZeroBounce single validation, batch, bulk file, and credits API calls, statuses, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/zerobounce
# Migrate from ZeroBounce
This guide maps the ZeroBounce API v2 (single validation, batch validation, the bulk file API, and credit balance) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). ZeroBounce API behavior checked on 2026-09-30 against the [ZeroBounce API reference](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-validate-emails).
## 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.
## Endpoint mapping
ZeroBounce uses the hosts `api.zerobounce.net` (or `api-us`, `api-eu`) and `bulkapi.zerobounce.net`. Tomba serves everything from `api.tomba.io`.
| ZeroBounce | Tomba |
| ------------------------------------------------- | --------------------------------------------------------------------- |
| `GET /v2/validate` (or `POST`) | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `POST /v2/validatebatch` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `POST https://bulkapi.zerobounce.net/v2/sendfile` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET /v2/filestatus` | `GET /v1/bulk/verifier/{id}/progress` |
| `GET /v2/getfile` | `GET /v1/bulk/verifier/{id}/download` |
| `GET /v2/deletefile` | `DELETE /v1/bulk/verifier/{id}/delete` |
| `GET /v2/getcredits` | [`GET /v1/me`](/api/account#get-account) |
## Authentication changes
ZeroBounce reads the key from the `api_key` parameter, in the query string or the request body. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string or body:
```bash
# ZeroBounce
curl "https://api.zerobounce.net/v2/validate?api_key=$ZEROBOUNCE_API_KEY&email=jane.doe@stripe.com"
# 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"
```
Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| ZeroBounce | Tomba | Notes |
| -------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------- |
| `api_key` | `X-Tomba-Key` and `X-Tomba-Secret` headers | |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `ip_address` | No equivalent | |
| `timeout` | No equivalent | Set the timeout in your HTTP client. |
| `activity_data`, `verify_plus` | No equivalent | |
| Batch `email_batch[].email_address` | `list` | A newline-separated string of addresses. |
| Send file `file` | `file` | Must be sent with the content type `text/csv`. Also send `name`. |
| `email_address_column` | `email_field_index` | Zero-based in Tomba: subtract 1. |
| `has_header_row` | No equivalent | Tomba always reads the first row as a header. |
| `return_url` | `webhook_url` | Receives a signed event when the job ends. See [Bulk job events](/webhook#bulk-job-events). |
| `first_name_column`, `last_name_column`, `gender_column` | No equivalent | |
| `ip_address_column`, `remove_duplicate`, `allow_phase_2` | No equivalent | |
| `file_id` | `{id}` in the path | |
| Get file `download_type`, `activity_data` | No equivalent | Tomba's download `type` selects `full` or `valid` rows; see [Lifecycle](/bulks#lifecycle). |
## Response field mapping
ZeroBounce 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).
| ZeroBounce | Tomba | Notes |
| -------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------- |
| `address` | `data.email.email` | An empty string when the domain is disposable. |
| `status` | `data.email.status`, `data.email.result` | See [Status mapping](#status-mapping). |
| `sub_status` | No equivalent | Tomba reports `greylisted`, `block`, `disposable`, and `mx_check` as fields. |
| `free_email` | `data.email.webmail` | |
| `catchall_domain` | `data.email.accept_all` | |
| `mx_found` | `data.email.mx_check` | |
| `mx_record` | `data.email.mx.records` | An array of MX host names instead of one string. |
| `smtp_provider` | `data.email.smtp_provider` | |
| `domain_age_days` | No equivalent | `data.email.whois.created_date` gives the domain's registration date. |
| `account`, `domain` | No equivalent | Split the address at `@`. |
| `did_you_mean` | No equivalent | |
| `active_in_days`, `active_first_seen` | No equivalent | |
| `firstname`, `lastname`, `gender` | No equivalent | |
| `city`, `region`, `zipcode`, `country` | No equivalent | |
| `processed_at` | No equivalent | |
| `error` | `errors.message` | Returned with an HTTP error status; see [Error mapping](#error-mapping). |
Tomba also returns `score`, `block`, `greylisted`, `gibberish`, `regex`, and `whois`, which have no ZeroBounce counterpart. Read your remaining balance from the `X-Verify-Remaining` header or `GET /v1/me`; see [Check usage](/usage-and-quotas#check-usage).
## Status mapping
| ZeroBounce `status` | `sub_status` | Tomba `status` | Tomba `result` |
| ------------------- | --------------------- | ------------------------------------------------------- | --------------- |
| `valid` | Any | `valid` | `deliverable` |
| `invalid` | Any | `invalid` | `undeliverable` |
| `invalid` | `failed_syntax_check` | No status. The request fails with `422 params_invalid`. | |
| `catch-all` | Any | `accept_all` | `risky` |
| `unknown` | Any | `unknown` | `risky` |
| `do_not_mail` | `disposable` | `disposable` | Empty string |
| `do_not_mail` | Other sub-statuses | No equivalent. Tomba returns the mailbox result. | |
| `spamtrap`, `abuse` | | No equivalent. Tomba returns the mailbox result. | |
ZeroBounce returns some catch-all domains as `valid` with the `accept_all` sub-status. Tomba sets `status` to `accept_all` whenever the domain's mail server accepts mail for any address. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
ZeroBounce reports failures in the body: an `error` string on single validation and credits, an `errors` array on batch validation, and `success: false` with `error_message` on the file endpoints. Tomba returns an HTTP error status with an `errors` object, so check the status code instead:
| ZeroBounce | Tomba | What to do |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `Invalid API Key or your account ran out of credits` | `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. |
| `Invalid API Key or your account ran out of credits` | `402 quota_exceeded` | Tomba separates the two cases. Wait for your usage window to renew or add credits. |
| Temporary block after exceeding the rate limit | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| 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
ZeroBounce's `validatebatch` returns results in the response. Tomba runs every list as an asynchronous `verifier` bulk job, so move batch calls to the same flow as your file uploads, or send one `GET /v1/email-verifier` request per address.
| ZeroBounce | Tomba |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `validatebatch` with `email_batch` | `POST /v1/bulk/verifier` with `name` and a newline-separated `list` of addresses |
| `sendfile` with `file` and `email_address_column` | `POST /v1/bulk/verifier` with `name`, a CSV `file`, and `email_field_index` |
| Validation starts on upload | Launch the job: `PUT /v1/bulk/verifier/{id}` |
| Poll `filestatus` for `file_status` and `complete_percentage` | Poll `GET /v1/bulk/verifier/{id}/progress` for `status` and `progress` until `status` is `completed` |
| The `return_url` callback | `webhook_url`. See [Bulk job events](/webhook#bulk-job-events). |
| `getfile` | `GET /v1/bulk/verifier/{id}/download?file=full` |
```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" \
-F "name=Newsletter list" \
-F "file=@emails.csv;type=text/csv" \
-F "email_field_index=0"
```
Tomba treats the first row of the file as a header, so add one if your ZeroBounce files have none. 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 `/v2/validate` and returns ZeroBounce's field names for the values Tomba supplies. It needs Node.js 18 or later.
```js title="validate.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
// Tomba status -> ZeroBounce [status, sub_status]
const STATUSES = {
valid: ["valid", ""],
invalid: ["invalid", ""],
accept_all: ["catch-all", ""],
unknown: ["unknown", ""],
disposable: ["do_not_mail", "disposable"],
};
async function validate(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) {
// Unlike ZeroBounce, errors use HTTP status codes, such as 402 or 429.
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
const [status, sub_status] =
STATUSES[check.status.toLowerCase()] ?? STATUSES.unknown;
return {
address: check.email || email,
status,
sub_status,
free_email: check.webmail,
catchall_domain: check.accept_all,
mx_found: check.mx_check,
mx_record: check.mx?.records?.[0] ?? null,
smtp_provider: check.smtp_provider,
};
}
console.log(await validate("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` from your URLs, request bodies, and logs.
2. Point single validations at `GET https://api.tomba.io/v1/email-verifier`, drop `timeout`, and set a client timeout of at least 180 seconds.
3. Replace checks of the `error` field with HTTP status handling: `402`, `422`, `429` with `Retry-After`, `408`, and `451`.
4. Translate `status` with the [status mapping](#status-mapping), comparing it case-insensitively, and stop branching on `sub_status`.
5. Replace `validatebatch` and `sendfile` with `verifier` bulk jobs: create, launch, receive the `webhook_url` event instead of `return_url`, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from NeverBounce
Map NeverBounce single check, jobs, and account API calls, result codes, flags, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/neverbounce
# Migrate from NeverBounce
This guide maps the NeverBounce API v4.2 (single check, jobs, and account info) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). NeverBounce API behavior checked on 2026-09-29 against the [NeverBounce API reference](https://developers.neverbounce.com/).
## 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. A client timeout of about 10 seconds, as NeverBounce suggests, cuts off Tomba verifications that take longer.
## Endpoint mapping
Replace the base URL `https://api.neverbounce.com/v4.2` (or `/v4`) with `https://api.tomba.io`.
| NeverBounce | Tomba |
| ------------------------------- | ---------------------------------------------------------- |
| `GET /single/check` (or `POST`) | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `POST /jobs/create` | `POST /v1/bulk/verifier` |
| `POST /jobs/parse` | Not needed. Tomba parses the list when you create the job. |
| `POST /jobs/start` | `PUT /v1/bulk/verifier/{id}` |
| `GET /jobs/status` | `GET /v1/bulk/verifier/{id}/progress` |
| `GET /jobs/results` | No equivalent. Download the job's CSV. |
| `GET /jobs/download` | `GET /v1/bulk/verifier/{id}/download` |
| `POST /jobs/delete` | `DELETE /v1/bulk/verifier/{id}/delete` |
| `GET /jobs/search` | `GET /v1/bulk/verifier` |
| `GET /account/info` | [`GET /v1/me`](/api/account#get-account) |
## Authentication changes
NeverBounce reads the key from the `key` parameter. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string or body:
```bash
# NeverBounce
curl "https://api.neverbounce.com/v4.2/single/check?key=$NEVERBOUNCE_API_KEY&email=jane.doe@stripe.com"
# 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"
```
Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| NeverBounce | Tomba | Notes |
| -------------- | ------------- | ------------------------------------------------------------------------------------------------------- |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `timeout` | No equivalent | Set the timeout in your HTTP client. |
| `address_info` | No equivalent | |
| `credits_info` | No equivalent | Read the `X-Verify-Remaining` header or `GET /v1/me`; see [Check usage](/usage-and-quotas#check-usage). |
## Response field mapping
NeverBounce 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).
| NeverBounce | Tomba | Notes |
| ---------------------- | ----------------------------------------- | ------------------------------------------------------ |
| `status` | HTTP status code, `errors.type` | See [Error mapping](#error-mapping). |
| `result` | `data.email.status`, `data.email.result` | See [Status mapping](#status-mapping). |
| `flags` | Booleans in `data.email` | See the flag table below. |
| `suggested_correction` | No equivalent | |
| `execution_time` | No equivalent | |
| `address_info` | No equivalent | `data.email.email` holds the address that was checked. |
| `credits_info` | `X-Verify-Remaining` header, `GET /v1/me` | See [Check usage](/usage-and-quotas#check-usage). |
| NeverBounce flag | Tomba field |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `has_dns_mx` | `data.email.mx_check` is `true` |
| `free_email_host` | `data.email.webmail` is `true` |
| `disposable_email` | `data.email.disposable` is `true` |
| `accepts_all` | `data.email.accept_all` is `true` |
| `bad_syntax` | The request fails with `422 params_invalid` |
| `has_dns`, `bad_dns`, `temporary_dns_error`, `smtp_connectable`, `connect_fails`, `role_account`, `profanity`, `government_host`, `academic_host`, `military_host`, `international_host`, `squatter_host`, `spamtrap_network`, `spelling_mistake`, `contains_alias`, `contains_subdomain`, `historical_response` | No equivalent |
Tomba also returns `block`, `greylisted`, `gibberish`, `regex`, `smtp_provider`, `mx.records`, and `whois`, which have no NeverBounce counterpart.
## Status mapping
| NeverBounce `result` | Tomba `status` | Tomba `result` |
| -------------------- | -------------- | --------------- |
| `valid` | `valid` | `deliverable` |
| `invalid` | `invalid` | `undeliverable` |
| `disposable` | `disposable` | Empty string |
| `catchall` | `accept_all` | `risky` |
| `unknown` | `unknown` | `risky` |
Branch on `status`: it has one value for each NeverBounce result, while `result` groups `accept_all` and `unknown` as `risky`. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
NeverBounce reports failures in the `status` field of an HTTP 200 response. Tomba returns an HTTP error status with an `errors` object, so check the status code rather than a field in the body.
| NeverBounce `status` | Tomba | What to do |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `auth_failure` | `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. |
| `throttle_triggered` | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `temp_unavail` | `500 api_error`, `504`, or `408 proxy_error` | Retry with backoff. `408` means the mailbox check failed. |
| `general_failure` | `422 params_invalid` for a bad parameter, `402 quota_exceeded` when your credits don't cover the request | Read `errors.type` to find the cause. |
| `bad_referrer` | No equivalent | |
| 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
| NeverBounce | Tomba |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `jobs/create` with `input_location: supplied` and `input` | `POST /v1/bulk/verifier` with `name` and a newline-separated `list` of addresses |
| `jobs/create` with `input_location: remote_url` | No equivalent. Upload the file as `file`; set `email_field_index` (zero-based) if its header doesn't name the address column. |
| `auto_parse`, `jobs/parse` | Not needed |
| `auto_start`, `jobs/start` | Launch with `PUT /v1/bulk/verifier/{id}` |
| `run_sample` | No equivalent |
| `callback_url` | `webhook_url`, which receives a signed `bulk.completed` event. See [Bulk job events](/webhook#bulk-job-events). |
| `jobs/download` segmentation (`valids`, `invalids`, …) | `GET /v1/bulk/verifier/{id}/download` with `file=full` or `file=valid` |
```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 `single/check` and returns NeverBounce's `result` value and the flags Tomba can supply. 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 -> NeverBounce result
const RESULTS = {
valid: "valid",
invalid: "invalid",
disposable: "disposable",
accept_all: "catchall",
unknown: "unknown",
};
async function singleCheck(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) {
// Failures use HTTP status codes, for example 402, 429, or 451.
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
const flags = [];
if (check.mx_check) flags.push("has_dns_mx");
if (check.webmail) flags.push("free_email_host");
if (check.disposable) flags.push("disposable_email");
if (check.accept_all) flags.push("accepts_all");
return { result: RESULTS[check.status.toLowerCase()] ?? "unknown", flags };
}
console.log(await singleCheck("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 the `key` parameter from your URLs and logs.
2. Point single checks at `GET https://api.tomba.io/v1/email-verifier` and raise your client timeout to at least 180 seconds.
3. Replace checks of the `status` field with HTTP status handling: `402`, `429` with `Retry-After`, `451`, and `408`.
4. Translate `result` and flags with the [status mapping](#status-mapping) and flag table, comparing `status` case-insensitively.
5. Replace jobs with `verifier` bulk jobs: create with `list` or `file`, launch, receive the `webhook_url` event instead of the callback, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from MillionVerifier
Map MillionVerifier single verification, bulk file, and credits API calls, result codes, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/millionverifier
# Migrate from MillionVerifier
This guide maps the MillionVerifier API (single verification v3, the bulk file API v2, and credits) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). MillionVerifier API behavior checked on 2026-09-29 against the [MillionVerifier API reference](https://developer.millionverifier.com/).
## 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.
## Endpoint mapping
MillionVerifier uses two hosts, `api.millionverifier.com` and `bulkapi.millionverifier.com`. Tomba serves everything from `api.tomba.io`.
| MillionVerifier | Tomba |
| ------------------------------------------------------------ | --------------------------------------------------------------------- |
| `GET https://api.millionverifier.com/api/v3/` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `POST https://bulkapi.millionverifier.com/bulkapi/v2/upload` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET /bulkapi/v2/fileinfo` | `GET /v1/bulk/verifier/{id}/progress` |
| `GET /bulkapi/v2/filelist` | `GET /v1/bulk/verifier` |
| `GET /bulkapi/v2/download` | `GET /v1/bulk/verifier/{id}/download` |
| `GET /bulkapi/stop` | No equivalent |
| `GET /bulkapi/v2/delete` | `DELETE /v1/bulk/verifier/{id}/delete` |
| `GET https://api.millionverifier.com/api/v3/credits` | [`GET /v1/me`](/api/account#get-account) |
## Authentication changes
MillionVerifier reads the key from the query string: `api` on the single and credits endpoints, `key` on the bulk endpoints. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string:
```bash
# MillionVerifier
curl "https://api.millionverifier.com/api/v3/?api=$MILLIONVERIFIER_API_KEY&email=jane.doe@stripe.com&timeout=10"
# 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"
```
Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| MillionVerifier | Tomba | Notes |
| -------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `api`, `key` | `X-Tomba-Key` and `X-Tomba-Secret` headers | |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `timeout` | No equivalent | Set the timeout in your HTTP client. |
| Upload `file_contents` | `file` | Also send `name`, and `email_field_index` (zero-based) if the header doesn't name the address column. |
| Download `filter=all` | `file=full` | |
| Download `filter=ok` | `file=valid` | |
| Download `filter` `ok_and_catch_all`, `unknown`, `invalid`, `custom` | No equivalent | Download `file=full` and filter the rows. |
| `file_id` | `{id}` in the path | |
## Response field mapping
MillionVerifier 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).
| MillionVerifier | Tomba | Notes |
| ---------------------- | ----------------------------------------- | --------------------------------------------------------------------------- |
| `email` | `data.email.email` | An empty string when the domain is disposable. |
| `result`, `resultcode` | `data.email.status`, `data.email.result` | See [Status mapping](#status-mapping). |
| `quality` | `data.email.result` | The closest field; see [Result values](/attributes/verifier#result-values). |
| `subresult` | No equivalent | Tomba reports `greylisted`, `block`, and `mx_check` as separate fields. |
| `free` | `data.email.webmail` | |
| `role` | No equivalent | |
| `didyoumean` | No equivalent | |
| `credits` | `X-Verify-Remaining` header, `GET /v1/me` | See [Check usage](/usage-and-quotas#check-usage). |
| `executiontime` | No equivalent | |
| `error` | `errors.message` | Returned with an HTTP error status; see [Error mapping](#error-mapping). |
| `livemode` | No equivalent | |
Tomba also returns `accept_all`, `disposable`, `gibberish`, `regex`, `smtp_provider`, `mx.records`, and `whois`, which have no MillionVerifier counterpart.
## Status mapping
| MillionVerifier `result` | `resultcode` | Tomba `status` | Tomba `result` |
| ------------------------ | ------------ | ------------------------------------------------ | --------------- |
| `ok` | 1 | `valid` | `deliverable` |
| `catch_all` | 2 | `accept_all` | `risky` |
| `unknown` | 3 | `unknown` | `risky` |
| `error` | 4 | No status. The request fails with an HTTP error. | |
| `disposable` | 5 | `disposable` | Empty string |
| `invalid` | 6 | `invalid` | `undeliverable` |
Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
MillionVerifier returns errors with HTTP 200, as `result: "error"` and an `error` string on the single endpoint, or an `error` field on the bulk endpoints. Tomba returns an HTTP error status with an `errors` object, so check the status code instead:
| MillionVerifier | Tomba | What to do |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `apikey_not_found` (single), `invalid_api_key` (bulk) | `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. |
| `No email specified` | `422 params_invalid` | Fix the parameter named in the message. |
| `insufficient_credits` (bulk upload) | `402 quota_exceeded` on single requests | For bulk jobs, the balance is checked at the first download instead; see [Billing](/bulks#billing). |
| No equivalent | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| 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
| MillionVerifier | Tomba |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Upload a file with `file_contents` | `POST /v1/bulk/verifier` with `name` and a CSV `file`, or a newline-separated `list` of addresses |
| No start call | Launch the job: `PUT /v1/bulk/verifier/{id}` |
| Poll `fileinfo` for `status` and `percent` | Poll `GET /v1/bulk/verifier/{id}/progress` for `status` and `progress` until `status` is `completed` |
| The `file_ready` webhook, set in account settings | `webhook_url` on the job, which receives a signed `bulk.completed` event. See [Bulk job events](/webhook#bulk-job-events). |
| `download` with a `filter` | `GET /v1/bulk/verifier/{id}/download?file=full` |
```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" \
-F "name=Newsletter list" \
-F "file=@emails.csv;type=text/csv" \
-F "email_field_index=0"
```
Tomba treats the first row of the file as a header. 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 `/api/v3/` and returns MillionVerifier's `result` and `resultcode` for the address. 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 -> MillionVerifier [result, resultcode]
const RESULTS = {
valid: ["ok", 1],
accept_all: ["catch_all", 2],
unknown: ["unknown", 3],
disposable: ["disposable", 5],
invalid: ["invalid", 6],
};
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) {
// Unlike MillionVerifier, errors use HTTP status codes, such as 402 or 429.
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
const [result, resultcode] =
RESULTS[check.status.toLowerCase()] ?? RESULTS.unknown;
return {
email: check.email || email,
result,
resultcode,
free: check.webmail,
};
}
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 the `api` and `key` parameters from your URLs and logs.
2. Point single verifications at `GET https://api.tomba.io/v1/email-verifier`, drop `timeout`, and set a client timeout of at least 180 seconds.
3. Replace checks of `result: "error"` with HTTP status handling: `402`, `422`, `429` with `Retry-After`, `408`, and `451`.
4. Translate `result` and `resultcode` with the [status mapping](#status-mapping), comparing `status` case-insensitively.
5. Replace file uploads with `verifier` bulk jobs: create, launch, receive the `webhook_url` event instead of the `file_ready` webhook, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from Kickbox
Map Kickbox single verification, batch, and balance API calls, results, reasons, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/kickbox
# Migrate from Kickbox
This guide maps the Kickbox API v2 (single verification, batch verification, and balance) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). Kickbox API behavior checked on 2026-09-29 against Kickbox's official client libraries on [GitHub](https://github.com/kickboxio).
## 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.
## Endpoint mapping
Replace the host `api.kickbox.com` with `api.tomba.io`.
| Kickbox | Tomba |
| --------------------------- | --------------------------------------------------------------------------------- |
| `GET /v2/verify` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `PUT /v2/verify-batch` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET /v2/verify-batch/{id}` | `GET /v1/bulk/verifier/{id}/progress`, then `GET /v1/bulk/verifier/{id}/download` |
| `GET /v2/balance` | [`GET /v1/me`](/api/account#get-account) |
## Authentication changes
Kickbox's client libraries send the key as `Authorization: token `. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Kickbox
curl "https://api.kickbox.com/v2/verify?email=jane.doe@stripe.com" \
-H "Authorization: token $KICKBOX_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"
```
Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| Kickbox | Tomba | Notes |
| --------- | ------------- | -------------------------------------------------------------------------------- |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `timeout` | No equivalent | Kickbox's value is in milliseconds. Set the timeout in your HTTP client instead. |
## Response field mapping
Kickbox 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).
| Kickbox | Tomba | Notes |
| -------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `result` | `data.email.result`, `data.email.status` | See [Status mapping](#status-mapping). |
| `reason` | `data.email.status` and the booleans in `data.email` | See [Status mapping](#status-mapping). |
| `role` | No equivalent | |
| `free` | `data.email.webmail` | |
| `disposable` | `data.email.disposable` | |
| `accept_all` | `data.email.accept_all` | |
| `did_you_mean` | No equivalent | |
| `sendex` | `data.email.score` | Kickbox scores from 0 to 1; Tomba's integer score is computed differently. Recalibrate any threshold you apply to it. |
| `email` | `data.email.email` | An empty string when the domain is disposable. |
| `user`, `domain` | No equivalent | Split `data.email.email` at `@`. |
| `success`, `message` | HTTP status code, `errors.message` | See [Error mapping](#error-mapping). |
| `X-Kickbox-Balance` header | `X-Verify-Remaining` header | Tomba's value is the balance before the current request; see [Check usage](/usage-and-quotas#check-usage). |
| `X-Kickbox-Response-Time` header | No equivalent | |
Tomba also returns `block`, `greylisted`, `gibberish`, `regex`, `mx_check`, `mx.records`, `smtp_provider`, and `whois`, which have no Kickbox counterpart.
## Status mapping
| Kickbox `result` | Tomba `status` | Tomba `result` |
| -------------------------------- | -------------- | --------------- |
| `deliverable` | `valid` | `deliverable` |
| `undeliverable` | `invalid` | `undeliverable` |
| `risky` | `accept_all` | `risky` |
| `risky`, with `disposable: true` | `disposable` | Empty string |
| `unknown` | `unknown` | `risky` |
| Kickbox `reason` | Tomba |
| ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `accepted_email` | `status` is `valid` |
| `rejected_email` | `status` is `invalid` |
| `invalid_email` | The request fails with `422 params_invalid` |
| `invalid_domain` | `mx_check` is `false` |
| `low_deliverability` | `status` is `accept_all` when the domain accepts all mail |
| `low_quality` | No single equivalent. Check `disposable`, `webmail`, and `gibberish`. |
| `no_connect`, `timeout`, `invalid_smtp`, `unavailable_smtp`, `unexpected_error` | No equivalent reason. An inconclusive check returns `status` `unknown`; a failed check returns `408 proxy_error`. |
Branch on `status` rather than `result`: Tomba's `unknown` status carries the result `risky`, and a disposable address has an empty `result`. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
Both APIs signal failures with HTTP status codes. Tomba's error body is an `errors` object with `type`, `message`, and `code`.
| Kickbox | Tomba | What to do |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `403` `Insufficient balance` | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| Other `4xx` with a `message` | `400` or `401 authentication_failed` for credentials, `400 api_key_expired`, `422 params_invalid` for parameters | Read `errors.type` to find the cause; [rotate](/authentication#rotate-keys) an expired key. |
| `5xx` | `500 api_error` | Retry with backoff. |
Tomba also returns `429 rate_limit` when you exceed your plan's rate limits (retry after `Retry-After`; see [Handle 429 responses](/rate-limits#handle-429-responses)), `408 proxy_error` when the mailbox check fails, and `451 claimed_email` when the owner of the address asked Tomba to stop processing it. Remove `451` addresses from your list. Error responses don't consume credits; every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
| Kickbox | Tomba |
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PUT /v2/verify-batch` with newline-separated addresses and an `X-Kickbox-Filename` header | `POST /v1/bulk/verifier` with a `name` and a newline-separated `list`, or a CSV `file` |
| The batch starts on submission | Launch it: `PUT /v1/bulk/verifier/{id}` |
| `GET /v2/verify-batch/{id}` | Poll `GET /v1/bulk/verifier/{id}/progress` until `status` is `completed`, then download the CSV from `GET /v1/bulk/verifier/{id}/download`. Or set `webhook_url` to receive an event when the job ends. |
```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 `/v2/verify` and returns Kickbox's field names for the values Tomba provides. 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 -> Kickbox result
const RESULTS = {
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) {
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
return {
result: RESULTS[check.status.toLowerCase()] ?? "unknown",
email: check.email || email,
free: check.webmail,
disposable: check.disposable,
accept_all: check.accept_all,
success: true,
};
}
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`, and send them as `X-Tomba-Key` and `X-Tomba-Secret`.
2. Point single verifications at `GET https://api.tomba.io/v1/email-verifier`, drop the `timeout` parameter, and set a client timeout of at least 180 seconds.
3. Read results from `data.email` and translate `result` and `reason` with the [status mapping](#status-mapping), comparing `status` case-insensitively.
4. Handle `402` where you handled `403 Insufficient balance`, retry `429` after `Retry-After`, and drop addresses that return `451`.
5. Replace batches with `verifier` bulk jobs: create, launch, poll `/progress`, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from an email verifier
Guides for moving email verification from ZeroBounce, NeverBounce, Emailable, and seven other providers to Tomba, and what every migration changes.
URL: /migration/email-verifier/introduction
# Migrate from an email verifier
Each guide maps one provider's single-address and list verification to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier).
## Guides
| Provider | Guide |
| --------------- | ------------------------------------------------------------------------- |
| NeverBounce | [Migrate from NeverBounce](/migration/email-verifier/neverbounce) |
| MillionVerifier | [Migrate from MillionVerifier](/migration/email-verifier/millionverifier) |
| BounceBan | [Migrate from BounceBan](/migration/email-verifier/bounceban) |
| Bouncer | [Migrate from Bouncer](/migration/email-verifier/bouncer) |
| Kickbox | [Migrate from Kickbox](/migration/email-verifier/kickbox) |
| Hunter | [Migrate from Hunter (email verifier)](/migration/email-verifier/hunter) |
| ZeroBounce | [Migrate from ZeroBounce](/migration/email-verifier/zerobounce) |
| Emailable | [Migrate from Emailable](/migration/email-verifier/emailable) |
| Clearout | [Migrate from Clearout](/migration/email-verifier/clearout) |
| DeBounce | [Migrate from DeBounce](/migration/email-verifier/debounce) |
## What changes in every migration
| Area | Change |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Endpoint | A single check becomes `GET https://api.tomba.io/v1/email-verifier?email=`. A list becomes a bulk job of type `verifier`. |
| Authentication | Every request sends `X-Tomba-Key` and `X-Tomba-Secret` headers. Tomba doesn't read keys from the query string. See [Authentication](/authentication). |
| Response | The checks are in `data.email`. `status` is the detailed outcome and `result` the deliverability verdict; see [Status values](/attributes/verifier#status-values) and [Result values](/attributes/verifier#result-values). Compare `status` case-insensitively. |
| Timing | Each request returns a final result: there's no timeout parameter, pending state, or result to poll for. Set your client timeout to at least 180 seconds. |
| Invalid input | A malformed address fails with `422 params_invalid` instead of returning a result. |
| Errors | Out of credits is `402 quota_exceeded`, throttling is `429 rate_limit` with `Retry-After`, a withheld address is `451 claimed_email`, and a failed mailbox check is `408 proxy_error`. See [Status codes](/error-handling#status-codes). |
| Billing | Only some results are charged, and repeating a request within your usage window is free. See [Credit costs](/usage-and-quotas#credit-costs) and [Duplicate requests](/usage-and-quotas#duplicate-requests). |
| Lists | Create and launch the job, poll its progress or receive its `webhook_url` event, and download a CSV. See [Lifecycle](/bulks#lifecycle). |
---
## Document: Migrate from Hunter (email verifier)
Map Hunter Email Verifier API calls, fields, statuses, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/hunter
# Migrate from Hunter (email verifier)
This guide maps the Hunter API v2 Email Verifier and Account endpoints to the Tomba [Email Verifier](/api/verifier#email-verifier). Hunter's Domain Search, Email Finder, and enrichment endpoints are covered in [Migrate from Hunter (email finder)](/migration/email-finder/hunter). Hunter API behavior checked on 2026-09-29 against the [Hunter API v2 reference](https://hunter.io/api-documentation/v2).
## 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.
## Endpoint mapping
Replace the base URL `https://api.hunter.io/v2` with `https://api.tomba.io/v1`.
| Hunter | Tomba | Notes |
| --------------------- | -------------------------------------------------------- | ------------------------------------------------- |
| `GET /email-verifier` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) | |
| `GET /account` | [`GET /v1/me`](/api/account#get-account) | See [Check usage](/usage-and-quotas#check-usage). |
## Authentication changes
Hunter accepts the key as the `api_key` parameter, the `X-API-KEY` header, or a bearer token. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string:
```bash
# Hunter
curl "https://api.hunter.io/v2/email-verifier?email=jane.doe@stripe.com" \
-H "X-API-KEY: $HUNTER_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"
```
Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| Hunter | Tomba | Notes |
| ------- | ------- | ------------------------------------------------------------------------------- |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
## Response field mapping
Tomba nests the checks in `data.email` and returns the sources in `data.sources`. Field definitions are in [Email verifier response](/attributes/verifier).
| Hunter | Tomba | Notes |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `data.status` | `data.email.status` | See [Status mapping](#status-mapping). |
| `data.result` | `data.email.result` | |
| `data.score` | `data.email.score` | Computed differently. |
| `data.email` | `data.email.email` | An empty string when the domain is disposable. |
| `data.regexp` | `data.email.regex` | |
| `data.gibberish`, `disposable`, `webmail`, `accept_all`, `block` | `data.email.gibberish`, `.disposable`, `.webmail`, `.accept_all`, `.block` | |
| `data.mx_records` | `data.email.mx_check` | Tomba's `data.email.mx.records` lists the MX hosts. |
| `data.smtp_check` | `data.email.status` is `valid` | Tomba's `smtp_check` field only mirrors `mx_check`. Don't read it as a mailbox result. |
| `data.smtp_server` | No equivalent | Tomba's `smtp_server` also mirrors `mx_check`. |
| `data.verification` | No equivalent | |
| `data.sources[]` | `data.sources[]` | `domain` is `website_url`. |
## Status mapping
| Hunter `status` | Tomba `status` | Tomba `result` |
| --------------- | ---------------------------------------- | --------------------- |
| `valid` | `valid` | `deliverable` |
| `invalid` | `invalid` | `undeliverable` |
| `accept_all` | `accept_all` | `risky` |
| `webmail` | The mailbox result, with `webmail: true` | Depends on the status |
| `disposable` | `disposable` | Empty string |
| `unknown` | `unknown` | `risky` |
Tomba has no `webmail` status: it checks webmail addresses like any other and sets the `webmail` field. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
Hunter returns `errors` as an array of `{id, code, details}`. Tomba returns a single `errors` object with `type`, `message`, and `code`. The meaning of `403` and `429` also changes:
| Hunter | Tomba | What to do |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `400` `wrong_params` or another `invalid_*` id | `422 params_invalid` | Fix the parameter named in the message. |
| `401` no valid 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. |
| `403` rate limit reached | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `429` usage limit reached | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `429` `restricted_account` | `401 authentication_failed` | [Contact support](https://app.tomba.io/contact). |
| `451` `claimed_email` | `451 claimed_email` | Same meaning. Remove the address from your data. |
| `202` verification in progress | Not returned | Remove the polling loop. |
| `222` SMTP server error | `408 proxy_error` | Retry later. |
| `5xx` | `500 api_error` | Retry with backoff. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
Hunter runs bulk Email Verifier lists from its dashboard, and its API endpoint takes one address per request. Tomba runs lists through the API as `verifier` bulk jobs: send a JSON `list` of addresses or a CSV file. See [Email verifier](/bulks#email-verifier).
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).
Tomba's single-request endpoint never returns `202`, so remove the retry loop you use for Hunter's Email Verifier.
## Code example
This function replaces a call to Hunter's Email Verifier and returns Hunter's field names. It needs Node.js 18 or later.
```js title="email-verifier.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
async function emailVerifier(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) {
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
const status = check.status.toLowerCase();
return {
status,
result: check.result,
score: check.score,
email: check.email,
regexp: check.regex,
gibberish: check.gibberish,
disposable: check.disposable,
webmail: check.webmail,
mx_records: check.mx_check,
smtp_check: status === "valid",
accept_all: check.accept_all,
block: check.block,
sources: (body.data.sources ?? []).map(
({ website_url, ...source }) => ({
domain: website_url,
...source,
}),
),
};
}
console.log(await emailVerifier("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`, `X-API-KEY`, or a bearer token.
2. Change the base URL and read the verifier fields under `data.email`.
3. Handle webmail addresses through the `webmail` field, compare `status` case-insensitively, and stop reading `smtp_check` as a mailbox result.
4. Swap error handling: `402` replaces Hunter's `429`, `429` replaces Hunter's `403`, `408` replaces `222`, and `errors` is an object, not an array.
5. Remove the `202` polling loop.
6. Move list processing to `verifier` bulk jobs.
7. Verify the same sample of addresses with both services and compare the results before you switch production traffic.
---
## Document: Migrate from Emailable
Map Emailable verify, batch, and account API calls, states, reasons, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/emailable
# 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.
---
## Document: Migrate from DeBounce
Map DeBounce single validation, bulk upload, and balance API calls, result codes, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/debounce
# Migrate from DeBounce
This guide maps the DeBounce API v1 (single validation, bulk upload and status, and balance) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). DeBounce API behavior checked on 2026-09-30 against the [DeBounce API reference](https://developers.debounce.com/) and its [result codes](https://help.debounce.com/understanding-results/result-codes/).
## 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). Tomba throttles by requests per time window, not by concurrent calls.
- Set your HTTP client timeout to at least 180 seconds. Tomba returns the finished result in the response.
## Endpoint mapping
DeBounce uses two hosts, `api.debounce.io` and `bulk.debounce.io`. Tomba serves everything from `api.tomba.io`.
| DeBounce | Tomba |
| ----------------------------------------- | --------------------------------------------------------------------- |
| `GET https://api.debounce.io/v1/` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `GET https://bulk.debounce.io/v1/upload/` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET https://bulk.debounce.io/v1/status/` | `GET /v1/bulk/verifier/{id}/progress` |
| The `download_link` from `/v1/status/` | `GET /v1/bulk/verifier/{id}/download` |
| `GET https://api.debounce.io/v1/balance/` | [`GET /v1/me`](/api/account#get-account) |
## Authentication changes
DeBounce reads the key from the `api` query parameter. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string:
```bash
# DeBounce
curl "https://api.debounce.io/v1/?api=$DEBOUNCE_API_KEY&email=jane.doe@stripe.com"
# 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 DeBounce from browser code with a `public_` 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
| DeBounce | Tomba | Notes |
| ----------------- | ------------------------------------------ | ------------------------------------------------------------------------------- |
| `api` | `X-Tomba-Key` and `X-Tomba-Secret` headers | |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `photo`, `append` | No equivalent | |
| `gsuite` | No equivalent | Every Tomba response includes `data.email.accept_all`. |
| Upload `url` | `file` or `list` | Tomba doesn't fetch lists from a URL. Upload the CSV as `file`, or send `list`. |
| `list_id` | `{id}` in the path | |
## Response field mapping
DeBounce returns the result in a `debounce` object, with booleans and numbers as strings. Tomba nests the checks in `data.email`, uses JSON booleans, and lists the public pages where the address appears in `data.sources`. Field definitions are in [Email verifier response](/attributes/verifier).
| DeBounce | Tomba | Notes |
| ---------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------- |
| `debounce.email` | `data.email.email` | An empty string when the domain is disposable. |
| `debounce.code`, `debounce.reason` | `data.email.status` | See [Status mapping](#status-mapping). |
| `debounce.result` | `data.email.result` | The closest field; see [Result values](/attributes/verifier#result-values). |
| `debounce.free_email` | `data.email.webmail` | |
| `debounce.role` | No equivalent | |
| `debounce.send_transactional` | No equivalent | |
| `debounce.did_you_mean` | No equivalent | |
| `success` | HTTP status code | See [Error mapping](#error-mapping). |
| `balance` | `X-Verify-Remaining` header, `GET /v1/me` | See [Check usage](/usage-and-quotas#check-usage). |
| `debounce.error` | `errors.message` | |
Tomba also returns `score`, `accept_all`, `block`, `greylisted`, `gibberish`, `disposable`, `mx_check`, `mx.records`, `smtp_provider`, `regex`, and `whois`, which have no field in DeBounce's response.
## Status mapping
| DeBounce `code` | `reason` | Tomba `status` | Tomba `result` |
| --------------- | ----------- | ------------------------------------------------------- | --------------- |
| `1` | Syntax | No status. The request fails with `422 params_invalid`. | |
| `2` | Spam Trap | No equivalent. Tomba returns the mailbox result. | |
| `3` | Disposable | `disposable` | Empty string |
| `4` | Accept-All | `accept_all` | `risky` |
| `5` | Deliverable | `valid` | `deliverable` |
| `6` | Invalid | `invalid` | `undeliverable` |
| `7` | Unknown | `unknown` | `risky` |
DeBounce reports role accounts in the `role` field rather than as code `8` in API responses; Tomba has no role flag. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
Both APIs signal failures with HTTP status codes. DeBounce's error body is `{"debounce": {"error", "code"}, "success": "0"}`; Tomba's is an `errors` object with `type`, `message`, and `code`.
| DeBounce | Tomba | What to do |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `401` `Wrong API` | `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` `Credits Low` | `402 quota_exceeded` on single requests | For bulk jobs, the balance is checked at the first download instead; see [Billing](/bulks#billing). |
| `429` `Maximum concurrent calls reached` | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| Maximum number of API bulk verify requests reached | `429 rate_limit` (running job limit) | Wait for a running job to finish; see [Limits](/bulks#limits). |
| `400` `URL parameter is not valid.` | No equivalent | Upload the file instead of a URL. |
| `List ID is not valid.` | `GET /v1/bulk/verifier/{id}` returns `404 unknown_record` | Check the job ID. |
| No equivalent | `422 params_invalid` | Fix the parameter named in the message. |
| 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
| DeBounce | Tomba |
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `/v1/upload/` with the `url` of a file on your server | `POST /v1/bulk/verifier` with `name` and a CSV `file`, or a newline-separated `list` of addresses |
| Validation starts on upload, or is queued behind a running job | Launch the job: `PUT /v1/bulk/verifier/{id}` |
| Poll `/v1/status/` for `status` and `percentage` | Poll `GET /v1/bulk/verifier/{id}/progress` for `status` and `progress` until `status` is `completed` |
| Fetch the CSV from `download_link` | `GET /v1/bulk/verifier/{id}/download?file=full` |
```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" \
-F "name=Newsletter list" \
-F "file=@emails.csv;type=text/csv" \
-F "email_field_index=0"
```
Tomba treats the first row of the file as a header. A DeBounce list with one address per line and no header loses its first address, so add a header row first. 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 `api.debounce.io/v1/` and returns DeBounce's `code` and `reason` for the address. It needs Node.js 18 or later.
```js title="validate.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
// Tomba status -> DeBounce [code, reason]
const CODES = {
disposable: ["3", "Disposable"],
accept_all: ["4", "Accept-All"],
valid: ["5", "Deliverable"],
invalid: ["6", "Invalid"],
unknown: ["7", "Unknown"],
};
async function validate(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) {
// A malformed address (DeBounce code 1) fails with 422 params_invalid.
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
const [code, reason] = CODES[check.status.toLowerCase()] ?? CODES.unknown;
return {
email: check.email || email,
code,
reason,
free_email: String(check.webmail),
};
}
console.log(await validate("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 the `api` parameter from your URLs and logs.
2. Point single validations at `GET https://api.tomba.io/v1/email-verifier` and set a client timeout of at least 180 seconds.
3. Replace checks of `success` with HTTP status handling: `402`, `422`, `429` with `Retry-After`, `408`, and `451`.
4. Translate `code` with the [status mapping](#status-mapping), comparing `status` case-insensitively, and read Tomba's booleans as JSON booleans rather than strings.
5. Replace URL uploads with `verifier` bulk jobs: upload the CSV with a header row, launch, poll `/progress`, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from Clearout
Map Clearout instant verify, bulk verify, and credits API calls, statuses, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/clearout
# Migrate from Clearout
This guide maps the Clearout API v2 email verification endpoints (instant verify, bulk verify, and available credits) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). Clearout API behavior checked on 2026-09-30 against the [Clearout API reference](https://docs.clearout.io/developers/api/email-verify).
## 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.
## Endpoint mapping
Replace the base URL `https://api.clearout.io/v2` with `https://api.tomba.io/v1`. Clearout shows the base URL for your account's region under Developer, Reference in its app; replace that one if it differs.
| Clearout | Tomba |
| ---------------------------------------- | --------------------------------------------------------------------- |
| `POST /email_verify/instant` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `POST /email_verify/bulk` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET /email_verify/bulk/progress_status` | `GET /v1/bulk/verifier/{id}/progress` |
| `POST /download/result` | `GET /v1/bulk/verifier/{id}/download` |
| `POST /email_verify/list/remove` | `DELETE /v1/bulk/verifier/{id}/delete` |
| `POST /email_verify/list/cancel` | No equivalent |
| `POST /email_verify/list` | `GET /v1/bulk/verifier` |
| `GET /email_verify/getcredits` | [`GET /v1/me`](/api/account#get-account) |
Clearout's single-check endpoints (`/email/verify/catchall`, `/disposable`, `/business`, `/free`, `/role`, `/gibberish`) have no separate Tomba endpoints. The Email Verifier response carries `accept_all`, `disposable`, `webmail`, and `gibberish` for every address.
## Authentication changes
Clearout reads an API token from the `Authorization` header. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Clearout
curl -X POST "https://api.clearout.io/v2/email_verify/instant" \
-H "Authorization: Bearer $CLEAROUT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "jane.doe@stripe.com"}'
# 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"
```
Tomba takes the address as a query parameter on a `GET` request, not in a JSON body. Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| Clearout | Tomba | Notes |
| -------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `Authorization` header | `X-Tomba-Key` and `X-Tomba-Secret` headers | |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `timeout` | No equivalent | Set the timeout in your HTTP client. |
| `response=flat` | No equivalent | Read the checks from `data.email`. |
| Bulk `file` | `file` | CSV only, sent with the content type `text/csv`. Convert XLSX files first. Also send `name`. |
| Email column found by its header | `email_field_index` | Zero-based, and optional when the header names the column, such as `email`. |
| Bulk `optimize` | No equivalent | |
| Bulk `ignore_duplicate_file` | No equivalent | |
| `list_id` | `{id}` in the path | |
## Response field mapping
Clearout returns the result in `data`. 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).
| Clearout | Tomba | Notes |
| ---------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
| `status` (top level) | HTTP status code | See [Error mapping](#error-mapping). |
| `data.email_address` | `data.email.email` | An empty string when the domain is disposable. |
| `data.status` | `data.email.status`, `data.email.result` | See [Status mapping](#status-mapping). |
| `data.safe_to_send` | `data.email.result` | The closest field; see [Result values](/attributes/verifier#result-values). |
| `data.sub_status.code`, `data.sub_status.desc` | No equivalent | Tomba reports `greylisted`, `block`, and `mx_check` as separate fields. |
| `data.disposable` | `data.email.disposable` | A boolean instead of `"yes"` or `"no"`. |
| `data.free` | `data.email.webmail` | A boolean instead of `"yes"` or `"no"`. |
| `data.gibberish` | `data.email.gibberish` | A boolean instead of `"yes"` or `"no"`. |
| `data.role` | No equivalent | |
| `data.detail_info.mx_record` | `data.email.mx.records` | An array of MX host names instead of one string. |
| `data.detail_info.smtp_provider` | `data.email.smtp_provider` | |
| `data.detail_info.account`, `.domain` | No equivalent | Split the address at `@`. |
| `data.suggested_email_address` | No equivalent | |
| `data.bounce_type` | No equivalent | |
| `data.verified_on`, `data.time_taken` | No equivalent | |
| `data.profile` | No equivalent | |
| `error.code`, `error.message` | `errors.type`, `errors.message` | See [Error mapping](#error-mapping). |
Tomba also returns `score`, `accept_all`, `block`, `greylisted`, `regex`, and `whois`, which have no field in Clearout's instant verify response. Read your remaining balance from the `X-Verify-Remaining` header or `GET /v1/me`; see [Check usage](/usage-and-quotas#check-usage).
## Status mapping
Clearout documents four primary statuses. Its API returns them in `data.status`; the example response shows `valid` in lower case, so compare case-insensitively.
| Clearout status | Tomba `status` | Tomba `result` |
| ---------------------------------------- | ------------------------------------------------------- | --------------- |
| Valid | `valid` | `deliverable` |
| Invalid | `invalid` | `undeliverable` |
| Invalid, sub-status `400` (syntax error) | No status. The request fails with `422 params_invalid`. | |
| Catch All | `accept_all` | `risky` |
| Unknown | `unknown` | `risky` |
| Any status with `disposable: "yes"` | `disposable` | Empty string |
Tomba reports a disposable domain as its own `status` rather than as a flag next to a mailbox result. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
Both APIs signal failures with HTTP status codes. Clearout's error body is `{"status": …, "error": {"code", "message"}}`; Tomba's is an `errors` object with `type`, `message`, and `code`.
| Clearout | Tomba | What to do |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `400` Bad Request | `422 params_invalid` | Fix the parameter named in the message. |
| `401` Unauthorized | `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` with code `1002`, `1028`, or `1031` | `402 quota_exceeded` on single requests | For bulk jobs, the balance is checked at the first download instead; see [Billing](/bulks#billing). |
| `429` with code `1030` | `429 rate_limit` | Retry after the `Retry-After` delay, not `x-ratelimit-reset`; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| Code `1001` (too many bulk requests) | `429 rate_limit` (running job limit) | Wait for a running job to finish; see [Limits](/bulks#limits). |
| Code `1004` (no email column found) | `422 params_invalid` | Name the header `email`, or send `email_field_index`. |
| `415` Invalid Content Type | No equivalent | Tomba's single check is a `GET` request with no body. |
| `500`, `503` | `500 api_error` | Retry with backoff. |
| `524` Request Timeout | No equivalent | Tomba returns the final result; keep the client timeout at 180 seconds or more. |
| 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
| Clearout | Tomba |
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `POST /email_verify/bulk` with a CSV or XLSX `file` | `POST /v1/bulk/verifier` with `name` and a CSV `file`, or a newline-separated `list` of addresses |
| Verification is queued on upload | Launch the job: `PUT /v1/bulk/verifier/{id}` |
| Poll `progress_status` for `progress_status` and `percentile` | Poll `GET /v1/bulk/verifier/{id}/progress` for `status` and `progress` until `status` is `completed` |
| The `email_verifier.bulk.completed` webhook | `webhook_url`, which receives a signed `bulk.completed` event. See [Bulk job events](/webhook#bulk-job-events). |
| `POST /download/result` returns a file URL in `data.url` | `GET /v1/bulk/verifier/{id}/download?file=full` returns the CSV itself |
```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" \
-F "name=Newsletter list" \
-F "file=@emails.csv;type=text/csv" \
-F "email_field_index=0"
```
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 `/email_verify/instant` and returns Clearout'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 -> Clearout primary status, as Clearout's docs name them.
// Its API example returns "valid" in lower case; compare case-insensitively.
const STATUSES = {
valid: "Valid",
invalid: "Invalid",
accept_all: "Catch All",
unknown: "Unknown",
};
const yesNo = (value) => (value ? "yes" : "no");
async function instantVerify(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) {
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
const status = check.status.toLowerCase();
return {
email_address: check.email || email,
// A disposable address has no mailbox status in Tomba.
status: STATUSES[status] ?? "Unknown",
disposable: yesNo(status === "disposable" || check.disposable),
free: yesNo(check.webmail),
gibberish: yesNo(check.gibberish),
detail_info: {
mx_record: check.mx?.records?.[0] ?? null,
smtp_provider: check.smtp_provider,
},
};
}
console.log(await instantVerify("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 the `Authorization` header.
2. Change instant verification from a `POST` with a JSON body to `GET https://api.tomba.io/v1/email-verifier?email=`, drop `timeout`, and set a client timeout of at least 180 seconds.
3. Swap error handling: `422` for bad parameters, `402` for credits, `429` with `Retry-After`, and `408` and `451` as new cases.
4. Translate `status` with the [status mapping](#status-mapping), comparing it case-insensitively, and read `disposable`, `webmail`, and `gibberish` as booleans.
5. Replace bulk uploads with `verifier` bulk jobs: convert XLSX to CSV, launch, receive the `webhook_url` event instead of the webhook, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from Bouncer
Map Bouncer real-time verification, batch, and credits API calls, statuses, reasons, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/bouncer
# Migrate from Bouncer
This guide maps the Bouncer API v1.1 (real-time verification, batch verification, and credits) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). Bouncer API behavior checked on 2026-09-29 against the [Bouncer API documentation](https://docs.usebouncer.com/).
## 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.
## Endpoint mapping
Replace the host `api.usebouncer.com` with `api.tomba.io`.
| Bouncer | Tomba |
| ------------------------------------------------- | --------------------------------------------------------------------- |
| `GET /v1.1/email/verify` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `POST /v1.1/email/verify/batch` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET /v1.1/email/verify/batch/{batchId}` | `GET /v1/bulk/verifier/{id}/progress` |
| `GET /v1.1/email/verify/batch/{batchId}/download` | `GET /v1/bulk/verifier/{id}/download` |
| `POST /v1.1/email/verify/batch/{batchId}/finish` | No equivalent |
| `DELETE /v1.1/email/verify/batch/{batchId}` | `DELETE /v1/bulk/verifier/{id}/delete` |
| `POST /v1.1/email/verify/batch/sync` | No equivalent. Use a bulk job, or send single requests. |
| `GET /v1.1/credits` | [`GET /v1/me`](/api/account#get-account) |
## Authentication changes
Bouncer reads the key from the `x-api-key` header or from basic authentication. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Bouncer
curl "https://api.usebouncer.com/v1.1/email/verify?email=jane.doe@stripe.com" \
-H "x-api-key: $BOUNCER_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"
```
Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| Bouncer | Tomba | Notes |
| ------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------- |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `timeout` | No equivalent | Set the timeout in your HTTP client. |
| Batch body `[{"email": …}]` | `list` | A string with one address per line. |
| Batch `callback` | No equivalent | Poll the job's progress. |
| Batch `skip-header` | No equivalent | Tomba always treats the first row of a CSV file as a header. |
| Status `with-stats` | No equivalent | |
| Download `download=all` | `file=full` | |
| Download `download=deliverable` | `file=valid` | |
| Download `download` `risky`, `undeliverable`, `unknown` | No equivalent | Download `file=full` and filter the rows. |
## Response field mapping
Bouncer returns a flat object whose flags are the strings `yes`, `no`, and `unknown`. Tomba nests the checks in `data.email`, uses booleans, and lists the public pages where the address appears in `data.sources`. Field definitions are in [Email verifier response](/attributes/verifier).
| Bouncer | Tomba | Notes |
| --------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
| `email` | `data.email.email` | An empty string when the domain is disposable. |
| `status` | `data.email.result`, `data.email.status` | See [Status mapping](#status-mapping). |
| `reason` | `data.email.status` and the booleans in `data.email` | See [Status mapping](#status-mapping). |
| `domain.acceptAll` | `data.email.accept_all` | |
| `domain.disposable` | `data.email.disposable` | |
| `domain.free` | `data.email.webmail` | |
| `domain.name` | No equivalent | Split `data.email.email` at `@`. |
| `account.role`, `account.disabled`, `account.fullMailbox` | No equivalent | |
| `dns.type`, `dns.record` | `data.email.mx_check`, `data.email.mx.records` | |
| `provider` | `data.email.smtp_provider` | |
| `score` | `data.email.score` | Computed differently. Recalibrate any threshold you apply to it. |
| `toxic`, `toxicity` | No equivalent | |
| `retryAfter` | No equivalent | `data.email.greylisted` is `true` when the mail server deferred the check. |
| `didYouMean` | No equivalent | |
Tomba also returns `block`, `gibberish`, `regex`, and `whois`, which have no Bouncer counterpart.
## Status mapping
| Bouncer `status` | Tomba `status` | Tomba `result` |
| ------------------------------------ | -------------- | --------------- |
| `deliverable` | `valid` | `deliverable` |
| `risky`, reason `low_deliverability` | `accept_all` | `risky` |
| `risky`, reason `low_quality` | `disposable` | Empty string |
| `undeliverable` | `invalid` | `undeliverable` |
| `unknown` | `unknown` | `risky` |
| Bouncer `reason` | Tomba |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `accepted_email` | `status` is `valid` |
| `rejected_email` | `status` is `invalid` |
| `invalid_email` | The request fails with `422 params_invalid` |
| `invalid_domain` | `mx_check` is `false` |
| `low_deliverability` | `status` is `accept_all` for a catch-all domain. A full mailbox has no equivalent. |
| `low_quality` | `status` is `disposable` for a disposable domain |
| `dns_error`, `unavailable_smtp`, `unsupported`, `timeout`, `unknown` | No equivalent reason. An inconclusive check returns `status` `unknown`; a failed check returns `408 proxy_error`. |
Branch on `status` rather than `result`: Tomba's `unknown` status carries the result `risky`, and a disposable address has an empty `result`. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
| Bouncer | Tomba | What to do |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `400` | `422 params_invalid` | Fix the parameter named in the message. |
| `401` `user not found` | `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` `Payment Required` | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `429` | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `503` | `500 api_error` or `504` | Retry with backoff. |
| `404` batch not found | `404 unknown_record` | Check the job ID. |
| `405` batch not completed | `404` | Download only after `/progress` reports `completed`. |
| 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. |
Bouncer's error body has `status`, `error`, and `message`; Tomba's `errors` object has `type`, `message`, and `code`. Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
| Bouncer | Tomba |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `POST /v1.1/email/verify/batch` with a JSON array or a text file | `POST /v1/bulk/verifier` with `name` and a newline-separated `list`, or a CSV `file` |
| The batch is queued on creation | Launch it: `PUT /v1/bulk/verifier/{id}` |
| Poll the batch status, or pass `callback` | Poll `GET /v1/bulk/verifier/{id}/progress` until `status` is `completed`, or pass `webhook_url` to receive an event. |
| Download JSON, or CSV with `Accept: text/csv` | `GET /v1/bulk/verifier/{id}/download` returns CSV only |
```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.1/email/verify` and returns Bouncer's field names, with `yes` and `no` flags. 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 -> Bouncer status
const STATUSES = {
valid: "deliverable",
accept_all: "risky",
disposable: "risky",
invalid: "undeliverable",
unknown: "unknown",
};
const yesNo = (flag) => (flag ? "yes" : "no");
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) {
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
return {
email: check.email || email,
status: STATUSES[check.status.toLowerCase()] ?? "unknown",
domain: {
acceptAll: yesNo(check.accept_all),
disposable: yesNo(check.disposable),
free: yesNo(check.webmail),
},
provider: check.smtp_provider,
score: check.score,
};
}
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`, and send them as `X-Tomba-Key` and `X-Tomba-Secret` instead of `x-api-key`.
2. Point single verifications at `GET https://api.tomba.io/v1/email-verifier`, drop `timeout`, and set a client timeout of at least 180 seconds.
3. Read results from `data.email`, convert `yes`/`no` flag checks to booleans, and translate `status` and `reason` with the [status mapping](#status-mapping).
4. Handle `402` and `429` as before, add `451` and `408`, and treat an empty `data` array as an unknown job.
5. Replace batches with `verifier` bulk jobs: create, launch, poll `/progress` instead of using `callback`, and download the CSV.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from BounceBan
Map BounceBan single and bulk verification calls, response fields, results, and errors to the Tomba Email Verifier.
URL: /migration/email-verifier/bounceban
# Migrate from BounceBan
This guide maps the BounceBan v1 API (single verification, status polling, bulk tasks, and account) to the Tomba [Email Verifier](/api/verifier#email-verifier) and [bulk verifier jobs](/bulks#email-verifier). BounceBan API behavior checked on 2026-09-29 against the [BounceBan API reference](https://bounceban.com/public/doc/api.html).
## 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 to the first request.
## Endpoint mapping
Replace the host `api.bounceban.com` (and `api-waterfall.bounceban.com`) with `api.tomba.io`.
| BounceBan | Tomba |
| --------------------------------------------------------- | --------------------------------------------------------------------- |
| `GET /v1/verify/single` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `GET /v1/verify/single` on the waterfall host | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| `GET /v1/verify/single/status` | No equivalent. The first response is final. |
| `POST /v1/verify/bulk`, `POST /v1/verify/bulk/file` | `POST /v1/bulk/verifier`, then `PUT /v1/bulk/verifier/{id}` to launch |
| `GET /v1/verify/bulk/status` | `GET /v1/bulk/verifier/{id}/progress` |
| `GET /v1/verify/bulk/dump`, `POST /v1/verify/bulk/export` | `GET /v1/bulk/verifier/{id}/download` (CSV) |
| `POST /v1/verify/bulk/emails` | No equivalent. Download the job's CSV. |
| `POST /v1/verify/bulk/destroy` | `DELETE /v1/bulk/verifier/{id}/delete` |
| `GET /v1/account` | [`GET /v1/me`](/api/account#get-account) |
## Authentication changes
BounceBan reads the key from the `Authorization` header. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# BounceBan
curl "https://api.bounceban.com/v1/verify/single?email=jane.doe@stripe.com" \
-H "Authorization: $BOUNCEBAN_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"
```
Tomba API keys expire; plan their rotation with [Key expiry](/authentication#key-expiry).
## Parameter mapping
| BounceBan | Tomba | Notes |
| -------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email` | `email` | Required. A malformed address returns `422 params_invalid` instead of a result. |
| `url` | `webhook_url` | Tomba still returns the result in the response, and also posts it to your URL when the result qualifies. See [Per-request callbacks](/webhook#per-request-callbacks). |
| `mode` | No equivalent | |
| `disable_catchall_verify` | No equivalent | |
| `timeout` (waterfall host) | No equivalent | Set the timeout in your HTTP client. |
| `id` (status endpoint) | No equivalent | |
## Response field mapping
BounceBan 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).
| BounceBan | Tomba | Notes |
| --------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------- |
| `id` | No equivalent | |
| `status` | No equivalent | Tomba has no `verifying` or `queue` state. |
| `try_again_at` | No equivalent | |
| `email` | `data.email.email` | An empty string when the domain is disposable. |
| `result` | `data.email.result`, `data.email.status` | See [Status mapping](#status-mapping). |
| `score` | `data.email.score` | Computed differently. Recalibrate any threshold you apply to it. |
| `is_disposable` | `data.email.disposable` | |
| `is_accept_all` | `data.email.accept_all` | |
| `is_role` | No equivalent | |
| `is_free` | `data.email.webmail` | |
| `mx_records` | `data.email.mx.records` | `data.email.mx_check` is `true` when the domain has MX records. |
| `smtp_provider` | `data.email.smtp_provider` | |
| `mode` | No equivalent | |
| `verify_at` | No equivalent | |
| `credits_consumed`, `credits_remaining` | `X-Verify-Remaining` header, `GET /v1/me` | See [Check usage](/usage-and-quotas#check-usage). |
Tomba also returns `block`, `greylisted`, `gibberish`, `regex`, `smtp_check`, and `whois`, which have no BounceBan counterpart.
## Status mapping
| BounceBan `result` | Tomba `status` | Tomba `result` |
| ----------------------------------- | -------------- | --------------- |
| `deliverable` | `valid` | `deliverable` |
| `risky` | `accept_all` | `risky` |
| `undeliverable` | `invalid` | `undeliverable` |
| `unknown` | `unknown` | `risky` |
| Reported with `is_disposable: true` | `disposable` | Empty string |
Branch on `status`, not `result`: Tomba's `unknown` status carries the result `risky`, and a disposable address has an empty `result`. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
| BounceBan | Tomba | What to do |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `400` invalid parameter | `422 params_invalid` | Fix the parameter named in the message. |
| `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. |
| `403` insufficient credits | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `405` account blocked | `401` or `403 authentication_failed` | [Contact support](https://app.tomba.io/contact). |
| `408` waterfall timeout | `408 proxy_error` | The mailbox check failed. Retry later. |
| `429` rate limited | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| No equivalent | `451 claimed_email` | The owner of the address asked Tomba to stop processing it. Remove it from your list. |
| `500` | `500 api_error` | Retry with backoff. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
Tomba answers a single verification synchronously, so remove any polling of `/v1/verify/single/status`. For lists, run a bulk job of type `verifier`:
| BounceBan step | Tomba step |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Create a task with `emails` or a CSV `file` | `POST /v1/bulk/verifier` with `name` and a newline-separated `list`, or a CSV `file`. Send BounceBan's `email_column` as `email_field_index`: both are zero-based. |
| Task starts on creation | Launch it: `PUT /v1/bulk/verifier/{id}` |
| `GET /v1/verify/bulk/status`, or the `bulk.task_finished` webhook | Poll `GET /v1/bulk/verifier/{id}/progress` until `status` is `completed`, or set `webhook_url` to receive a `bulk.completed` event. |
| Dump or export results | `GET /v1/bulk/verifier/{id}/download?file=full`. `file=valid` returns only valid rows. |
```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 verifies one address with Tomba and returns it in BounceBan's shape, so code that reads BounceBan results keeps working. 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 -> BounceBan result. Disposable addresses count as undeliverable.
const RESULTS = {
valid: "deliverable",
accept_all: "risky",
invalid: "undeliverable",
unknown: "unknown",
disposable: "undeliverable",
};
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) {
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const check = body.data.email;
return {
email: check.email || email,
result: RESULTS[check.status.toLowerCase()] ?? "unknown",
score: check.score,
is_disposable: check.disposable,
is_accept_all: check.accept_all,
is_free: check.webmail,
mx_records: check.mx?.records ?? [],
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`, and send them as `X-Tomba-Key` and `X-Tomba-Secret` instead of `Authorization`.
2. Point single verifications at `GET https://api.tomba.io/v1/email-verifier` and remove status polling and `try_again_at` handling.
3. Read results from `data.email` and translate them with the [status mapping](#status-mapping), comparing `status` case-insensitively.
4. Handle `402` where you handled `403`, retry `429` after `Retry-After`, and drop addresses that return `451`.
5. Replace bulk tasks with `verifier` jobs: create, launch, poll `/progress`, and download the CSV. Send `email_column` as `email_field_index`.
6. Verify the same sample list with both services and compare the mapped results before you switch production traffic.
---
## Document: Migrate from Voila Norbert
Map Voila Norbert name search, domain search, bulk, verifier, enrichment, and account API calls to Tomba endpoints, fields, and errors.
URL: /migration/email-finder/voilanorbert
# 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.
---
## Document: Migrate from Snov.io
Map Snov.io email finder, domain search, email verifier, LinkedIn, and account API methods to Tomba endpoints, fields, and statuses.
URL: /migration/email-finder/snov
# Migrate from Snov.io
This guide maps the Snov.io API (the v2 email finder, domain search, email verifier, LinkedIn profile, and company domain methods, and the v1 email count, profile, and balance methods) to Tomba. Snov.io API behavior checked on 2026-09-29 against the [Snov.io API reference](https://snov.io/api).
## 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).
- Snov.io's v2 methods are asynchronous: you start a task, then fetch its result with the `task_hash`. Tomba returns the result in the response, so each start and result pair becomes one request. Set your HTTP client timeout to at least 180 seconds.
## Endpoint mapping
Replace the host `api.snov.io` with `api.tomba.io`.
| Snov.io | Tomba | Notes |
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `POST /v1/oauth/access_token` | No equivalent | Send the key and secret on every request. |
| `POST /v2/emails-by-domain-by-name/start`, `GET /v2/emails-by-domain-by-name/result` | [`GET /v1/email-finder`](/api/finder#email-finder) | One person per request. |
| `POST /v2/domain-search/start`, `GET /v2/domain-search/result/{task_hash}` | [`GET /v1/domain-search`](/api/finder#domain-search) | Company details are in `data.organization`. |
| `POST /v2/domain-search/prospects/start` and `.../prospects/search-emails/start/{prospect_hash}`, with their results | [`GET /v1/domain-search`](/api/finder#domain-search) | Returns people and their addresses in one response. |
| `POST /v2/domain-search/domain-emails/start`, with its result | [`GET /v1/domain-search`](/api/finder#domain-search) | |
| `POST /v2/domain-search/generic-contacts/start`, with its result | [`GET /v1/domain-search`](/api/finder#domain-search) with `type=generic` | |
| `POST /v2/email-verification/start`, `GET /v2/email-verification/result` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) | One address per request. |
| `POST /v2/li-profiles-by-urls/start`, with its result | [`GET /v1/linkedin`](/api/finder#linkedin-finder) | One profile per request. Returns the person's email address with their name and position. |
| `POST /v2/company-domain-by-name/start`, with its result | [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) | One name per request. |
| `POST /v1/get-domain-emails-count` | [`GET /v1/email-count`](/api/finder#email-count) | |
| `POST /v1/get-profile-by-email` | [`GET /v1/enrich`](/api/finder#email-enrichment) | |
| `GET /v1/get-balance` | [`GET /v1/me`](/api/account#get-account) | See [Check usage](/usage-and-quotas#check-usage). |
## Authentication changes
Snov.io exchanges a client ID and secret for an access token that lasts one hour, then sends it as a bearer token. Tomba has no token exchange: send the key and secret as `X-Tomba-Key` and `X-Tomba-Secret` on every request.
```bash
# Snov.io: get an access token
curl -X POST "https://api.snov.io/v1/oauth/access_token" \
-d "grant_type=client_credentials" \
-d "client_id=$SNOV_CLIENT_ID" \
-d "client_secret=$SNOV_CLIENT_SECRET"
# Tomba: send the key and secret with the request
curl "https://api.tomba.io/v1/email-finder?domain=stripe.com&first_name=Jane&last_name=Doe" \
-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
| Snov.io method and field | Tomba parameter | Notes |
| ---------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Email finder `rows[].first_name`, `rows[].last_name` | `first_name`, `last_name` | Send one person per request, or use a bulk `finder` job for lists. |
| Email finder `rows[].domain` | `domain` | Tomba also accepts a company name as `company`. |
| Domain search `domain` | `domain` | |
| Prospects `positions[]` | No equivalent | `department` filters by department from a fixed list; see the [API reference](/api/finder#domain-search). |
| Prospects `page` | `page` | |
| Email verifier `emails[]` | `email` | Send one address per request, or use a bulk `verifier` job for lists. |
| LinkedIn profiles `urls[]` | `url` | |
| Company domain `names[]` | `query` | |
| Email count `domain` | `domain` | |
| Profile by email `email` | `email` | |
| `webhook_url` | `webhook_url` | Tomba still returns the result in the response, and also posts it to your URL when the result qualifies. See [Per-request callbacks](/webhook#per-request-callbacks). |
| `task_hash` | No equivalent | |
## Response field mapping
Tomba wraps every result in `data`.
| Snov.io | Tomba | Notes |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Result `status` (`completed`, `in_progress`) | No equivalent | Every Tomba response is final. |
| Finder `data[].people` | `data.full_name` | |
| Finder `data[].result[].email` | `data.email` | `null` when no address is found. |
| Finder and verifier `smtp_status` | Finder `data.verification.status`; verifier `data.email.status` | See [Status mapping](#status-mapping). |
| Verifier `is_valid_format` | `data.email.regex` | A malformed address fails with `422 params_invalid`. |
| Verifier `is_disposable`, `is_webmail`, `is_gibberish` | `data.email.disposable`, `.webmail`, `.gibberish` | The email finder rejects disposable and webmail domains with `400 unknown_record`. |
| `unknown_status_reason` | No equivalent | A catch-all domain gives the status `accept_all`. |
| Domain search `company_name`, `website`, `city`, `founded`, `industry`, `size` | `data.organization.organization`, `.website_url`, `.location.city`, `.founded`, `.industries`, `.company_size` | |
| Domain search `hq_phone` | No equivalent | [`GET /v1/companies/find`](/api/enrichment#company-api) returns `data.site.phoneNumbers`. |
| Domain search `emails_count`, `generic_contacts_count` | `data.total`, `data.generic_emails` from [`GET /v1/email-count`](/api/finder#email-count) | |
| Prospects `first_name`, `last_name`, `position` | `data.emails[].first_name`, `.last_name`, `.position` | |
| Prospects `source_page` | `data.emails[].sources[].uri` | |
| Domain emails `data[].email` | `data.emails[].email` | Each address comes with `verification.status`. |
| LinkedIn profiles `name`, `first_name`, `last_name`, `country` | `data.full_name`, `.first_name`, `.last_name`, `.country` | `data.country` is a two-letter country code. |
| LinkedIn profiles `positions[]` | `data.position`, `data.company` | The current position only. |
| LinkedIn profiles `industry`, `location`, `skills` | No equivalent | |
| Email count `result` | `data.total` | |
| Profile by email `name`, `firstName`, `lastName`, `country` | `data.full_name`, `.first_name`, `.last_name`, `.country` | |
| Profile by email `currentJobs[]` | `data.position`, `data.company`, `data.website_url` | |
| Profile by email `previousJobs[]`, `logo`, `locality`, `lastUpdateDate` | No equivalent | |
| Balance `data.balance` | `GET /v1/me` | See [Check usage](/usage-and-quotas#check-usage). |
## Status mapping
| Snov.io `smtp_status` | Tomba `status` | Tomba `result` (verifier) |
| -------------------------------------------------- | -------------- | ------------------------- |
| `valid` | `valid` | `deliverable` |
| `not_valid` | `invalid` | `undeliverable` |
| `unknown`, with `unknown_status_reason` `catchall` | `accept_all` | `risky` |
| `unknown`, other reasons | `unknown` | `risky` |
| Reported with `is_disposable: true` | `disposable` | Empty string |
The email finder uses the same status values in `data.verification.status`, which is `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
Snov.io reports a lack of credits in the result's `status` field. Tomba returns an HTTP error status with an `errors` object:
| Snov.io | Tomba | What to do |
| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Result `status` `not_enough_credits` | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| Expired access token | No equivalent | Tomba has no access tokens. A key past its expiry date returns `400 api_key_expired`; [rotate](/authentication#rotate-keys) it. |
Also handle Tomba's `400` and `401 authentication_failed` for missing or wrong credentials, `422 params_invalid` for bad parameters, `429 rate_limit` when you exceed your plan's rate limits (see [Handle 429 responses](/rate-limits#handle-429-responses)), and `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
Replace each Snov.io start and result pair with a single Tomba request, and remove the code that stores `task_hash` values and polls for results. For lists, run a Tomba bulk job instead of sending batches of 10:
| Snov.io method | Tomba bulk type |
| ------------------------------- | ------------------------------------------------------------------------------------------- |
| Email finder rows | `finder`: a CSV file with name and domain columns. See [Email finder](/bulks#email-finder). |
| Email verifier `emails[]` | `verifier`: a JSON `list` or a CSV file. See [Email verifier](/bulks#email-verifier). |
| Domain search over many domains | `search`: a JSON `list` of domains. See [Domain search](/bulks#domain-search). |
| LinkedIn profile URLs | `linkedin`: a JSON `list` of profile URLs. See [LinkedIn finder](/bulks#linkedin-finder). |
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 the `emails-by-domain-by-name` start and result calls. It returns the same shape as Snov.io's result `data` array. It needs Node.js 18 or later.
```js title="find-emails.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
// rows: [{ first_name, last_name, domain }], as sent to Snov.io.
async function emailsByDomainByName(rows) {
const data = [];
for (const { first_name, last_name, domain } of rows) {
const url = new URL("https://api.tomba.io/v1/email-finder");
url.search = new URLSearchParams({
first_name,
last_name,
domain,
}).toString();
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;
const status = found.verification?.status?.toLowerCase();
data.push({
people: `${first_name} ${last_name}`,
result: found.email
? [
{
email: found.email,
smtp_status: status === "valid" ? "valid" : "unknown",
},
]
: [],
});
}
return data;
}
const data = await emailsByDomainByName([
{ first_name: "Jane", last_name: "Doe", domain: "stripe.com" },
{ first_name: "John", last_name: "Smith", domain: "shopify.com" },
]);
console.log(JSON.stringify(data, null, 2));
```
## 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 the access token exchange and refresh.
2. Replace each start and result pair with one `GET` request, and delete `task_hash` storage and polling.
3. Send one person, address, profile, or name per request, and move large lists to bulk jobs.
4. Translate `smtp_status` and `unknown_status_reason` with the [status mapping](#status-mapping), comparing `status` case-insensitively.
5. Handle `402` where you handled `not_enough_credits`, retry `429` after `Retry-After`, and drop addresses that return `451`.
6. Run the same sample of people and addresses through both services and compare the results before you switch production traffic.
---
## Document: Migrate from Skrapp
Map Skrapp email finder, bulk finder, email verifier, account, and list API calls to Tomba endpoints, fields, statuses, and errors.
URL: /migration/email-finder/skrapp
# Migrate from Skrapp
This guide maps the Skrapp API (the v2 finder, account, and list endpoints and the v3 verifier endpoints) to Tomba. Skrapp API behavior checked on 2026-09-30 against the [Skrapp API reference](https://skrapp.io/api).
## 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 host `api.skrapp.io` with `api.tomba.io`, and the `/api/v2` and `/v3` prefixes with `/v1`.
| Skrapp | Tomba | Notes |
| -------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
| `GET /api/v2/find` | [`GET /v1/email-finder`](/api/finder#email-finder) | |
| `POST /api/v2/find_bulk` | `POST /v1/bulk/finder` | See [Bulk and async jobs](#bulk-and-async-jobs). |
| `GET /v3/verify` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) | |
| `GET /v3/verify` with `enrich` | [`GET /v1/enrich`](/api/finder#email-enrichment) | Returns the person and company for an address. |
| `GET /v3/verify_bulk` | `POST /v1/bulk/verifier` | See [Email verifier](/bulks#email-verifier). |
| `GET /api/v2/account` | [`GET /v1/me`](/api/account#get-account) | See [Check usage](/usage-and-quotas#check-usage). |
| `GET /api/v2/list` | [`GET /v1/leads_lists`](/api/lead-lists#retrieve-leads-lists) | |
| `GET /api/v2/list/:listId/leads` | [`GET /v1/leads`](/api/leads#retrieve-leads) with `list` | Pages with `page` and `limit` instead of `start` and `size`. |
## Authentication changes
Skrapp reads the key from the `X-Access-Key` header. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Skrapp
curl "https://api.skrapp.io/api/v2/find?firstName=Jane&lastName=Doe&domain=stripe.com" \
-H "X-Access-Key: $SKRAPP_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
| Skrapp | Tomba | Notes |
| ------------------------------ | ------------------------- | --------------------------------------------------------------------------------- |
| Finder `firstName`, `lastName` | `first_name`, `last_name` | Tomba also accepts `full_name`. |
| Finder `domain` | `domain` | Tomba returns `400 unknown_record` for a disposable or webmail domain. |
| Finder `company` | `company` | When you send both, Tomba uses `domain`. |
| Verifier `email` | `email` | A malformed address returns `422 params_invalid` instead of a result. |
| Verifier `enrich` | No equivalent | Call [`GET /v1/enrich`](/api/finder#email-enrichment) for the person and company. |
| List leads `listId` | `list` | |
| List leads `start`, `size` | `page`, `limit` | Tomba pages start at 1: `page` = `start` / `limit` + 1. |
| List leads `kw` | No equivalent | |
## Response field mapping
Tomba wraps every result in `data`.
| Skrapp | Tomba | Notes |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Finder `email` | `data.email` | `null` when no address is found; the status is `200`, not `404`. |
| Finder `accuracy` | `data.score` | Computed differently. Recalibrate any threshold you apply to it. |
| Finder `firstName`, `lastName` | `data.first_name`, `data.last_name` | |
| Finder `companyName` | `data.company` | |
| Finder `quality.status` | `data.verification.status` | `null` when the address hasn't been verified. See [Status mapping](#status-mapping). |
| Finder `quality.result` | No equivalent | Verify the address with [`GET /v1/email-verifier`](/api/verifier#email-verifier) for a `result`. |
| Verifier `email` | `data.email.email` | An empty string when the domain is disposable. |
| Verifier `email_status` | `data.email.status` | See [Status mapping](#status-mapping). |
| Verifier `result` | `data.email.result` | See [Result values](/attributes/verifier#result-values). |
| Verifier `firstName`, `lastName`, `companyName`, `title` | `data.first_name`, `.last_name`, `.company`, `.position` from [`GET /v1/enrich`](/api/finder#email-enrichment) | |
| Account `credits.remaining`, `credits.total` | `GET /v1/me` | See [Check usage](/usage-and-quotas#check-usage). |
Tomba's email finder also returns `full_name`, `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
| Skrapp `email_status` | Tomba `status` | Tomba `result` |
| --------------------- | -------------- | --------------- |
| `valid` | `valid` | `deliverable` |
| `catch-all` | `accept_all` | `risky` |
| `invalid` | `invalid` | `undeliverable` |
| `unknown` | `unknown` | `risky` |
Tomba also returns `disposable`, with an empty `result`, for a disposable domain. The email finder uses the same status values in `data.verification.status`. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
Skrapp returns an `error` code, a `message`, and the `remaining` credits. Tomba returns an `errors` object with `type`, `message`, and `code`:
| Skrapp | Tomba | What to do |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `400` missing or malformed parameter | `422 params_invalid` | Fix the parameter named in the message. |
| `401` invalid, missing, or revoked 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. |
| `404` no address found | `200` with `data.email` `null` | Check `data.email` instead of the status. |
| `429` rate limited | `429 rate_limit` | Retry after the `Retry-After` delay; 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
Skrapp's bulk finder takes up to 100 people in a JSON body and returns a job `id` to poll. Its bulk verifier takes up to 50 addresses per request and answers directly. Tomba runs both as bulk jobs:
| Skrapp endpoint | Tomba bulk type |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `POST /api/v2/find_bulk` | `finder`: a CSV file or JSON `data` rows with name and domain or company columns. See [Email finder](/bulks#email-finder). |
| `GET /v3/verify_bulk` | `verifier`: a JSON `list` of addresses or a CSV file. See [Email verifier](/bulks#email-verifier). |
Each job is created, launched, polled, and downloaded as CSV; see [Lifecycle](/bulks#lifecycle). Row limits are in [Bulk types](/bulks#bulk-types). 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 `tId` you attach to each Skrapp row has no equivalent. Keep your own ID in an extra CSV column.
## Code example
This function replaces a call to Skrapp's `GET /api/v2/find` and returns Skrapp's field names, or `null` where Skrapp returned `404`. It needs Node.js 18 or later.
```js title="find.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
// Tomba status -> Skrapp email_status.
const SKRAPP_STATUS = {
valid: "valid",
accept_all: "catch-all",
invalid: "invalid",
unknown: "unknown",
};
// Accepts Skrapp's parameters: firstName, lastName, and domain or company.
async function find({ firstName, lastName, domain, company }) {
const url = new URL("https://api.tomba.io/v1/email-finder");
url.searchParams.set("first_name", firstName);
url.searchParams.set("last_name", lastName);
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;
if (!found.email) return null;
const status = found.verification?.status?.toLowerCase();
return {
email: found.email,
accuracy: found.score,
firstName: found.first_name,
lastName: found.last_name,
companyName: found.company,
quality: { status: SKRAPP_STATUS[status] ?? null },
};
}
console.log(
await find({ firstName: "Jane", lastName: "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-Access-Key`.
2. Change the host and path prefixes, and rename `firstName` and `lastName` to `first_name` and `last_name`.
3. Treat `data.email` `null` as not found instead of a `404`.
4. Translate `email_status` with the [status mapping](#status-mapping), comparing `status` case-insensitively, and call `/v1/enrich` where you used `enrich=true`.
5. Handle `402` for exhausted credits, retry `429` after `Retry-After`, and drop addresses that return `451`.
6. Move `find_bulk` and `verify_bulk` batches to `finder` and `verifier` bulk jobs.
7. Run the same sample of people and addresses through both services and compare the results before you switch production traffic.
---
## Document: Migrate from Prospeo
Map Prospeo Enrich Person, Enrich Company, bulk, search, and account API calls to Tomba endpoints, parameters, fields, and errors.
URL: /migration/email-finder/prospeo
# Migrate from Prospeo
This guide maps the Prospeo API (Enrich Person, Bulk Enrich Person, Enrich Company, Bulk Enrich Company, Search Person, Search Company, and Account Information) to Tomba. Prospeo API behavior checked on 2026-09-29 against the [Prospeo API documentation](https://prospeo.io/api-docs).
## 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).
- Prospeo's Enrich Person takes whichever datapoints you have. Tomba has one endpoint per input: a name and company, a LinkedIn URL, or an email address. Route each call by the datapoint you send.
## Endpoint mapping
Replace the host `api.prospeo.io` with `api.tomba.io`.
| Prospeo | Tomba | Notes |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `POST /enrich-person` with a name and company | [`GET /v1/email-finder`](/api/finder#email-finder) | |
| `POST /enrich-person` with `linkedin_url` | [`GET /v1/linkedin`](/api/finder#linkedin-finder) | |
| `POST /enrich-person` with `email` | [`GET /v1/enrich`](/api/finder#email-enrichment) | |
| `POST /enrich-person` with `only_verified_mobile` | [`GET /v1/phone-finder`](/api/phone#phone-finder) | Accepts an email address, a LinkedIn URL, or a domain. |
| `POST /bulk-enrich-person` | Bulk jobs of type `finder`, `linkedin`, or `enrich` | See [Bulk and async jobs](#bulk-and-async-jobs). |
| `POST /enrich-company` | [`GET /v1/companies/find`](/api/enrichment#company-api) | Looks up by domain only. |
| `POST /bulk-enrich-company` | Bulk jobs of type `company` | |
| `POST /search-person` filtered by company website | [`GET /v1/domain-search`](/api/finder#domain-search) | Returns people with their email addresses, one domain per request. |
| `POST /search-company` | [`POST /v1/reveal/search`](/api/reveal#search-companies) | Uses Tomba's own filters. |
| `POST /search-suggestions` | No equivalent | |
| `GET /account-information` | [`GET /v1/me`](/api/account#get-account) | See [Check usage](/usage-and-quotas#check-usage). |
| Older `/email-finder`, `/domain-search`, and `/email-verifier` paths | `GET /v1/email-finder`, `GET /v1/domain-search`, `GET /v1/email-verifier` | |
## Authentication changes
Prospeo reads the key from the `X-KEY` header. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and its lookups are `GET` requests with query parameters instead of `POST` requests with JSON bodies:
```bash
# Prospeo
curl -X POST "https://api.prospeo.io/enrich-person" \
-H "X-KEY: $PROSPEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"only_verified_email": true, "data": {"full_name": "Jane Doe", "company_website": "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
| Prospeo field | Tomba parameter | Notes |
| ---------------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `data.first_name`, `data.last_name`, `data.full_name` | `first_name`, `last_name`, `full_name` | On `/v1/email-finder`. |
| `data.company_website` | `domain` | |
| `data.company_name` | `company` | |
| `data.company_linkedin_url` | No equivalent | Send `domain` or `company`. |
| `data.linkedin_url` | `url` | On `/v1/linkedin`. |
| `data.email` | `email` | On `/v1/enrich`. |
| `data.person_id` | No equivalent | |
| `only_verified_email` | No equivalent | Filter on `data.verification.status`; see [Status mapping](#status-mapping). |
| `enrich_mobile` | `enrich_mobile` | Adds phone numbers in `data.phone_data`, at an extra cost. |
| `only_verified_mobile` | No equivalent | Use `/v1/phone-finder`. |
| Enrich Company `data.company_website` | `domain` | On `/v1/companies/find`. |
| Enrich Company `data.company_name`, `data.company_linkedin_url`, `data.company_id` | No equivalent | Get the domain for a company name with [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions). |
| Search Person `filters.company.websites.include` | `domain` | On `/v1/domain-search`, one domain per request. |
| Search `page` | `page` | |
## Response field mapping
Tomba wraps every result in `data`. For Enrich Person calls answered by `/v1/email-finder`, `/v1/linkedin`, or `/v1/enrich`:
| Prospeo | Tomba | Notes |
| ------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `person.email.email` | `data.email` | `null` when no address is found. |
| `person.email.status` | `data.verification.status` | See [Status mapping](#status-mapping). |
| `person.email.revealed`, `verification_method` | No equivalent | |
| `person.email.email_mx_provider` | No equivalent | The email verifier returns `data.email.smtp_provider`. |
| `person.first_name`, `last_name`, `full_name` | `data.first_name`, `.last_name`, `.full_name` | |
| `person.linkedin_url` | `data.linkedin` | |
| `person.current_job_title` | `data.position` | |
| `person.location.country_code` | `data.country` | |
| `person.location.country`, `state`, `city`, `time_zone` | No equivalent | |
| `person.mobile` | `data.phone_data` | Returned with `enrich_mobile=true`. See [Phone](/attributes/phone). |
| `person.person_id`, `headline`, `job_history`, `skills` | No equivalent | |
| `company.name`, `company.domain` | `data.company`, `data.website_url` | |
| `free_enrichment` | No equivalent | Repeating a request within your usage window is free; see [Duplicate requests](/usage-and-quotas#duplicate-requests). |
For Enrich Company calls answered by `/v1/companies/find`, the company is in `data`:
| Prospeo `company` | Tomba | Notes |
| ------------------------------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `name`, `domain`, `description` | `data.name`, `data.domain`, `data.description` | |
| `employee_range` | `data.metrics.employees` | A range, such as `51-200`. |
| `employee_count` | No equivalent | |
| `industry` | No equivalent | [`GET /v1/domain-search`](/api/finder#domain-search) returns it in `data.organization.industries`. |
| `founded` | `data.foundedYear` | A string. |
| `location.country`, `country_code`, `state`, `city` | `data.geo.country`, `.countryCode`, `.state`, `.city` | |
| `linkedin_url`, `twitter_url`, `facebook_url` | `data.linkedin.handle`, `data.twitter.handle`, `data.facebook.handle` | Handles, not URLs. |
| `technology.technology_names` | `data.tech` | |
| `sic_codes`, `naics_codes` | `data.category.sic4Codes`, `data.category.naics6Codes` | |
| `phone_hq` | `data.site.phoneNumbers` | |
| `logo_url` | No equivalent | Use `https://logo.tomba.io/{domain}`; see [Logo API](/logo-api). |
| `revenue_range`, `funding`, `job_postings`, `keywords` | No equivalent | |
Field definitions are in [Enrichment](/attributes/enrichment).
## Status mapping
| Prospeo `email.status` | Tomba |
| ---------------------- | ------------------------------------------------------------------- |
| `VERIFIED` | `data.verification.status` is `valid` |
| `UNAVAILABLE` | `data.email` is `null`, or `data.verification.status` isn't `valid` |
Tomba returns its best match with a status, including catch-all (`accept_all`) and unconfirmed (`unknown`) addresses. If you sent `only_verified_email: true`, keep only results whose status is `valid`. Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
Prospeo returns errors with HTTP 400 and an `error_code`. Tomba uses a distinct HTTP status for each cause, with an `errors` object:
| Prospeo `error_code` | Tomba | What to do |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `NO_MATCH` | `200` with `data.email` `null` | Treat as no match. `/v1/companies/find` returns `200` with an `errors` body, `type` `not_found`. |
| `INVALID_DATAPOINTS`, `INVALID_REQUEST` | `422 params_invalid` | Fix the parameter named in the message. |
| `INSUFFICIENT_CREDITS` | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `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. |
| `INTERNAL_ERROR` | `500 api_error` | Retry with backoff. |
| `429` rate limit | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| No equivalent | `400 unknown_record` | The email finder received a disposable or webmail domain. |
| No equivalent | `451 claimed_email` | The owner of the address asked Tomba to stop processing it. Remove it from your data. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
Prospeo's bulk endpoints answer up to 50 records in one synchronous response. A Tomba bulk job runs in the background: create it, launch it, poll its progress, and download the results as CSV. For a few records, send single requests instead.
| Prospeo | Tomba bulk type |
| ------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Bulk Enrich Person with names and companies | `finder`: a CSV file with name and domain columns. See [Email finder](/bulks#email-finder). |
| Bulk Enrich Person with LinkedIn URLs | `linkedin`: a JSON `list` of profile URLs. See [LinkedIn finder](/bulks#linkedin-finder). |
| Bulk Enrich Person with email addresses | `enrich`: a JSON `list` of addresses. See [Email enrichment](/bulks#email-enrichment). |
| Bulk Enrich Company | `company`: a JSON `list` of domains. See [Company enrichment](/bulks#company-enrichment). |
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 an Enrich Person call made with a full name, a company website, and `only_verified_email: true`, and returns Prospeo's field names. It needs Node.js 18 or later.
```js title="enrich-person.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
async function enrichPerson(fullName, companyWebsite) {
const url = new URL("https://api.tomba.io/v1/email-finder");
url.searchParams.set("full_name", fullName);
url.searchParams.set("domain", companyWebsite);
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}`,
);
}
// Like only_verified_email: true, return only addresses verified as valid.
const found = body.data;
if (!found.email || found.verification?.status?.toLowerCase() !== "valid") {
return { error: true, error_code: "NO_MATCH" };
}
return {
error: false,
person: {
full_name: found.full_name,
first_name: found.first_name,
last_name: found.last_name,
current_job_title: found.position || null,
linkedin_url: found.linkedin || null,
email: { status: "VERIFIED", email: found.email },
},
company: { name: found.company, domain: found.website_url },
};
}
console.log(await enrichPerson("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 `X-KEY`.
2. Route each Enrich Person call by its datapoint: name and company to `/v1/email-finder`, LinkedIn URL to `/v1/linkedin`, email address to `/v1/enrich`.
3. Turn JSON bodies into query parameters: `company_website` becomes `domain` and `company_name` becomes `company`.
4. Read `data.email` and `data.verification.status`, and keep only `valid` addresses where you used `only_verified_email`.
5. Replace `error_code` handling with HTTP status handling: `402`, `422`, `429` with `Retry-After`, and `451`.
6. Move bulk enrichment to Tomba bulk jobs, or loop over single requests for small batches.
7. Run the same sample of people and companies through both services and compare the results before you switch production traffic.
---
## Document: Migrate from an email finder
Guides for moving email finding from Hunter, Apollo.io, Snov.io, and other providers to Tomba, and what every migration changes.
URL: /migration/email-finder/introduction
# Migrate from an email finder
Each guide maps one provider's email finder, domain search, and related lookups to the Tomba [Email Finder](/api/finder#email-finder), [Domain Search](/api/finder#domain-search), and [bulk jobs](/bulks).
## Guides
| Provider | Guide |
| ------------- | -------------------------------------------------------------------- |
| Hunter | [Migrate from Hunter (email finder)](/migration/email-finder/hunter) |
| Snov.io | [Migrate from Snov.io](/migration/email-finder/snov) |
| Prospeo | [Migrate from Prospeo](/migration/email-finder/prospeo) |
| Findymail | [Migrate from Findymail](/migration/email-finder/findymail) |
| Apollo.io | [Migrate from Apollo.io](/migration/email-finder/apollo) |
| Voila Norbert | [Migrate from Voila Norbert](/migration/email-finder/voilanorbert) |
| Skrapp | [Migrate from Skrapp](/migration/email-finder/skrapp) |
## What changes in every migration
| Area | Change |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Endpoints | Tomba has one endpoint per input: a name and company go to [`GET /v1/email-finder`](/api/finder#email-finder), a domain to [`GET /v1/domain-search`](/api/finder#domain-search), a LinkedIn URL to [`GET /v1/linkedin`](/api/finder#linkedin-finder), an email address to [`GET /v1/enrich`](/api/finder#email-enrichment), and verification to [`GET /v1/email-verifier`](/api/verifier#email-verifier). |
| Authentication | Every request sends `X-Tomba-Key` and `X-Tomba-Secret` headers. There's no token exchange, and Tomba doesn't read keys from the query string. See [Authentication](/authentication). |
| Requests | Lookups are `GET` requests with query parameters, one person or domain per request. |
| Timing | Each request returns a final result: there are no task IDs to store or results to poll for. Set your client timeout to at least 180 seconds. |
| No result | The email finder returns `200` with `data.email` set to `null`. |
| Verification | The email finder returns its best match with a status in `data.verification.status`, including catch-all and unconfirmed addresses. See [Status values](/attributes/verifier#status-values). |
| Errors | Out of credits is `402 quota_exceeded`, throttling is `429 rate_limit` with `Retry-After`, a withheld address is `451 claimed_email`, and a disposable or webmail domain sent to the email finder is `400 unknown_record`. See [Status codes](/error-handling#status-codes). |
| Billing | Only some results are charged, and repeating a request within your usage window is free. See [Credit costs](/usage-and-quotas#credit-costs) and [Duplicate requests](/usage-and-quotas#duplicate-requests). |
| Lists | Run a bulk job: `finder` takes rows of names and companies, as JSON `data` or a CSV file, `search` a `list` of domains, and `linkedin`, `enrich`, and `verifier` either one. Create and launch the job, poll its progress or receive its `webhook_url` event, and download a CSV. See [Lifecycle](/bulks#lifecycle). |
---
## Document: Migrate from Hunter (email finder)
Map Hunter Domain Search, Email Finder, Email Count, and enrichment API calls to their Tomba equivalents.
URL: /migration/email-finder/hunter
# Migrate from Hunter (email finder)
This guide maps the Hunter API v2 Domain Search, Email Finder, Email Count, Account, and People, Companies, and Combined enrichment endpoints to Tomba. Hunter's Email Verifier is covered in [Migrate from Hunter (email verifier)](/migration/email-verifier/hunter). Hunter API behavior checked on 2026-09-29 against the [Hunter API v2 reference](https://hunter.io/api-documentation/v2).
## 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.hunter.io/v2` with `https://api.tomba.io/v1`.
| Hunter | Tomba | Notes |
| ------------------------------------------ | ------------------------------------------------------- | ------------------------------------------------- |
| `GET /domain-search` | [`GET /v1/domain-search`](/api/finder#domain-search) | See [Domain search](#domain-search). |
| `GET /email-finder` | [`GET /v1/email-finder`](/api/finder#email-finder) | See [Email finder](#email-finder). |
| `GET /email-finder` with `linkedin_handle` | [`GET /v1/linkedin`](/api/finder#linkedin-finder) | |
| `GET /email-count` | [`GET /v1/email-count`](/api/finder#email-count) | |
| `GET /people/find` | [`GET /v1/people/find`](/api/enrichment#person-api) | |
| `GET /companies/find` | [`GET /v1/companies/find`](/api/enrichment#company-api) | |
| `GET /combined/find` | [`GET /v1/combined/find`](/api/enrichment#combined-api) | |
| `GET /account` | [`GET /v1/me`](/api/account#get-account) | See [Check usage](/usage-and-quotas#check-usage). |
## Authentication changes
Hunter accepts the key as the `api_key` parameter, the `X-API-KEY` header, or a bearer token. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`, and doesn't accept credentials in the query string:
```bash
# Hunter
curl "https://api.hunter.io/v2/email-finder?domain=stripe.com&first_name=Jane&last_name=Doe" \
-H "X-API-KEY: $HUNTER_API_KEY"
# Tomba
curl "https://api.tomba.io/v1/email-finder?domain=stripe.com&first_name=Jane&last_name=Doe" \
-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).
## Domain search
### Parameters
| Hunter | Tomba | Notes |
| ------------------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `domain` | `domain` | |
| `company` | `company` | |
| `limit` | `limit` | Tomba accepts a fixed set of values; see the [API reference](/api/finder#domain-search). The cost depends on `limit`; see [Credit costs](/usage-and-quotas#credit-costs). |
| `offset` | `page` | Tomba pages start at 1: `page` = `offset` / `limit` + 1. |
| `type` | `type` | |
| `department` | `department` | Comma-separated in both APIs, but the department names differ. The values are in the [API reference](/api/finder#domain-search). |
| `location` with a country | `country` | A two-letter country code. Continent, region, state, and city filters have no equivalent. |
| `seniority`, `verification_status` | No equivalent | Each result carries `seniority` and `verification.status`; filter the results in your code. |
| `decision_maker`, `required_field`, `job_titles` | No equivalent | |
| `aggregations` | No equivalent | Use [`GET /v1/email-count`](/api/finder#email-count) for counts by department and seniority. |
### Response fields
| Hunter | Tomba | Notes |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `data.domain` | `data.organization.website_url` | |
| `data.organization` | `data.organization.organization` | |
| `data.pattern` | `data.organization.pattern` | |
| `data.accept_all`, `data.disposable`, `data.webmail` | `data.organization.accept_all`, `.disposable`, `.webmail` | |
| `data.linked_domains` | No equivalent | |
| `data.emails[].value` | `data.emails[].email` | |
| `data.emails[].confidence` | `data.emails[].score` | Computed differently. Recalibrate any threshold you apply to it. |
| `data.emails[].type`, `first_name`, `last_name`, `position`, `seniority`, `department` | Same names | Department values differ. |
| `data.emails[].twitter`, `linkedin` | Same names | Tomba returns full profile URLs; Hunter returns the Twitter handle. |
| `data.emails[].phone_number` | `data.emails[].phone_number` | A boolean in Tomba. Request numbers with `enrich_mobile=true`, at an extra cost. |
| `data.emails[].position_raw`, `decision_maker` | No equivalent | |
| `data.emails[].verification` | `data.emails[].verification` | Same `date` and `status` keys. |
| `data.emails[].sources[].domain` | `data.emails[].sources[].website_url` | `uri`, `extracted_on`, `last_seen_on`, and `still_on_page` keep their names. |
| `meta.results` | `meta.total` | |
| `meta.limit`, `meta.offset` | `meta.pageSize`, `meta.current` | `meta.current` is the page number. `meta.total_pages` gives the page count. |
| `meta.results_approximate` | No equivalent | |
Tomba also returns company details in `data.organization`, such as `industries`, `company_size`, `location`, and `social_links`.
## Email finder
### Parameters
| Hunter | Tomba | Notes |
| ------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
| `domain` | `domain` | Tomba returns `400 unknown_record` for a disposable or webmail domain. |
| `company` | `company` | |
| `first_name`, `last_name` | `first_name`, `last_name` | |
| `full_name` | `full_name` | |
| `linkedin_handle` | `url` on [`GET /v1/linkedin`](/api/finder#linkedin-finder) | Send the profile URL. |
| `max_duration` | No equivalent | Set the timeout in your HTTP client. |
### Response fields
| Hunter | Tomba | Notes |
| ------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data.email` | `data.email` | `null` when no address is found; the status is still `200`. |
| `data.first_name`, `last_name`, `position`, `company`, `accept_all` | Same names | |
| `data.score` | `data.score` | Computed differently. |
| `data.domain` | `data.website_url` | |
| `data.twitter` | `data.twitter` | A full URL in Tomba. |
| `data.linkedin_url` | `data.linkedin` | |
| `data.phone_number` | `data.phone_number` | A boolean in Tomba. |
| `data.verification` | `data.verification` | Same `date` and `status` keys. Tomba can also return `invalid`, or `null` when the address hasn't been verified; see [Status values](/attributes/verifier#status-values). |
| `data.sources[]` | `data.sources[]` | `domain` is `website_url`. |
| `meta.params` | No equivalent | |
Tomba also returns `full_name`, `pattern`, `department`, `type`, `country`, and `gender`.
## Email count and enrichment
| Hunter | Tomba | Notes |
| ---------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Email Count `domain`, `type` | `domain`, `type` | Tomba doesn't accept `company` here. |
| Email Count `data.total`, `personal_emails`, `generic_emails`, `department`, `seniority` | Same paths | The department keys differ; see [Email count](/attributes/email-count). |
| People `email` | `email` | Tomba doesn't accept `linkedin_handle`. |
| Companies `domain` | `domain` | |
| `clearbit_format` | No equivalent | Tomba's enrichment responses always use the Clearbit-style shape; see [Enrichment](/attributes/enrichment). |
| `404` when nothing is found | `200` with an `errors` body, `type` `not_found` | Check the body for `errors`. |
## Error mapping
Hunter returns `errors` as an array of `{id, code, details}`. Tomba returns a single `errors` object with `type`, `message`, and `code`. The meaning of `403` and `429` also changes:
| Hunter | Tomba | What to do |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `400` `wrong_params` or another `invalid_*` id | `422 params_invalid` | Fix the parameter named in the message. |
| `400` `invalid_domain` | `400 unknown_record` for a disposable or webmail domain | Use a company domain. |
| `401` no valid 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. |
| `403` rate limit reached | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `429` usage limit reached | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `429` `restricted_account` | `401 authentication_failed` | [Contact support](https://app.tomba.io/contact). |
| `451` `claimed_email` | `451 claimed_email` | Same meaning. Remove the address from your data. |
| `5xx` | `500 api_error` | Retry with backoff. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
Hunter runs bulk Domain Search and Email Finder lists from its dashboard, and its API endpoints take one item per request. Tomba runs lists through the API as bulk jobs:
| Hunter task | Tomba bulk type |
| ---------------------------------- | -------------------------------------------------------------------------------------------- |
| Domain Search over many domains | `search`: a JSON `list` of domains. See [Domain search](/bulks#domain-search). |
| Email Finder over a list of people | `finder`: a CSV file with name and company columns. See [Email finder](/bulks#email-finder). |
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 call to Hunter's Email Finder and returns Hunter's field names, or `null` when no address is found. It needs Node.js 18 or later.
```js title="email-finder.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
// Accepts Hunter's parameters: domain or company, and first_name + last_name or full_name.
async function emailFinder(params) {
const url = new URL("https://api.tomba.io/v1/email-finder");
for (const key of [
"domain",
"company",
"first_name",
"last_name",
"full_name",
]) {
if (params[key]) url.searchParams.set(key, params[key]);
}
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;
return {
first_name: found.first_name,
last_name: found.last_name,
email: found.email,
score: found.score,
domain: found.website_url,
accept_all: found.accept_all,
position: found.position,
twitter: found.twitter,
linkedin_url: found.linkedin,
company: found.company,
sources: (found.sources ?? []).map(({ website_url, ...source }) => ({
domain: website_url,
...source,
})),
verification: found.verification ?? null,
};
}
console.log(
await emailFinder({
domain: "stripe.com",
first_name: "Jane",
last_name: "Doe",
}),
);
```
## 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`, `X-API-KEY`, or a bearer token.
2. Change the base URL, convert `offset` to `page`, send `linkedin_handle` lookups to `/v1/linkedin`, and filter results in code where Tomba has no filter.
3. Update response parsing: `value` becomes `email`, source `domain` becomes `website_url`, and the domain search company fields move under `data.organization`.
4. Swap error handling: `402` replaces Hunter's `429`, `429` replaces Hunter's `403`, and `errors` is an object, not an array.
5. Move list processing to bulk jobs.
6. Run the same sample of domains and people through both services and compare the results before you switch production traffic.
---
## Document: Migrate from Findymail
Map Findymail finder, reverse lookup, phone, company, and verifier API calls to Tomba endpoints, parameters, fields, and errors.
URL: /migration/email-finder/findymail
# Migrate from Findymail
This guide maps the Findymail API (the `/api/search/*` endpoints, `/api/verify`, and `/api/credits`) to Tomba. Findymail API behavior checked on 2026-09-29 against the [Findymail API reference](https://app.findymail.com/docs/).
## 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).
- Findymail endpoints take `POST` requests with JSON bodies. Tomba lookups are `GET` requests with query parameters.
## Endpoint mapping
Replace the host `app.findymail.com` with `api.tomba.io`.
| Findymail | Tomba | Notes |
| ----------------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------- |
| `POST /api/search/name` | [`GET /v1/email-finder`](/api/finder#email-finder) | |
| `POST /api/search/domain` | [`GET /v1/domain-search`](/api/finder#domain-search) | |
| `POST /api/search/employees` | [`GET /v1/domain-search`](/api/finder#domain-search) | Returns people with their addresses. Filters by department, not job title. |
| `POST /api/search/business-profile` | [`GET /v1/linkedin`](/api/finder#linkedin-finder) | |
| `POST /api/search/reverse-email` | [`GET /v1/enrich`](/api/finder#email-enrichment) | Returns the person, company, and LinkedIn URL for an address. |
| `POST /api/search/phone` | [`GET /v1/phone-finder`](/api/phone#phone-finder) | Also accepts an email address or a domain. |
| `POST /api/search/company` | [`GET /v1/companies/find`](/api/enrichment#company-api) | Looks up by domain only. |
| `POST /api/verify` | [`GET /v1/email-verifier`](/api/verifier#email-verifier) | |
| `GET /api/credits` | [`GET /v1/me`](/api/account#get-account) | |
## Authentication changes
Findymail reads a bearer token. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Findymail
curl -X POST "https://app.findymail.com/api/search/name" \
-H "Authorization: Bearer $FINDYMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Jane Doe", "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
| Findymail endpoint and field | Tomba parameter | Notes |
| -------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `search/name` `name` | `full_name` | Or send `first_name` and `last_name`. |
| `search/name` `domain` | `domain` or `company` | Findymail takes a domain or a company name in one field. Send a domain as `domain` and a company name as `company`. |
| `webhook_url` | `webhook_url` | Tomba still returns the result in the response, and also posts it to your URL when the result qualifies. See [Per-request callbacks](/webhook#per-request-callbacks). |
| `search/domain` `domain`, `search/employees` `website` | `domain` | |
| `search/domain` `roles`, `search/employees` `job_titles` | `department` | Tomba filters by department from a fixed list, not by free-text role. The values are in the [API reference](/api/finder#domain-search). |
| `search/employees` `count` | `limit` | The cost depends on `limit`; see [Credit costs](/usage-and-quotas#credit-costs). |
| `search/business-profile` `linkedin_url` | `url` | |
| `search/reverse-email` `email` | `email` | |
| `search/reverse-email` `with_profile` | No equivalent | `/v1/enrich` returns every person field it has. |
| `search/phone` `linkedin_url` | `linkedin` | |
| `search/company` `domain` | `domain` | |
| `search/company` `linkedin_url`, `name` | No equivalent | Get the domain for a company name with [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions). |
| `verify` `email` | `email` | |
## Response field mapping
Tomba wraps every result in `data`.
| Findymail | Tomba | Notes |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `contact.email` | `data.email` | `null` when no address is found. |
| `contact.name` | `data.full_name` | `data.first_name` and `data.last_name` are also returned. |
| `contact.domain` | `data.website_url` | |
| `contacts[]` (`search/domain`) | `data.emails[]` | Each item has `email`, `full_name`, `position`, `department`, and `verification`. |
| Employees `name`, `jobTitle`, `linkedinUrl` | `data.emails[].full_name`, `position`, `linkedin` | |
| Employees `companyName`, `companyWebsite` | `data.organization.organization`, `data.organization.website_url` | |
| Reverse email `linkedin_url` | `data.linkedin` | |
| Reverse email profile `fullName`, `jobTitle`, `companyName`, `companyWebsite`, `country` | `data.full_name`, `data.position`, `data.company`, `data.website_url`, `data.country` | `data.country` is a two-letter country code. |
| Reverse email profile `headline`, `summary`, `skills`, `jobs`, `educations`, `certificates`, `city`, `region` | No equivalent | |
| Phone `phone` | `data.e164_format` | Tomba also returns national and international formats. See [Phone](/attributes/phone). |
| Phone `line_type` | `data.line_type` | Tomba uses its own line type values. |
| Company `name`, `domain`, `description` | `data.name`, `data.domain`, `data.description` | |
| Company `company_size` | `data.metrics.employees` | A range, such as `51-200`. |
| Company `linkedin_url` | `data.linkedin.handle` | The handle only, not a URL. |
| Company `industry` | No equivalent | [`GET /v1/domain-search`](/api/finder#domain-search) returns it in `data.organization.industries`. |
| Verify `email` | `data.email.email` | |
| Verify `verified` | `data.email.status` | See [Status mapping](#status-mapping). |
| Verify `provider` | `data.email.smtp_provider` | |
| Credits `credits`, `verifier_credits` | `GET /v1/me` | See [Check usage](/usage-and-quotas#check-usage). |
The email finder returns its best match with a verification status in `data.verification.status`, including catch-all (`accept_all`) and unconfirmed (`unknown`) matches. If your code expects only verified addresses, keep results whose status is `valid`.
## Status mapping
Findymail's verifier returns a boolean. Tomba returns a detailed `status` and a `result` verdict:
| Findymail `verified` | Tomba `status` | Tomba `result` |
| -------------------- | ------------------------- | --------------- |
| `true` | `valid` | `deliverable` |
| `false` | `invalid` | `undeliverable` |
| `false` | `accept_all` or `unknown` | `risky` |
| `false` | `disposable` | Empty string |
Compare `status` case-insensitively. Each value is defined in [Status values](/attributes/verifier#status-values).
## Error mapping
| Findymail | Tomba | What to do |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `401` `Unauthenticated.` | `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` `Not enough credits` | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `423` `Subscription is paused` | `401 authentication_failed` | The message says the subscription has expired. Renew it. |
| `422` invalid data | `422 params_invalid` | Fix the parameter named in the message. |
| `404` company not found | `200` with an `errors` body, `type` `not_found` | Check the body for `errors` on `/v1/companies/find`. |
| `429` `Too Many Attempts.` | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| No equivalent | `400 unknown_record` | The email finder received a disposable or webmail domain. |
| No equivalent | `451 claimed_email` | The owner of the address asked Tomba to stop processing it. Remove it from your data. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
A Tomba lookup always returns its result in the response. With `webhook_url` it also posts a copy, but the request doesn't move to the background as it does on Findymail.
Findymail has no bulk endpoint. To process a list with Tomba, replace per-contact loops with a bulk job: `finder` for names and companies, `linkedin` for profile URLs, `enrich` for email addresses, `search` for domains, and `verifier` for verification. Email finder jobs take a CSV file:
```bash
curl -X POST "https://api.tomba.io/v1/bulk/finder" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
-F "name=Conference leads" \
-F "file=@leads.csv;type=text/csv" \
-F "full_name_field_index=0" \
-F "domain_field_index=1"
```
Tomba treats the first row of the file as a header. Launch the job, poll its progress, and download the results as shown in [Lifecycle](/bulks#lifecycle). Input formats per type are in [Bulk types](/bulks#bulk-types), and a job is charged when you first download its results; see [Billing](/bulks#billing).
## Code example
This function replaces a call to `POST /api/search/name` and returns Findymail's `contact` shape, or `null` when no address is found. It needs Node.js 18 or later.
```js title="find-email.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
// Findymail's `domain` field accepts a domain or a company name.
// Tomba takes a domain as `domain` and a company name as `company`.
async function findFromName(name, domainOrCompany) {
const url = new URL("https://api.tomba.io/v1/email-finder");
url.searchParams.set("full_name", name);
url.searchParams.set(
domainOrCompany.includes(".") ? "domain" : "company",
domainOrCompany,
);
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 { email, full_name, website_url, verification } = body.data;
if (!email) return null;
return {
contact: { name: full_name, domain: website_url, email },
status: verification?.status ?? null,
};
}
console.log(await findFromName("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 a bearer token.
2. Change each `POST` with a JSON body into a `GET` with query parameters, and send Findymail's `domain` value as `domain` or `company`.
3. Read results from `data` instead of `contact`, treat `data.email: null` as no match, and filter on `data.verification.status` if you need verified addresses only.
4. Handle `402` as before, retry `429` after `Retry-After`, skip `400 unknown_record` domains, and remove addresses that return `451`.
5. Move large lists from per-contact loops to bulk jobs, and set `webhook_url` or poll `/progress` to know when they end.
6. Run the same sample of contacts through both services and compare the results before you switch production traffic.
---
## Document: Migrate from Apollo.io
Map Apollo.io People Enrichment and Bulk People Enrichment API calls to the Tomba Email Finder, LinkedIn Finder, Person API, and bulk jobs.
URL: /migration/email-finder/apollo
# 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.
---
## Document: Migrate from the Clearbit Person API
Replace Clearbit Person API lookups with Tomba's GET /v1/people/find: authentication, parameters, response fields, and errors.
URL: /migration/clearbit/person-api
# Migrate from the Clearbit Person API
This guide maps Clearbit's Person API (`GET https://person.clearbit.com/v2/people/find`) to Tomba's [`GET /v1/people/find`](/api/enrichment#person-api), which returns a Clearbit-style person object. Clearbit behavior checked on 2026-09-29 against the [last public Clearbit API reference](https://web.archive.org/web/20240909071651/https://dashboard.clearbit.com/docs) (September 2024).
## Before you begin
- Get your API key (`ta_…`) and secret (`ts_…`); see [Authentication](/authentication).
- Check what a lookup costs in [Credit costs](/usage-and-quotas#credit-costs) and your plan's throttling in [Limits by plan](/rate-limits#limits-by-plan).
- Read [Migrate from Clearbit](/migration/clearbit/introduction) for the changes shared by every Clearbit endpoint.
## Endpoint mapping
| Clearbit | Tomba |
| -------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `GET https://person.clearbit.com/v2/people/find?email=` | `GET https://api.tomba.io/v1/people/find?email=` |
| `GET https://person-stream.clearbit.com/v2/people/find?email=` | `GET https://api.tomba.io/v1/people/find?email=` |
| `POST https://person.clearbit.com/v1/people/{id}/flag` | [`POST /v1/flag`](/api/flag#flag-incorrect-data) with `flag_type` `email` |
## Authentication changes
Clearbit takes the key as the basic auth username or as a bearer token. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Clearbit
curl "https://person.clearbit.com/v2/people/find?email=jane.doe@stripe.com" \
-H "Authorization: Bearer $CLEARBIT_API_KEY"
# Tomba
curl "https://api.tomba.io/v1/people/find?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
| Clearbit | Tomba | Notes |
| --------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email` | `email` | Required. |
| `webhook_url` | `webhook_url` | Tomba still returns the person in the response, and also posts a copy when a person is found. See [Per-request callbacks](/webhook#per-request-callbacks). |
| `webhook_id` | No equivalent | |
| `given_name`, `family_name`, `ip_address`, `location`, `company`, `company_domain`, `linkedin`, `twitter`, `facebook` | No equivalent | Tomba matches on the email address only. |
| `subscribe`, `suppression` | No equivalent | |
| `API-Version` header | No equivalent | |
## Response field mapping
Tomba returns the person in `data` and the address you looked up in `meta.email`. Every field in Clearbit's reference is listed below; "Always `null`" means Tomba returns the key but never fills it. Field definitions are in [Enrichment](/attributes/enrichment).
| Clearbit | Tomba | Notes |
| ---------------------------------------------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | No equivalent | |
| `name.fullName`, `givenName`, `familyName` | Same paths in `data` | |
| `email`, `indexedAt` | Same paths | |
| `location` | `data.location` | The two-letter country code, not the city, state, and country. |
| `geo.country`, `geo.countryCode` | Same paths | |
| `geo.city`, `state`, `stateCode`, `lat`, `lng` | Same paths | Always `null`. |
| `timeZone`, `utcOffset`, `bio`, `site`, `avatar` | Same paths | Always `null`. |
| `employment.domain`, `name`, `title` | Same paths | |
| `employment.role` | `data.employment.role` | Tomba's department names, which differ from Clearbit's roles: for example `hr`, not `human_resources`. |
| `employment.subRole`, `seniority` | Same paths | Always `null`. |
| `linkedin.handle` | `data.linkedin.handle` | A full profile URL, not Clearbit's `in/…` path. |
| `twitter.handle` | `data.twitter.handle` | A full profile URL, not a username. |
| `twitter.id`, `bio`, `followers`, `following`, `statuses`, `favorites`, `location`, `site`, `avatar` | Same paths | Always `null`. |
| `facebook.handle`, `github.*`, `gravatar.*` | Same paths | Always `null`. |
| `googleplus.handle` | No equivalent | |
| `fuzzy`, `activeAt`, `inactiveAt` | Same paths | Always `null`. |
| `emailProvider` | `data.emailProvider` | Always `null`. Tomba doesn't look up webmail addresses; see [Error mapping](#error-mapping). |
| `phone` | `data.phone` | A boolean: `true` when Tomba has a phone number for the person. Get the number from [`GET /v1/phone-finder`](/api/phone#phone-finder). |
Tomba also returns `data.gender` and `data.verification`.
## Status mapping
Clearbit's Person API has no deliverability status. Tomba adds `data.verification`, with the `date` and `status` of the address's last verification. The status uses the verifier's values, described in [Status values](/attributes/verifier#status-values), and is `null` when the address hasn't been verified. Compare it case-insensitively.
## Error mapping
| Clearbit | Tomba | What to do |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `202` lookup queued | Not returned | Remove the retry-on-`202` logic. |
| `404` person not found | `200` with an `errors` object: `type` `not_found`, `code` 404 | Check the body for `errors`, not only the status. Tomba also returns this when it knows the address but not the person's name. |
| `422` validation error | `422 params_invalid` for a malformed address. A disposable or webmail address returns `200` with an `errors` object: `type` `params_invalid`, `code` 422. | Send a work address. |
| `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. |
| `402` over quota | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `429` rate limit | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| No equivalent | `451 claimed_email` | The owner of the address asked Tomba to stop processing it. Remove it from your data. |
| `50X` | `500 api_error` | Retry with backoff. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
Tomba answers every person lookup synchronously, so remove `202` retries. A `webhook_url` callback carries the same JSON as the response, without Clearbit's `id`, `type`, and `status` wrapper, and isn't signed.
To enrich a list of addresses, run a bulk job of type `enrich` with a newline-separated `list`:
```bash
curl -X POST "https://api.tomba.io/v1/bulk/enrich" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Sign-ups", "list": "jane.doe@stripe.com\njohn.smith@shopify.com"}'
```
Launch the job, poll its progress, and download the results as CSV, as shown in [Lifecycle](/bulks#lifecycle). The CSV has Tomba's email enrichment columns, not Clearbit's JSON shape. A job is charged when you first download its results; see [Billing](/bulks#billing).
## Code example
This function replaces a Person API lookup. It returns the Clearbit-style person object, or `null` when Tomba has no record. It needs Node.js 18 or later.
```js title="find-person.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
async function findPerson(email) {
const url = new URL("https://api.tomba.io/v1/people/find");
url.searchParams.set("email", email);
const res = await fetch(url, { headers: TOMBA_HEADERS });
const body = await res.json();
if (body.errors) {
// Unknown people and disposable or webmail addresses come back as HTTP 200 with an errors object.
if (res.ok) return null;
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
return body.data;
}
const person = await findPerson("jane.doe@stripe.com");
if (person) {
console.log(
person.name.fullName,
person.employment.title,
person.employment.name,
);
console.log(
"Verification status:",
person.verification?.status ?? "not verified",
);
}
```
## 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 auth or a bearer token.
2. Call `https://api.tomba.io/v1/people/find` with the email address only, and read the person from `data`.
3. Treat a `200` response with an `errors` object as not found, and remove `202` retries.
4. Update code that depends on fields Tomba formats differently (`location`, `employment.role`, `linkedin.handle`, `twitter.handle`, `phone`) or leaves `null`.
5. Handle `402`, retry `429` after `Retry-After`, and remove addresses that return `451`.
6. Look up the same sample of addresses with both services and compare the fields you use before you switch production traffic.
---
## Document: Migrate from Clearbit
Map Clearbit Person, Company, Combined, Logo, Autocomplete, and Name to Domain API calls to Tomba, and the changes every Clearbit migration shares.
URL: /migration/clearbit/introduction
# Migrate from Clearbit
Tomba's [Person](/api/enrichment#person-api), [Company](/api/enrichment#company-api), and [Combined](/api/enrichment#combined-api) endpoints return Clearbit-style objects, so most migrations change the host, the authentication, and how you read the response envelope. Clearbit's API reference is no longer public; Clearbit behavior on these pages was checked on 2026-09-29 against its [last public version](https://web.archive.org/web/20240909071651/https://dashboard.clearbit.com/docs) (September 2024).
## Endpoint mapping
| Clearbit | Tomba | Guide or notes |
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET https://person.clearbit.com/v2/people/find` | `GET https://api.tomba.io/v1/people/find` | [Person API](/migration/clearbit/person-api) |
| `GET https://company.clearbit.com/v2/companies/find` | `GET https://api.tomba.io/v1/companies/find` | [Company API](/migration/clearbit/company-api) |
| `GET https://person.clearbit.com/v2/combined/find` | `GET https://api.tomba.io/v1/combined/find` | [Combined API](/migration/clearbit/combined-api) |
| Streaming hosts `person-stream.clearbit.com` and `company-stream.clearbit.com` | The same Tomba endpoints | Every Tomba response is final. |
| `GET https://logo.clearbit.com/{domain}` | `https://logo.tomba.io/{domain}` | No authentication. See [Logo API](/logo-api). Clearbit's Logo API was shut down in December 2025 ([HubSpot announcement](https://developers.hubspot.com/changelog/upcoming-sunset-of-clearbits-free-logo-api)). |
| `GET https://autocomplete.clearbit.com/v1/companies/suggest` | [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) | Needs your key and secret, so call it from your server, not from a browser. |
| `GET https://company.clearbit.com/v1/domains/find` (Name to Domain) | [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions) | Returns a list of matches with `name`, `domain`, and `email_count`. |
| `POST https://person.clearbit.com/v1/people/{id}/flag`, `POST https://company.clearbit.com/v2/companies/flag` | [`POST /v1/flag`](/api/flag#flag-incorrect-data) | Set `flag_type` to `email` or `organization`. |
| `GET https://discovery.clearbit.com/v1/companies/search` | [`POST /v1/reveal/search`](/api/reveal#search-companies) | Uses Tomba's own filters. |
| `GET https://prospector.clearbit.com/v2/people/search` | [`GET /v1/domain-search`](/api/finder#domain-search) | Returns people and their addresses for one domain. |
| Reveal (`reveal.clearbit.com`, IP address to company) | No equivalent | Tomba's `/v1/reveal/search` searches companies by filters; it doesn't resolve IP addresses. |
| Risk (`risk.clearbit.com`) | No equivalent | |
## What changes for every endpoint
| Area | Clearbit | Tomba |
| --------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Authentication | HTTP basic auth with the key as the username, or `Authorization: Bearer` | `X-Tomba-Key` and `X-Tomba-Secret` headers. See [Authentication](/authentication). |
| Response envelope | The object at the top level | The object in `data`, and the input in `meta` (`meta.email` or `meta.domain`) |
| Not found | `404` | `200` with an `errors` object: `type` `not_found`, `code` 404 |
| Disposable or webmail input | — | `200` with an `errors` object: `type` `params_invalid`, `code` 422 |
| Queued lookups | An empty `202`: retry later or wait for the webhook | Never returned. Each request waits for its result; set your client timeout to at least 180 seconds. |
| Webhooks | `webhook_url` or an account-wide webhook, with a signed `{id, type, status, body}` payload | `webhook_url` on the Person and Combined endpoints only. Tomba still returns the result, and posts an unsigned copy of the response body. See [Per-request callbacks](/webhook#per-request-callbacks). |
| Error body | `{"error": {"type", "message"}}` | `{"errors": {"type", "message", "code"}}`. See [Errors](/error-handling#error-types). |
| Unfilled fields | `null` when unknown | Several Clearbit fields are always `null`. Each guide lists them. |
| Versioning | `API-Version` header | The `/v1` path; there's no version header. |
| Billing | — | See [Credit costs](/usage-and-quotas#credit-costs). Repeating a lookup within your usage window is free; see [Duplicate requests](/usage-and-quotas#duplicate-requests). |
---
## Document: Migrate from the Clearbit Company API
Replace Clearbit Company API lookups with Tomba's GET /v1/companies/find: authentication, parameters, response fields, and errors.
URL: /migration/clearbit/company-api
# Migrate from the Clearbit Company API
This guide maps Clearbit's Company API (`GET https://company.clearbit.com/v2/companies/find`) to Tomba's [`GET /v1/companies/find`](/api/enrichment#company-api), which returns a Clearbit-style company object. Clearbit behavior checked on 2026-09-29 against the [last public Clearbit API reference](https://web.archive.org/web/20240909071651/https://dashboard.clearbit.com/docs) (September 2024).
## Before you begin
- Get your API key (`ta_…`) and secret (`ts_…`); see [Authentication](/authentication).
- Check what a lookup costs in [Credit costs](/usage-and-quotas#credit-costs) and your plan's throttling in [Limits by plan](/rate-limits#limits-by-plan).
- Read [Migrate from Clearbit](/migration/clearbit/introduction) for the changes shared by every Clearbit endpoint.
## Endpoint mapping
| Clearbit | Tomba |
| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `GET https://company.clearbit.com/v2/companies/find?domain=` | `GET https://api.tomba.io/v1/companies/find?domain=` |
| `GET https://company-stream.clearbit.com/v1/companies/domain/{domain}` | `GET https://api.tomba.io/v1/companies/find?domain={domain}` |
| `POST https://company.clearbit.com/v2/companies/flag?domain=` | [`POST /v1/flag`](/api/flag#flag-incorrect-data) with `flag_type` `organization` |
## Authentication changes
Clearbit takes the key as the basic auth username or as a bearer token. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Clearbit
curl "https://company.clearbit.com/v2/companies/find?domain=stripe.com" \
-u "$CLEARBIT_API_KEY:"
# Tomba
curl "https://api.tomba.io/v1/companies/find?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
| Clearbit | Tomba | Notes |
| ------------------------------------------------- | ------------- | -------------------------------------------- |
| `domain` | `domain` | Required. |
| `webhook_url`, `webhook_id` | No equivalent | `/v1/companies/find` doesn't send callbacks. |
| `company_name`, `linkedin`, `twitter`, `facebook` | No equivalent | Tomba looks up by domain only. |
| `API-Version` header | No equivalent | |
## Response field mapping
Tomba returns the company in `data` and the domain you looked up in `meta.domain`. Every field in Clearbit's reference is listed below; "Always `null`" means Tomba returns the key but never fills it. Field definitions are in [Enrichment](/attributes/enrichment).
| Clearbit | Tomba | Notes |
| -------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | No equivalent | |
| `name`, `domain`, `description`, `tags`, `indexedAt` | Same paths in `data` | |
| `legalName` | `data.legalName` | The same value as `name`. |
| `domainAliases` | `data.domainAliases` | Always `null`. |
| `site.phoneNumbers`, `site.emailAddresses` | Same paths | `site.emailAddresses` lists up to six generic addresses Tomba found for the domain, such as `info@`. |
| `category.sector`, `industryGroup`, `industry`, `subIndustry` | Same paths | Always `null`. [`GET /v1/domain-search`](/api/finder#domain-search) returns the industry in `data.organization.industries`. |
| `category.gicsCode` | No equivalent | |
| `category.sicCode`, `sic4Codes`, `naicsCode`, `naics6Codes`, `naics6Codes2022` | Same paths | |
| `foundedYear` | `data.foundedYear` | A string, such as `"2010"`, instead of an integer. |
| `location` | `data.location` | The two-letter country code, not the full address. |
| `timeZone`, `utcOffset` | Same paths | Always `null`. |
| `geo.streetAddress`, `city`, `postalCode`, `state`, `country`, `countryCode` | Same paths | |
| `geo.streetNumber`, `geo.streetName` | Same paths | Both hold the full street address, the same value as `geo.streetAddress`. |
| `geo.stateCode` | `data.geo.stateCode` | Holds the same value as `geo.state`. |
| `geo.subPremise`, `lat`, `lng` | Same paths | Always `null`. |
| `logo` | `data.logo` | Always `null`. Use `https://logo.tomba.io/{domain}`; see [Logo API](/logo-api). |
| `facebook.handle`, `twitter.handle` | Same paths | |
| `facebook.likes` | `data.facebook.likes` | Always `null`. |
| `linkedin.handle` | `data.linkedin.handle` | Without Clearbit's `company/` prefix. |
| `twitter.id`, `bio`, `followers`, `following`, `location`, `site`, `avatar` | Same paths | Always `null`. |
| `crunchbase.handle` | No equivalent | |
| `emailProvider` | `data.emailProvider` | The name of the domain's mail provider, such as `Google`, instead of a boolean. |
| `type` | `data.type` | Tomba's company types differ from Clearbit's; see [Company attributes](/attributes/company). |
| `ticker`, `identifiers.usEIN` | Same paths | Always `null`. |
| `phone` | No equivalent | Use `data.site.phoneNumbers`. |
| `metrics.employeesRange` | `data.metrics.employees` | A range string, such as `1001-5000`. Tomba's ranges differ from Clearbit's. |
| `metrics.employees` | No equivalent | Tomba's `metrics.employees` holds a range, not a count. |
| `metrics.annualRevenue`, `metrics.estimatedAnnualRevenue` | Same paths | Both hold the same string. |
| `metrics.trafficRank` | `data.metrics.trafficRank` | A numeric rank as a string, instead of Clearbit's `very_high` to `very_low` values. |
| `metrics.alexaUsRank`, `alexaGlobalRank`, `marketCap`, `raised`, `fiscalYearEnd` | Same paths | Always `null`. |
| `tech`, `techCategories` | Same paths | Technology and category names. |
| `parent.domain`, `ultimateParent.domain` | Same paths | Always `null`. |
Tomba also returns `data.whois`, with the domain's `registrar_name`, `created_date`, and `referral_url`.
## Error mapping
| Clearbit | Tomba | What to do |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `202` lookup queued | Not returned | Remove the retry-on-`202` logic. |
| `404` company not found | `200` with an `errors` object: `type` `not_found`, `code` 404 | Check the body for `errors`, not only the status. |
| `422` invalid domain | `422 params_invalid` for a malformed domain. A disposable or webmail domain returns `200` with an `errors` object: `type` `params_invalid`, `code` 422. | Send the company's own domain. |
| `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. |
| `402` over quota | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `429` rate limit | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| `50X` | `500 api_error` | Retry with backoff. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
Tomba answers every company lookup synchronously and sends no callback for it, so remove `202` retries and webhook handlers. To enrich a list of domains, run a bulk job of type `company` with a newline-separated `list`:
```bash
curl -X POST "https://api.tomba.io/v1/bulk/company" \
-H "X-Tomba-Key: $TOMBA_API_KEY" \
-H "X-Tomba-Secret: $TOMBA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Target accounts", "list": "stripe.com\nshopify.com"}'
```
Launch the job, poll its progress, and download the results as CSV, as shown in [Lifecycle](/bulks#lifecycle). The CSV doesn't follow Clearbit's JSON shape. A job is charged when you first download its results; see [Billing](/bulks#billing).
## Code example
This function replaces a Company API lookup. It returns the Clearbit-style company object, or `null` when Tomba has no record, and fills `logo` with Tomba's logo URL. It needs Node.js 18 or later.
```js title="find-company.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
async function findCompany(domain) {
const url = new URL("https://api.tomba.io/v1/companies/find");
url.searchParams.set("domain", domain);
const res = await fetch(url, { headers: TOMBA_HEADERS });
const body = await res.json();
if (body.errors) {
// Unknown, disposable, and webmail domains come back as HTTP 200 with an errors object.
if (res.ok) return null;
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const company = body.data;
company.logo = `https://logo.tomba.io/${company.domain}`;
return company;
}
const company = await findCompany("stripe.com");
console.log(company?.name, company?.metrics.employees, company?.logo);
```
## 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 auth or a bearer token.
2. Call `https://api.tomba.io/v1/companies/find` and read the company from `data`.
3. Treat a `200` response with an `errors` object as not found, and remove `202` retries and webhook handling.
4. Update code that depends on fields Tomba formats differently (`foundedYear`, `location`, `emailProvider`, `type`, `metrics`, `linkedin.handle`) or leaves `null`, and build logo URLs on `logo.tomba.io`.
5. Handle `402`, and retry `429` after `Retry-After`.
6. Look up the same sample of domains with both services and compare the fields you use before you switch production traffic.
---
## Document: Migrate from the Clearbit Combined API
Replace Clearbit Combined API lookups with Tomba's GET /v1/combined/find: authentication, parameters, response shape, and errors.
URL: /migration/clearbit/combined-api
# Migrate from the Clearbit Combined API
This guide maps Clearbit's Combined API (`GET https://person.clearbit.com/v2/combined/find`) to Tomba's [`GET /v1/combined/find`](/api/enrichment#combined-api), which returns a Clearbit-style person and company for an email address. Clearbit behavior checked on 2026-09-29 against the [last public Clearbit API reference](https://web.archive.org/web/20240909071651/https://dashboard.clearbit.com/docs) (September 2024).
## Before you begin
- Get your API key (`ta_…`) and secret (`ts_…`); see [Authentication](/authentication).
- Check what a lookup costs in [Credit costs](/usage-and-quotas#credit-costs) and your plan's throttling in [Limits by plan](/rate-limits#limits-by-plan).
- Read [Migrate from Clearbit](/migration/clearbit/introduction) for the changes shared by every Clearbit endpoint.
## Endpoint mapping
| Clearbit | Tomba |
| ---------------------------------------------------------------- | -------------------------------------------------- |
| `GET https://person.clearbit.com/v2/combined/find?email=` | `GET https://api.tomba.io/v1/combined/find?email=` |
| `GET https://person-stream.clearbit.com/v2/combined/find?email=` | `GET https://api.tomba.io/v1/combined/find?email=` |
## Authentication changes
Clearbit takes the key as the basic auth username or as a bearer token. Tomba needs two headers, `X-Tomba-Key` and `X-Tomba-Secret`:
```bash
# Clearbit
curl "https://person.clearbit.com/v2/combined/find?email=jane.doe@stripe.com" \
-H "Authorization: Bearer $CLEARBIT_API_KEY"
# Tomba
curl "https://api.tomba.io/v1/combined/find?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
| Clearbit | Tomba | Notes |
| --------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email` | `email` | Required. |
| `webhook_url` | `webhook_url` | Tomba still returns the result in the response, and also posts a copy when a person is found. See [Per-request callbacks](/webhook#per-request-callbacks). |
| `webhook_id` | No equivalent | |
| `given_name`, `family_name`, `ip_address`, `location`, `company`, `company_domain`, `linkedin`, `twitter`, `facebook` | No equivalent | Tomba matches on the email address only. |
| `subscribe`, `suppression` | No equivalent | |
| `API-Version` header | No equivalent | |
## Response field mapping
| Clearbit | Tomba | Notes |
| ------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `person` | `data.person` | Always an object. When Tomba has no person for the address, the whole response is `not_found`; see [Error mapping](#error-mapping). |
| `company` | `data.company` | When Tomba has no company for the person's domain, an object whose fields are all `null`, not `null` itself. Check `data.company.domain`. |
| No equivalent | `meta.email` | The address you looked up. An empty string when no company was found. |
Inside `data.person` and `data.company`, fields map as described in the Person and Company guides:
- [Person API response fields](/migration/clearbit/person-api#response-field-mapping)
- [Company API response fields](/migration/clearbit/company-api#response-field-mapping)
## Status mapping
Clearbit's Combined API has no deliverability status. Tomba adds `data.person.verification`, with the `date` and `status` of the address's last verification. The status uses the verifier's values, described in [Status values](/attributes/verifier#status-values), and is `null` when the address hasn't been verified. Compare it case-insensitively.
## Error mapping
| Clearbit | Tomba | What to do |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `202` lookup queued | Not returned | Remove the retry-on-`202` logic. |
| `200` with only `company` | `200` with an `errors` object: `type` `not_found`, `code` 404 | Tomba needs the person. To still get the company, call [`GET /v1/companies/find`](/api/enrichment#company-api) with the address's domain. |
| `404` neither found | `200` with an `errors` object: `type` `not_found`, `code` 404 | Check the body for `errors`, not only the status. |
| `422` validation error | `422 params_invalid` for a malformed address. A disposable or webmail address returns `200` with an `errors` object: `type` `params_invalid`, `code` 422. | Send a work address. |
| `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. |
| `402` over quota | `402 quota_exceeded` | Wait for your usage window to renew or add credits. |
| `429` rate limit | `429 rate_limit` | Retry after the `Retry-After` delay; see [Handle 429 responses](/rate-limits#handle-429-responses). |
| No equivalent | `451 claimed_email` | The owner of the address asked Tomba to stop processing it. Remove it from your data. |
| `50X` | `500 api_error` | Retry with backoff. |
Error responses don't consume credits. Every type is listed in [Errors](/error-handling#error-types).
## Bulk and async jobs
Tomba answers every combined lookup synchronously, so remove `202` retries. A `webhook_url` callback carries the same JSON as the response, without Clearbit's `id`, `type`, and `status` wrapper, and isn't signed.
There is no combined bulk type. For a list, run an `enrich` job for the addresses and a `company` job for their domains; see [Email enrichment](/bulks#email-enrichment) and [Company enrichment](/bulks#company-enrichment). Each job is created, launched, polled, and downloaded as CSV; see [Lifecycle](/bulks#lifecycle).
## Code example
This function replaces a Combined API lookup and returns Clearbit's `{person, company}` shape, with `company` set to `null` when Tomba has no company. It needs Node.js 18 or later.
```js title="find-combined.mjs"
const TOMBA_HEADERS = {
"X-Tomba-Key": process.env.TOMBA_API_KEY,
"X-Tomba-Secret": process.env.TOMBA_SECRET_KEY,
};
async function findCombined(email) {
const url = new URL("https://api.tomba.io/v1/combined/find");
url.searchParams.set("email", email);
const res = await fetch(url, { headers: TOMBA_HEADERS });
const body = await res.json();
if (body.errors) {
// No person found, or a disposable or webmail address: HTTP 200 with an errors object.
if (res.ok) return { person: null, company: null };
throw new Error(
`${res.status} ${body.errors.type}: ${body.errors.message}`,
);
}
const { person, company } = body.data;
// Tomba returns an object with null fields when it has no company.
return { person, company: company.domain ? company : null };
}
const { person, company } = await findCombined("jane.doe@stripe.com");
console.log(person?.name.fullName, company?.name ?? "no company");
```
## 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 auth or a bearer token.
2. Call `https://api.tomba.io/v1/combined/find` with the email address only, and read `data.person` and `data.company`.
3. Treat a `200` response with an `errors` object as not found, and check `data.company.domain` instead of testing `company` for `null`.
4. Where you relied on company-only results, add a fallback call to `/v1/companies/find`.
5. Apply the person and company field changes from the [Person](/migration/clearbit/person-api) and [Company](/migration/clearbit/company-api) guides, and remove `202` retries.
6. Handle `402`, retry `429` after `Retry-After`, and remove addresses that return `451`.
7. Look up the same sample of addresses with both services and compare the fields you use before you switch production traffic.
---
## Document: Similar Domains
Companies similar to a domain (one row per similar company). Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/similar-domains
# Similar Domains
Companies similar to a domain (one row per similar company). Grow a target account list with companies similar to your best customers (lookalikes).
**Credits:** 1 search credit when found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `domain` (Company domain)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.similar_finder('acme.com');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"SIMILAR_DOMAIN": "initech.com",
"NAME": "Initech",
"INDUSTRIES": "Software Development"
},
{
"SIMILAR_DOMAIN": "globex.com",
"NAME": "Globex",
"INDUSTRIES": "Software Development"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.best_customers (WEBSITE STRING);
INSERT INTO my_db.public.best_customers VALUES
('acme.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.best_customers TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.find_similar_domains('MY_DB.PUBLIC.BEST_CUSTOMERS', 'WEBSITE', 'BEST_CUSTOMERS_SIMILAR_FINDER', 100);
SELECT INPUT_DOMAIN, SIMILAR_DOMAIN, NAME, INDUSTRIES
FROM TOMBA.results.BEST_CUSTOMERS_SIMILAR_FINDER
WHERE lookup_status = 'found';
```
Signature: `find_similar_domains(input_table STRING, domain_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#similar-domains).
## Tips
- The output has one row per similar company.
- Chain it: enrich the lookalikes with Company Enrichment, then find people with Domain Search.
## Related
- [Company Enrichment](/snowflake/tools/company-enrichment)
- [Companies Search](/snowflake/tools/companies-search)
- [Domain Search](/snowflake/tools/domain-search)
---
## Document: Phone Validator
Validate and format a phone number (line type, carrier, region). Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/phone-validator
# Phone Validator
Validate and format a phone number (line type, carrier, region). Check and normalize the phone numbers you already have: valid or not, line type, carrier, E.164 format.
**Credits:** 1 search credit per validation. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `phone` (Phone number)
Optional: `country_code` (Country code (ISO-2, optional)).
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.phone_validator('+1 415 555 0100', 'US');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"VALID": true,
"E164_FORMAT": "+14155550100",
"LINE_TYPE": "fixed_line",
"REGION": "California"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.contacts (PHONE STRING, COUNTRY STRING);
INSERT INTO my_db.public.contacts VALUES
('+1 415 555 0100', 'US'),
('030 1234567', 'DE');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.contacts TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.validate_phones('MY_DB.PUBLIC.CONTACTS', 'PHONE', 'CONTACTS_PHONE_VALIDATOR', 100, 'COUNTRY');
SELECT INPUT_PHONE, VALID, E164_FORMAT, INTL_FORMAT, LINE_TYPE, CARRIER, REGION
FROM TOMBA.results.CONTACTS_PHONE_VALIDATOR
WHERE lookup_status = 'found';
```
Signature: `validate_phones(input_table STRING, phone_column STRING, output_table STRING, max_rows INT DEFAULT 100000, country_code_column STRING DEFAULT '')`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#phone-validator).
## Tips
- The country column is optional but helps with local-format numbers (`030 1234567`). It takes a **column name**, not a value like `'US'`.
- Store `E164_FORMAT` in your CRM: it is the standard international format.
## Related
- [Phone Finder](/snowflake/tools/phone-finder)
---
## Document: Phone Finder
Find a phone number from an email, a company domain or a LinkedIn URL. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/phone-finder
# Phone Finder
Find a phone number from an email, a company domain or a LinkedIn URL. Add direct phone numbers to contacts for calling campaigns.
**Credits:** 5 search credits when a valid phone is found (1 when searching by domain). Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Any one of these input combinations:
- `email` (Email address)
- `domain` (Company domain)
- `linkedin` (LinkedIn profile URL)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.phone_finder('jane.doe@acme.com');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 5,
"results": [
{
"VALID": true,
"E164_FORMAT": "+14155550100",
"LINE_TYPE": "mobile",
"CARRIER": "Example Mobile",
"REGION": "California"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.contacts (EMAIL STRING);
INSERT INTO my_db.public.contacts VALUES
('jane.doe@acme.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.contacts TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.find_phones('MY_DB.PUBLIC.CONTACTS', 'EMAIL', 'CONTACTS_PHONE_FINDER', 100);
SELECT INPUT_EMAIL, VALID, E164_FORMAT, INTL_FORMAT, LINE_TYPE, CARRIER, COUNTRY_CODE
FROM TOMBA.results.CONTACTS_PHONE_FINDER
WHERE lookup_status = 'found';
```
Signature: `find_phones(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
### Other inputs with `run_tool`
Company phone numbers from domains (1 credit each), for a table that has a `WEBSITE` column:
```sql
CALL TOMBA.core.run_tool('phone_finder', 'MY_DB.PUBLIC.ACCOUNTS', {'domain': 'WEBSITE'},
'ACCOUNTS_PHONE_FINDER', 100, {});
```
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#phone-finder).
## Tips
- 5 credits when a valid phone is found by email or LinkedIn URL; 1 credit when searching by company domain.
- The table procedure takes an email column. For domains or LinkedIn URLs, use `run_tool` with `{'domain': ...}` or `{'linkedin': ...}`.
## Related
- [Phone Validator](/snowflake/tools/phone-validator)
- [Email Finder](/snowflake/tools/email-finder)
---
## Document: Person Enrichment
Full person profile (employment, location, social handles) for an email. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/person-enrichment
# Person Enrichment
Full person profile (employment, location, social handles) for an email. Qualify leads by role, or route them by country.
**Credits:** 1 search credit when found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `email` (Email address)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.person_enrichment('jane.doe@acme.com');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"FULL_NAME": "Jane Doe",
"TITLE": "Head of Sales",
"ROLE": "sales",
"COMPANY_NAME": "Acme",
"COMPANY_DOMAIN": "acme.com",
"COUNTRY": "Germany"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.signups (EMAIL STRING);
INSERT INTO my_db.public.signups VALUES
('jane.doe@acme.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.signups TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.enrich_people('MY_DB.PUBLIC.SIGNUPS', 'EMAIL', 'SIGNUPS_PERSON_ENRICHMENT', 100);
SELECT INPUT_EMAIL, FULL_NAME, TITLE, ROLE, COMPANY_NAME, COMPANY_DOMAIN, COUNTRY
FROM TOMBA.results.SIGNUPS_PERSON_ENRICHMENT
WHERE lookup_status = 'found';
```
Signature: `enrich_people(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#person-enrichment).
## Tips
- `ROLE` is the person's department, such as `sales` or `engineering`: use it to segment leads.
- Need the company's firmographics too? Combined Enrichment returns both for 2 credits.
## Related
- [Email Enrichment](/snowflake/tools/email-enrichment)
- [Combined Enrichment](/snowflake/tools/combined-enrichment)
- [Company Enrichment](/snowflake/tools/company-enrichment)
---
## Document: LinkedIn Finder
Find the professional email behind a LinkedIn profile URL. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/linkedin-finder
# LinkedIn Finder
Find the professional email behind a LinkedIn profile URL. You have LinkedIn profile URLs (from Sales Navigator exports or event lists) and need the work email.
**Credits:** 1 search credit when found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `url` (LinkedIn profile URL)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.linkedin_finder('https://www.linkedin.com/in/jane-doe');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"EMAIL": "jane.doe@acme.com",
"FULL_NAME": "Jane Doe",
"POSITION": "Head of Sales",
"COMPANY": "Acme"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.prospects (LINKEDIN_URL STRING);
INSERT INTO my_db.public.prospects VALUES
('https://www.linkedin.com/in/jane-doe');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.prospects TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.find_emails_from_linkedin('MY_DB.PUBLIC.PROSPECTS', 'LINKEDIN_URL', 'PROSPECTS_LINKEDIN_FINDER', 100);
SELECT INPUT_URL, EMAIL, FULL_NAME, POSITION, COMPANY, VERIFICATION_STATUS
FROM TOMBA.results.PROSPECTS_LINKEDIN_FINDER
WHERE lookup_status = 'found';
```
Signature: `find_emails_from_linkedin(input_table STRING, url_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#linkedin-finder).
## Tips
- Use the public profile URL (`https://www.linkedin.com/in/...`).
## Related
- [Email Finder](/snowflake/tools/email-finder)
- [Phone Finder](/snowflake/tools/phone-finder)
---
## Document: Email Verifier
Check whether email addresses are deliverable with Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/email-verifier
# Email Verifier
Check whether an email address is deliverable. Clean a list before a campaign: know which addresses are deliverable, risky or invalid.
**Credits:** 1 verification credit (unknown and disposable results are free). Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `email` (Email address)
| Option | Type | Default | Description |
| --------------- | ------- | ------- | -------------------------------------------------------- |
| `enrich_mobile` | BOOLEAN | `false` | Also look up mobile phones (+5 credits per phone found). |
## Try one value
```sql
CALL TOMBA.core.email_verifier('jane.doe@acme.com');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"EMAIL": "jane.doe@acme.com",
"STATUS": "valid",
"RESULT": "deliverable",
"SCORE": 99,
"ACCEPT_ALL": false
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.contacts (EMAIL STRING);
INSERT INTO my_db.public.contacts VALUES
('jane.doe@acme.com'),
('info@globex.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.contacts TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.verify_emails('MY_DB.PUBLIC.CONTACTS', 'EMAIL', 'CONTACTS_EMAIL_VERIFIER', 100);
SELECT INPUT_EMAIL, STATUS, RESULT, SCORE, ACCEPT_ALL, DISPOSABLE, WEBMAIL
FROM TOMBA.results.CONTACTS_EMAIL_VERIFIER
WHERE lookup_status = 'found';
```
Signature: `verify_emails(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#email-verifier).
## Tips
- `STATUS` and `RESULT` hold the verifier's `status` and `result` in lowercase. What each value means:
[Email verifier response](/attributes/verifier#status-values).
- Send only `valid` addresses, and treat `accept_all` as risky: the server accepts every address, so it can't be confirmed.
## Related
- [Email Finder](/snowflake/tools/email-finder)
- [Email Enrichment](/snowflake/tools/email-enrichment)
---
## Document: Email Finder
Find a person's professional email from their name and company domain. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/email-finder
# Email Finder
Find a person's professional email from their name and company domain. You have names and companies (leads, event attendees, a CRM export) and need a professional email for each person.
**Credits:** 1 search credit when an email is found (+5 per phone with enrich_mobile). Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Any one of these input combinations:
- `domain` (Company domain) + `first_name` (First name) + `last_name` (Last name)
- `domain` (Company domain) + `full_name` (Full name)
- `company` (Company name) + `first_name` (First name) + `last_name` (Last name)
- `company` (Company name) + `full_name` (Full name)
| Option | Type | Default | Description |
| --------------- | ------- | ------- | -------------------------------------------------------- |
| `enrich_mobile` | BOOLEAN | `false` | Also look up mobile phones (+5 credits per phone found). |
## Try one value
```sql
CALL TOMBA.core.email_finder('acme.com', 'Jane', 'Doe');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"EMAIL": "jane.doe@acme.com",
"SCORE": 97,
"VERIFICATION_STATUS": "valid",
"POSITION": "Head of Sales"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.leads (COMPANY_DOMAIN STRING, FIRST_NAME STRING, LAST_NAME STRING);
INSERT INTO my_db.public.leads VALUES
('acme.com', 'Jane', 'Doe'),
('globex.com', 'John', 'Smith');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.leads TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.find_emails('MY_DB.PUBLIC.LEADS', 'COMPANY_DOMAIN', 'FIRST_NAME', 'LAST_NAME', 'LEADS_EMAIL_FINDER', 100);
SELECT INPUT_DOMAIN, INPUT_FIRST_NAME, INPUT_LAST_NAME, EMAIL, SCORE, VERIFICATION_STATUS, POSITION
FROM TOMBA.results.LEADS_EMAIL_FINDER
WHERE lookup_status = 'found';
```
Signature: `find_emails(input_table STRING, domain_column STRING, first_name_column STRING, last_name_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
### Other inputs with `run_tool`
With a company name and a full name instead of a domain and first and last names, for a table that has
`COMPANY_NAME` and `CONTACT_NAME` columns:
```sql
CALL TOMBA.core.run_tool('email_finder', 'MY_DB.PUBLIC.CONTACTS', {'company': 'COMPANY_NAME', 'full_name': 'CONTACT_NAME'},
'CONTACTS_EMAIL_FINDER', 100, {});
```
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#email-finder).
## Tips
- `SCORE` is Tomba's confidence (0–100); `VERIFICATION_STATUS` tells you whether the address was verified.
- The table procedure takes domain + first + last name. For full names or company names, use `run_tool` as shown above.
- Add `{'enrich_mobile': true}` to the `run_tool` options to also look up mobile numbers (+5 credits per phone
found). The numbers are in `DATA:phone_data`; the `PHONE_NUMBER` column only says whether Tomba has one.
## Related
- [Email Verifier](/snowflake/tools/email-verifier)
- [LinkedIn Finder](/snowflake/tools/linkedin-finder)
- [Domain Search](/snowflake/tools/domain-search)
---
## Document: Email Enrichment
Person details (name, position, company, social profiles) for an email. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/email-enrichment
# Email Enrichment
Person details (name, position, company, social profiles) for an email. You have emails (sign-ups, inbound leads) and want the person behind each: name, position, company, social profiles.
**Credits:** 1 search credit when found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `email` (Email address)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.email_enrichment('jane.doe@acme.com');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"EMAIL": "jane.doe@acme.com",
"FULL_NAME": "Jane Doe",
"POSITION": "Head of Sales",
"COMPANY": "Acme",
"LINKEDIN": "https://www.linkedin.com/in/..."
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.signups (EMAIL STRING);
INSERT INTO my_db.public.signups VALUES
('jane.doe@acme.com'),
('john@globex.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.signups TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.enrich_emails('MY_DB.PUBLIC.SIGNUPS', 'EMAIL', 'SIGNUPS_EMAIL_ENRICHMENT', 100);
SELECT INPUT_EMAIL, FULL_NAME, POSITION, COMPANY, COUNTRY, LINKEDIN
FROM TOMBA.results.SIGNUPS_EMAIL_ENRICHMENT
WHERE lookup_status = 'found';
```
Signature: `enrich_emails(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#email-enrichment).
## Tips
- Free-mail addresses (gmail.com, …) rarely match: enrichment is for work emails.
- For the person and their company in one call, use [Combined Enrichment](/snowflake/tools/combined-enrichment).
## Related
- [Person Enrichment](/snowflake/tools/person-enrichment)
- [Combined Enrichment](/snowflake/tools/combined-enrichment)
---
## Document: Domain Search
List the email addresses found for a company domain (one row per email). Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/domain-search
# Domain Search
List the email addresses found for a company domain (one row per email). Find the people and email addresses Tomba knows at a company, for account-based prospecting.
**Credits:** 1 search credit per 10 emails requested, when emails are found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `domain` (Company domain)
| Option | Type | Default | Description |
| ------------------- | ------- | ------- | ----------------------------------------------------------------------------------------- |
| `emails_per_domain` | INT | `10` | Emails per domain. One of: 1, 5, 10, 20, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500. |
| `enrich_mobile` | BOOLEAN | `false` | Also look up mobile phones (+5 credits per phone found). |
| `department` | STRING | `''` | Departments (comma separated). |
| `country` | STRING | `''` | Country (ISO-2). |
| `email_type` | STRING | `''` | Email type. One of: '', 'all', 'personal', 'generic'. |
## Try one value
```sql
CALL TOMBA.core.domain_search('acme.com', 10);
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"EMAIL": "jane.doe@acme.com",
"FULL_NAME": "Jane Doe",
"POSITION": "Head of Sales",
"DEPARTMENT": "sales",
"SENIORITY": "senior"
},
{
"EMAIL": "john@acme.com",
"FULL_NAME": "John Roe",
"POSITION": "CTO",
"DEPARTMENT": "engineering",
"SENIORITY": "executive"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.accounts (WEBSITE STRING);
INSERT INTO my_db.public.accounts VALUES
('acme.com'),
('globex.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.accounts TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.search_domains('MY_DB.PUBLIC.ACCOUNTS', 'WEBSITE', 'ACCOUNTS_DOMAIN_SEARCH', 100);
SELECT INPUT_DOMAIN, EMAIL, FULL_NAME, POSITION, DEPARTMENT, SENIORITY, VERIFICATION_STATUS
FROM TOMBA.results.ACCOUNTS_DOMAIN_SEARCH
WHERE lookup_status = 'found';
```
Signature: `search_domains(input_table STRING, domain_column STRING, output_table STRING, max_rows INT DEFAULT 100000, emails_per_domain INT DEFAULT 10)`
### Other inputs with `run_tool`
Only sales people in Germany, 20 per domain:
```sql
CALL TOMBA.core.run_tool('domain_search', 'MY_DB.PUBLIC.ACCOUNTS', {'domain': 'WEBSITE'},
'ACCOUNTS_DOMAIN_SEARCH_2', 100, {'emails_per_domain': 20, 'department': 'sales', 'country': 'DE'});
```
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#domain-search).
## Tips
- The output has one row per email, so a domain can produce many rows.
- Credits: 1 per 10 emails requested (`emails_per_domain` 20 = 2 credits per domain with results).
- Filter by `department`, `country` or `email_type` (`personal` or `generic`) with `run_tool` options.
## Related
- [Email Finder](/snowflake/tools/email-finder)
- [Company Enrichment](/snowflake/tools/company-enrichment)
- [Similar Domains](/snowflake/tools/similar-domains)
---
## Document: Company Enrichment
Firmographics (size, revenue, founding year, location) for a company domain. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/company-enrichment
# Company Enrichment
Firmographics (size, revenue, founding year, location) for a company domain. Add them to your accounts for segmentation and routing.
**Credits:** 1 search credit when found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `domain` (Company domain)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.company_enrichment('acme.com');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"NAME": "Acme",
"EMPLOYEES": "51-200",
"FOUNDED_YEAR": "2012",
"COUNTRY": "United States"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.accounts (WEBSITE STRING);
INSERT INTO my_db.public.accounts VALUES
('acme.com'),
('globex.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.accounts TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.enrich_companies('MY_DB.PUBLIC.ACCOUNTS', 'WEBSITE', 'ACCOUNTS_COMPANY_ENRICHMENT', 100);
SELECT INPUT_DOMAIN, NAME, EMPLOYEES, ANNUAL_REVENUE, FOUNDED_YEAR, COMPANY_TYPE, CITY, COUNTRY
FROM TOMBA.results.ACCOUNTS_COMPANY_ENRICHMENT
WHERE lookup_status = 'found';
```
Signature: `enrich_companies(input_table STRING, domain_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#company-enrichment).
## Tips
- The domain column can hold full URLs (`https://www.acme.com/about`): the app keeps only the domain.
- To find new companies rather than enrich known ones, use Companies Search.
## Related
- [Companies Search](/snowflake/tools/companies-search)
- [Similar Domains](/snowflake/tools/similar-domains)
- [Domain Search](/snowflake/tools/domain-search)
---
## Document: Companies Search
Build a table of companies from structured filters (country, industry, size, revenue, technologies) with Tomba for Snowflake.
URL: /snowflake/tools/companies-search
# Companies Search
Find companies that match structured filters and write them to a table: market sizing, territory planning,
or a fresh list of target accounts. You choose the filters; there is no free-text or AI search.
**Credits:** 1 search credit per 50 companies returned (companies come in pages of 50). See
[Pricing and limits](/snowflake/pricing-and-limits).
## Build a table of companies
```sql
CALL TOMBA.core.search_companies(
{'location_country': ['DE', 'AT'],
'industry': 'Financial Services',
'size': ['51-200', '201-500'],
'keywords': {'include': ['payments'], 'exclude': ['crypto']}},
'DACH_FINTECH', 500);
SELECT NAME, WEBSITE_URL, INDUSTRY, COMPANY_SIZE, REVENUE, CITY, COUNTRY
FROM TOMBA.results.DACH_FINTECH;
```
Signature: `search_companies(filters VARIANT, output_table STRING, max_results INT DEFAULT 500)`. At most
5,000 companies per call. For large searches, use `search_companies_async` (same arguments) to run it in the
[background](/snowflake/background-runs).
## Filters
A filter is a single value, a list of values, or `{'include': [...], 'exclude': [...]}`, with up to 50 values
each. Filters are combined with AND.
| Filter | Values |
| --------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `location_country` (alias `country`) | two-letter ISO codes: `DE`, `US`, … |
| `location_city` (alias `city`) | city names: `Berlin` |
| `location_state` (alias `state`) | US state codes (`CA`) or state/region names |
| `industry` (alias `industries`) | LinkedIn industry labels: `Financial Services`, `Software Development`, … |
| `size` | employees: `1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+` |
| `type` | `education`, `government`, `nonprofit`, `private`, `public`, `personal` |
| `revenue` | `0-1m`, `1m-10m`, `10m-50m`, `50m-100m`, `100m+` |
| `founded` | a year (`2015`) or `before 1990`, `1990-1999`, `2000-2009`, `2010-2019`, `2020+` |
| `keywords` (alias `keyword`) | words that describe the business: `payments` |
| `technologies` (alias `technology`) | technologies the company uses: `AWS`, `React` |
| `company` (alias `domain`) | company domains: `stripe.com` |
| `similar`, `suggestion`, `sic`, `naics` | passed to Tomba as given |
## More examples
```sql
-- SaaS companies in the US using Snowflake, founded since 2010
CALL TOMBA.core.search_companies(
{'country': 'US', 'industry': 'Software Development', 'technologies': 'Snowflake',
'founded': ['2010-2019', '2020+']}, 'US_SAAS_ON_SNOWFLAKE', 1000);
-- one page (50 companies) as a result, without a table
CALL TOMBA.core.lookup('companies_search', {'filters': {'country': 'FR', 'size': '11-50'}, 'page': 1});
```
## Output columns
One row per company: `INPUT_FILTERS` (the filters, as JSON), the company columns, and the common columns. Every
column, with its type: [Output columns](/snowflake/reference/output-columns#companies-search).
## Tips
- Use the Streamlit app's **Find companies** page to build filters without writing JSON.
- `TOTAL_EMAILS` shows how many emails Tomba knows at each company: sort by it to prioritize.
- Chain it: run [Domain Search](/snowflake/tools/domain-search) on `WEBSITE_URL` to find people, or
[Company Enrichment](/snowflake/tools/company-enrichment) for more firmographics.
## Related
- [Similar Domains](/snowflake/tools/similar-domains)
- [Company Enrichment](/snowflake/tools/company-enrichment)
- [Domain Search](/snowflake/tools/domain-search)
---
## Document: Combined Enrichment
Person and company profile in one lookup, from an email. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/combined-enrichment
# Combined Enrichment
Person and company profile in one lookup, from an email.
**Credits:** 2 search credits when found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `email` (Email address)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.combined_enrichment('jane.doe@acme.com');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 2,
"results": [
{
"FULL_NAME": "Jane Doe",
"TITLE": "Head of Sales",
"COMPANY_NAME": "Acme",
"COMPANY_EMPLOYEES": "51-200",
"COMPANY_COUNTRY": "United States"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.signups (EMAIL STRING);
INSERT INTO my_db.public.signups VALUES
('jane.doe@acme.com');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.signups TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.enrich_combined('MY_DB.PUBLIC.SIGNUPS', 'EMAIL', 'SIGNUPS_COMBINED_ENRICHMENT', 100);
SELECT INPUT_EMAIL, FULL_NAME, TITLE, ROLE, COMPANY_NAME, COMPANY_EMPLOYEES, COMPANY_COUNTRY
FROM TOMBA.results.SIGNUPS_COMBINED_ENRICHMENT
WHERE lookup_status = 'found';
```
Signature: `enrich_combined(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#combined-enrichment).
## Tips
- Costs 2 credits when found, the same as Person Enrichment plus Company Enrichment, in one call.
## Related
- [Person Enrichment](/snowflake/tools/person-enrichment)
- [Company Enrichment](/snowflake/tools/company-enrichment)
---
## Document: Author Finder
Find the email of the author of an article URL. Tutorial for Tomba for Snowflake: try one value, then run it over a table.
URL: /snowflake/tools/author-finder
# Author Finder
Find the email of the author of an article URL. Reach the writers of articles in your space: PR, content partnerships, link building.
**Credits:** 1 search credit when found. Lookups that find nothing cost nothing. See [Pricing and limits](/snowflake/pricing-and-limits).
## Inputs and options
Input: `url` (Article URL)
This tool has no options.
## Try one value
```sql
CALL TOMBA.core.author_finder('https://example.com/blog/how-we-scaled');
```
The call returns the result as a `VARIANT` (example values, shortened):
```json
{
"status": "found",
"credits": 1,
"results": [
{
"EMAIL": "jane.doe@example.com",
"FULL_NAME": "Jane Doe",
"ARTICLE_TITLE": "How we scaled",
"PUBLISHER_NAME": "Example"
}
]
}
```
`status` is `found`, `not_found`, `invalid` or `error`; `results` holds the output columns and `data` the complete
Tomba record.
## Run it over a table
Create a small table to try it (or use one of yours, after [granting the app access](/snowflake/installation#3-let-the-app-read-your-tables)):
```sql
CREATE OR REPLACE TABLE my_db.public.articles (URL STRING);
INSERT INTO my_db.public.articles VALUES
('https://example.com/blog/how-we-scaled');
GRANT USAGE ON DATABASE my_db TO APPLICATION TOMBA;
GRANT USAGE ON SCHEMA my_db.public TO APPLICATION TOMBA;
GRANT SELECT ON TABLE my_db.public.articles TO APPLICATION TOMBA;
```
Run the tool, then read the results:
```sql
CALL TOMBA.core.find_authors('MY_DB.PUBLIC.ARTICLES', 'URL', 'ARTICLES_AUTHOR_FINDER', 100);
SELECT INPUT_URL, EMAIL, FULL_NAME, ARTICLE_TITLE, PUBLISHER_NAME, PUBLISH_DATE
FROM TOMBA.results.ARTICLES_AUTHOR_FINDER
WHERE lookup_status = 'found';
```
Signature: `find_authors(input_table STRING, url_column STRING, output_table STRING, max_rows INT DEFAULT 100000)`
## Output columns
Each row has your `INPUT_` columns, the tool's columns, and the common columns (`LOOKUP_STATUS`,
`CREDITS`, `DATA`, …). Every column, with its type and values: [Output columns](/snowflake/reference/output-columns#author-finder).
## Tips
- Pass the article's own URL, not the site's home page.
## Related
- [Email Finder](/snowflake/tools/email-finder)
- [Email Verifier](/snowflake/tools/email-verifier)
---
## Document: Settings
Tomba for Snowflake settings: speed, cache and budgets.
URL: /snowflake/reference/settings
# Settings
Read the settings with `get_settings` (any app role). Change them with `set_setting` (`app_admin`), or on the
Streamlit app's **Settings** page.
```sql
CALL TOMBA.core.get_settings();
CALL TOMBA.core.set_setting('monthly_credit_budget', '100000');
```
| Setting | What it controls | Default | Range |
| ----------------------- | -------------------------------------------------------------------------------- | --------- | ------------- |
| `max_workers` | Lookups in flight at once | 60 | 1–100 |
| `requests_per_second` | Requests per second, shared by all lookups (Tomba allows 30 per install) | 28 | 1–30 |
| `chunk_size` | Rows written and billed together (a run can stop or be cancelled between chunks) | 1,000 | 50–5,000 |
| `max_rows_cap` | The most rows a single call may read, whatever its `max_rows` | 1,000,000 | 1–10,000,000 |
| `cache_days` | Reuse results younger than this many days, for free (0 turns the cache off) | 30 | 0–365 |
| `monthly_credit_budget` | Tomba credits all runs may use per calendar month, UTC (0 = no limit) | 0 | 0–100,000,000 |
See [Cache and budgets](/snowflake/cache-and-budgets) and [Pricing and limits](/snowflake/pricing-and-limits).
---
## Document: Procedures
Every Tomba for Snowflake procedure with its exact signature, what it does and the role it needs.
URL: /snowflake/reference/procedures
# Procedures
Every procedure is in the app's `core` schema, returns a `VARIANT`, and is called as
`CALL TOMBA.core.(...)`. Pass objects (not JSON strings) for `VARIANT` arguments, for example
`{'domain': 'WEBSITE'}`.
## Tools on a table
| Procedure | What it does | Role |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------ |
| `find_emails(input_table STRING, domain_column STRING, first_name_column STRING, last_name_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Email Finder over a table](/snowflake/tools/email-finder) | `app_public` |
| `verify_emails(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Email Verifier over a table](/snowflake/tools/email-verifier) | `app_public` |
| `search_domains(input_table STRING, domain_column STRING, output_table STRING, max_rows INT DEFAULT 100000, emails_per_domain INT DEFAULT 10)` | [Domain Search over a table](/snowflake/tools/domain-search) | `app_public` |
| `enrich_emails(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Email Enrichment over a table](/snowflake/tools/email-enrichment) | `app_public` |
| `enrich_people(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Person Enrichment over a table](/snowflake/tools/person-enrichment) | `app_public` |
| `enrich_companies(input_table STRING, domain_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Company Enrichment over a table](/snowflake/tools/company-enrichment) | `app_public` |
| `enrich_combined(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Combined Enrichment over a table](/snowflake/tools/combined-enrichment) | `app_public` |
| `find_emails_from_linkedin(input_table STRING, url_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [LinkedIn Finder over a table](/snowflake/tools/linkedin-finder) | `app_public` |
| `find_authors(input_table STRING, url_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Author Finder over a table](/snowflake/tools/author-finder) | `app_public` |
| `find_phones(input_table STRING, email_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Phone Finder over a table](/snowflake/tools/phone-finder) | `app_public` |
| `validate_phones(input_table STRING, phone_column STRING, output_table STRING, max_rows INT DEFAULT 100000, country_code_column STRING DEFAULT '')` | [Phone Validator over a table](/snowflake/tools/phone-validator) | `app_public` |
| `find_similar_domains(input_table STRING, domain_column STRING, output_table STRING, max_rows INT DEFAULT 100000)` | [Similar Domains over a table](/snowflake/tools/similar-domains) | `app_public` |
## Tools on one value
| Procedure | What it does | Role |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------- | ------------ |
| `email_finder(domain STRING, first_name STRING, last_name STRING)` | [Email Finder for one value](/snowflake/tools/email-finder) | `app_public` |
| `email_verifier(email STRING)` | [Email Verifier for one value](/snowflake/tools/email-verifier) | `app_public` |
| `domain_search(domain STRING, emails_per_domain INT DEFAULT 10)` | [Domain Search for one value](/snowflake/tools/domain-search) | `app_public` |
| `email_enrichment(email STRING)` | [Email Enrichment for one value](/snowflake/tools/email-enrichment) | `app_public` |
| `person_enrichment(email STRING)` | [Person Enrichment for one value](/snowflake/tools/person-enrichment) | `app_public` |
| `company_enrichment(domain STRING)` | [Company Enrichment for one value](/snowflake/tools/company-enrichment) | `app_public` |
| `combined_enrichment(email STRING)` | [Combined Enrichment for one value](/snowflake/tools/combined-enrichment) | `app_public` |
| `linkedin_finder(url STRING)` | [LinkedIn Finder for one value](/snowflake/tools/linkedin-finder) | `app_public` |
| `author_finder(url STRING)` | [Author Finder for one value](/snowflake/tools/author-finder) | `app_public` |
| `phone_finder(email STRING)` | [Phone Finder for one value](/snowflake/tools/phone-finder) | `app_public` |
| `phone_validator(phone STRING, country_code STRING DEFAULT '')` | [Phone Validator for one value](/snowflake/tools/phone-validator) | `app_public` |
| `similar_finder(domain STRING)` | [Similar Domains for one value](/snowflake/tools/similar-domains) | `app_public` |
## Any tool
| Procedure | What it does | Role |
| ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------ |
| `run_tool(tool STRING, input_table STRING, columns VARIANT, output_table STRING, max_rows INT, options VARIANT)` | [Any tool over a table, with a column mapping and options](/snowflake/enrich-a-table#other-input-combinations) | `app_public` |
| `lookup(tool STRING, params VARIANT)` | One lookup with any tool and inputs | `app_public` |
| `search_companies(filters VARIANT, output_table STRING, max_results INT DEFAULT 500)` | [Companies matching filters, into a table](/snowflake/tools/companies-search) | `app_public` |
## Background runs and resume
| Procedure | What it does | Role |
| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------ |
| `run_tool_async(tool STRING, input_table STRING, columns VARIANT, output_table STRING, max_rows INT, options VARIANT)` | [`run_tool` in the background; returns the run id](/snowflake/background-runs) | `app_public` |
| `search_companies_async(filters VARIANT, output_table STRING, max_results INT DEFAULT 500)` | [`search_companies` in the background](/snowflake/background-runs) | `app_public` |
| `cancel_run(run_id STRING)` | [Stop a running or queued run](/snowflake/background-runs#cancel) | `app_public` |
| `resume_run(run_id STRING)` | [Continue a stopped run](/snowflake/resume-and-auto-resume) | `app_public` |
| `set_auto_resume(run_id STRING, enabled BOOLEAN)` | [Turn auto-resume on or off for a run](/snowflake/resume-and-auto-resume#auto-resume) | `app_public` |
| `resume_pending()` | Resume every run marked for auto-resume now (the app runs it daily) | `app_public` |
| `process_jobs()` | Start queued background runs (the app runs it automatically) | `app_public` |
## Schedules
| Procedure | What it does | Role |
| ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------ |
| `create_schedule(name STRING, tool STRING, input_table STRING, columns VARIANT, output_table STRING, cron STRING, options VARIANT)` | [Keep a table enriched on a CRON schedule](/snowflake/schedules) | `app_admin` |
| `list_schedules()` | Schedules and their last run | `app_public` |
| `pause_schedule(name STRING)` | Pause a schedule | `app_admin` |
| `resume_schedule(name STRING)` | Restart a paused schedule | `app_admin` |
| `run_schedule(name STRING)` | Run a schedule now | `app_public` |
| `drop_schedule(name STRING)` | Delete a schedule (its table stays) | `app_admin` |
## Utilities
| Procedure | What it does | Role |
| ---------------------------------------- | ----------------------------------------------------------------------------------------- | ------------ |
| `status()` | App version, connection approval, trial, daily limit | `app_public` |
| `list_tools()` | Every tool with its inputs, options, credits and columns | `app_public` |
| `pricing()` | [Price per credit and credits per tool](/snowflake/pricing-and-limits) | `app_public` |
| `usage_summary(days INT DEFAULT 30)` | [Runs, credits and charges per day and tool](/snowflake/results-and-history#usage-by-day) | `app_public` |
| `get_settings()` | [Current settings](/snowflake/reference/settings) | `app_public` |
| `set_setting(name STRING, value STRING)` | [Change a setting](/snowflake/reference/settings) | `app_admin` |
| `clear_cache(tool STRING DEFAULT '')` | [Forget cached results for a tool ('' for all)](/snowflake/cache-and-budgets) | `app_admin` |
| `drop_result(output_table STRING)` | [Drop a result table](/snowflake/results-and-history) | `app_public` |
| `reset_credentials()` | Make the app reconnect to Tomba on its next lookup | `app_admin` |
Views: `TOMBA.core.runs` (every run) and `TOMBA.core.usage_by_day`. See [Results and history](/snowflake/results-and-history).
---
## Document: Output columns
The columns of Tomba for Snowflake result tables, with their types and values: common columns and each tool's columns.
URL: /snowflake/reference/output-columns
# Output columns
Every result table has the same layout: your `INPUT_` columns, then the tool's columns, then the common
columns. Types are Snowflake types: `STRING` is `VARCHAR` and `INT` is `NUMBER(38,0)`. Any tool column can be
NULL when Tomba has no value for it.
## Common columns
| Column | Type | Description |
| --------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `INPUT_` | `STRING` | Your input values, as text and before cleaning, one column per mapped input (for example `INPUT_DOMAIN` or `INPUT_EMAIL`). Use them to join the results back. |
| `LOOKUP_STATUS` | `STRING` | `found`, `not_found`, `invalid` (the value can't be used, so it wasn't sent), `error` (failed after retries) or `skipped` (not looked up because the run stopped first). |
| `CREDITS` | `INT` | Tomba credits the lookup used. When a lookup writes several rows, the first row carries them and the others show 0. Cached results and repeated inputs show 0. |
| `CREDIT_TYPE` | `STRING` | `search` or `verifier`. NULL when `CREDITS` is 0. |
| `DATA` | `VARIANT` | The complete Tomba record behind the row: every field, not only the columns. |
| `ERROR` | `STRING` | Why the row is `invalid`, `error` or `skipped`, and for some `not_found` rows. `claimed` means the person asked Tomba to stop processing their data. |
| `LOOKED_UP_AT` | `TIMESTAMP_NTZ` | When the row was looked up or read from the cache, in UTC. |
| `SOURCE` | `STRING` | `api` for a new lookup, or `cache` when a recent result was reused at no charge. |
Domain Search, Similar Domains and Companies Search write one row per result, so one input can produce many
rows.
## Columns per tool
### Email Finder
The [Email Finder](/snowflake/tools/email-finder) tool adds these columns:
| Column | Type | Description |
| --------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EMAIL` | `STRING` | The email address. |
| `SCORE` | `INT` | Tomba's confidence in the address, 0–100. |
| `VERIFICATION_STATUS` | `STRING` | The result of Tomba's last verification of the address, as a verifier `status` in lowercase ([values](/attributes/verifier#status-values)). NULL if it hasn't been verified. |
| `FIRST_NAME` | `STRING` | First name. |
| `LAST_NAME` | `STRING` | Last name. |
| `FULL_NAME` | `STRING` | Full name. |
| `POSITION` | `STRING` | Job title. |
| `DEPARTMENT` | `STRING` | Department. See [Department and role](#department-and-role). |
| `COMPANY` | `STRING` | Company name. |
| `WEBSITE_URL` | `STRING` | Company domain. |
| `COUNTRY` | `STRING` | The person's two-letter country code. |
| `LINKEDIN` | `STRING` | LinkedIn profile. |
| `TWITTER` | `STRING` | Twitter profile. |
| `EMAIL_TYPE` | `STRING` | `personal` or `generic`. See [Email type](#email-type). |
| `ACCEPT_ALL` | `BOOLEAN` | The domain's mail server accepts mail for any address (catch-all). |
| `PHONE_NUMBER` | `BOOLEAN` | Tomba has a phone number for this person. The number isn't in a column: with the `enrich_mobile` option, phones are in `DATA:phone_data`. |
### Email Verifier
The [Email Verifier](/snowflake/tools/email-verifier) tool's columns hold the API's response fields; see
[Email verifier response](/attributes/verifier) for what each check means.
| Column | Type | Description |
| --------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `EMAIL` | `STRING` | The address checked. NULL when the domain is disposable. |
| `STATUS` | `STRING` | The verifier's `status`, in lowercase. See [Status values](/attributes/verifier#status-values). |
| `RESULT` | `STRING` | The verifier's `result`, in lowercase. See [Result values](/attributes/verifier#result-values). NULL when `STATUS` is `disposable`. |
| `SCORE` | `INT` | Confidence score. Higher means more likely to be deliverable. |
| `ACCEPT_ALL` | `BOOLEAN` | The mail server accepts mail for any address (catch-all). |
| `DISPOSABLE` | `BOOLEAN` | The domain belongs to a disposable email provider. |
| `WEBMAIL` | `BOOLEAN` | The domain belongs to a webmail provider. |
| `GIBBERISH` | `BOOLEAN` | The part before `@` looks randomly generated. |
| `BLOCK` | `BOOLEAN` | The mail server blocked or refused the check. |
| `GREYLISTED` | `BOOLEAN` | The mail server temporarily deferred the check. |
| `MX_CHECK` | `BOOLEAN` | The domain has MX records. |
| `SMTP_SERVER` | `BOOLEAN` | Same value as `SMTP_CHECK`. |
| `SMTP_CHECK` | `BOOLEAN` | Same value as `MX_CHECK`. It doesn't show whether the mailbox exists; use `STATUS`. |
| `SMTP_PROVIDER` | `STRING` | Mail provider detected from the domain's MX records. |
### Domain Search
The [Domain Search](/snowflake/tools/domain-search) tool writes one row per email found at the domain.
| Column | Type | Description |
| ---------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EMAIL` | `STRING` | The email address. |
| `FIRST_NAME` | `STRING` | First name. |
| `LAST_NAME` | `STRING` | Last name. |
| `FULL_NAME` | `STRING` | Full name. |
| `POSITION` | `STRING` | Job title. |
| `DEPARTMENT` | `STRING` | Department. See [Department and role](#department-and-role). |
| `SENIORITY` | `STRING` | `junior`, `senior` or `executive`. |
| `EMAIL_TYPE` | `STRING` | `personal` or `generic`. See [Email type](#email-type). |
| `SCORE` | `INT` | Tomba's confidence in the address, 0–100. |
| `VERIFICATION_STATUS` | `STRING` | The result of Tomba's last verification of the address, as a verifier `status` in lowercase ([values](/attributes/verifier#status-values)). NULL if it hasn't been verified. |
| `COUNTRY` | `STRING` | The person's two-letter country code. |
| `LINKEDIN` | `STRING` | LinkedIn profile URL. |
| `TWITTER` | `STRING` | Twitter profile URL. |
| `PHONE_NUMBER` | `BOOLEAN` | Tomba has a phone number for this person. With the `enrich_mobile` option, phones are in `DATA:phone_data`. |
| `ORGANIZATION` | `STRING` | Company name. |
| `INDUSTRIES` | `STRING` | Company industry. |
| `COMPANY_SIZE` | `STRING` | Company size. |
| `COMPANY_TYPE` | `STRING` | Company type. |
| `ORGANIZATION_COUNTRY` | `STRING` | Company country. |
### Email Enrichment
The [Email Enrichment](/snowflake/tools/email-enrichment) tool has the same columns as
[Email Finder](#email-finder).
### Person Enrichment
The [Person Enrichment](/snowflake/tools/person-enrichment) tool adds these columns:
| Column | Type | Description |
| ----------------- | -------- | ------------------------------------------------------------ |
| `EMAIL` | `STRING` | The email address. |
| `FULL_NAME` | `STRING` | Full name. |
| `FIRST_NAME` | `STRING` | First name. |
| `LAST_NAME` | `STRING` | Last name. |
| `TITLE` | `STRING` | Job title. |
| `ROLE` | `STRING` | Department. See [Department and role](#department-and-role). |
| `SENIORITY` | `STRING` | Seniority. |
| `COMPANY_NAME` | `STRING` | Company name. |
| `COMPANY_DOMAIN` | `STRING` | Company domain. |
| `LOCATION` | `STRING` | The person's location, as a two-letter country code. |
| `CITY` | `STRING` | City. |
| `COUNTRY` | `STRING` | Country name, such as `Germany`. |
| `LINKEDIN_HANDLE` | `STRING` | LinkedIn profile. |
| `TWITTER_HANDLE` | `STRING` | Twitter profile. |
| `GITHUB_HANDLE` | `STRING` | GitHub profile. |
The Person Enrichment API doesn't return seniority, city or GitHub profile, so `SENIORITY`, `CITY` and
`GITHUB_HANDLE` are NULL.
### Company Enrichment
The [Company Enrichment](/snowflake/tools/company-enrichment) tool adds these columns:
| Column | Type | Description |
| ----------------- | -------- | ----------------------------------------------------- |
| `NAME` | `STRING` | Company name. |
| `LEGAL_NAME` | `STRING` | Legal name. The API returns the same value as `NAME`. |
| `DOMAIN` | `STRING` | Company domain. |
| `DESCRIPTION` | `STRING` | Company description. |
| `INDUSTRY` | `STRING` | Industry. |
| `SECTOR` | `STRING` | Sector. |
| `EMPLOYEES` | `STRING` | Company size (employees), as text. |
| `ANNUAL_REVENUE` | `STRING` | Annual revenue, as text. |
| `FOUNDED_YEAR` | `STRING` | Year founded, as text. |
| `COMPANY_TYPE` | `STRING` | Company type. |
| `CITY` | `STRING` | City. |
| `COUNTRY` | `STRING` | Country name, such as `United States`. |
| `COUNTRY_CODE` | `STRING` | Two-letter country code. |
| `LINKEDIN_HANDLE` | `STRING` | LinkedIn page. |
| `TWITTER_HANDLE` | `STRING` | Twitter profile. |
| `LOGO` | `STRING` | Logo. |
The Company Enrichment API doesn't return industry, sector or logo, so `INDUSTRY`, `SECTOR` and `LOGO` are NULL.
### Combined Enrichment
The [Combined Enrichment](/snowflake/tools/combined-enrichment) tool has the
[Person Enrichment](#person-enrichment) columns, then:
| Column | Type | Description |
| ------------------------- | -------- | ---------------------------------- |
| `COMPANY_INDUSTRY` | `STRING` | Company industry. |
| `COMPANY_EMPLOYEES` | `STRING` | Company size (employees), as text. |
| `COMPANY_COUNTRY` | `STRING` | Company country name. |
| `COMPANY_LINKEDIN_HANDLE` | `STRING` | Company LinkedIn page. |
`COMPANY_NAME` and `COMPANY_DOMAIN` come from the company record when it has them. `SENIORITY`, `CITY`,
`GITHUB_HANDLE` and `COMPANY_INDUSTRY` are NULL: the API doesn't return them.
### LinkedIn Finder
The [LinkedIn Finder](/snowflake/tools/linkedin-finder) tool has the same columns as [Email Finder](#email-finder).
### Author Finder
The [Author Finder](/snowflake/tools/author-finder) tool has the [Email Finder](#email-finder) columns, then:
| Column | Type | Description |
| ---------------- | -------- | -------------------------- |
| `ARTICLE_TITLE` | `STRING` | Title of the article. |
| `PUBLISHER_NAME` | `STRING` | Publisher of the article. |
| `PUBLISH_DATE` | `STRING` | Publication date, as text. |
### Phone Finder
The [Phone Finder](/snowflake/tools/phone-finder) tool adds these columns:
| Column | Type | Description |
| -------------- | --------- | ---------------------------------------------------- |
| `VALID` | `BOOLEAN` | The number is valid for its country. |
| `E164_FORMAT` | `STRING` | The number in E.164 format, such as `+14155550100`. |
| `INTL_FORMAT` | `STRING` | International format, such as `+1 415-555-0100`. |
| `LOCAL_FORMAT` | `STRING` | National format, such as `(415) 555-0100`. |
| `COUNTRY_CODE` | `STRING` | Two-letter country code of the number, such as `US`. |
| `LINE_TYPE` | `STRING` | The kind of line. See [Line type](#line-type). |
| `CARRIER` | `STRING` | Carrier name. |
| `REGION` | `STRING` | Region of the number. |
| `TIMEZONES` | `STRING` | Time zones of the number, comma-separated. |
### Phone Validator
The [Phone Validator](/snowflake/tools/phone-validator) tool has the same columns as [Phone Finder](#phone-finder).
### Similar Domains
The [Similar Domains](/snowflake/tools/similar-domains) tool writes one row per similar company.
| Column | Type | Description |
| ---------------- | -------- | ------------------------------ |
| `SIMILAR_DOMAIN` | `STRING` | Domain of the similar company. |
| `NAME` | `STRING` | Its name. |
| `INDUSTRIES` | `STRING` | Its industry. |
### Companies Search
The [Companies Search](/snowflake/tools/companies-search) tool writes one row per company. Instead of one
`INPUT_` column per input, each row starts with `INPUT_FILTERS`: the filters of the search, as JSON.
| Column | Type | Description |
| -------------- | -------- | ----------------------------------------------------- |
| `NAME` | `STRING` | Company name. |
| `WEBSITE_URL` | `STRING` | Company domain. |
| `INDUSTRY` | `STRING` | Industry. |
| `COMPANY_SIZE` | `STRING` | Company size. |
| `COMPANY_TYPE` | `STRING` | Company type. |
| `FOUNDED` | `STRING` | Year founded. |
| `REVENUE` | `STRING` | Revenue range. |
| `COUNTRY` | `STRING` | Country. |
| `STATE` | `STRING` | State or region. |
| `CITY` | `STRING` | City. |
| `TOTAL_EMAILS` | `INT` | Number of email addresses Tomba knows at the company. |
| `LINKEDIN_URL` | `STRING` | LinkedIn page URL. |
| `DESCRIPTION` | `STRING` | Company description. |
## Values
### Email type
| Value | Meaning |
| ---------- | ------------------------------------------------------ |
| `personal` | The address of a person. |
| `generic` | A shared or role address, such as `sales@` or `info@`. |
### Department and role
`DEPARTMENT` and `ROLE` take one of these values: `accounting`, `administrative`, `communication`, `diversity`,
`education`, `engineering`, `executive`, `facilities`, `finance`, `healthcare`, `hr`, `it`, `legal`,
`management`, `marketing`, `operations`, `pr`, `sales`, `security`, `software`, `support`, `warehouse`.
### Line type
| Value | Meaning |
| ---------------------- | -------------------------------------------------------------- |
| `MOBILE` | Mobile number. |
| `FIXED_LINE` | Landline. |
| `FIXED_LINE_OR_MOBILE` | Landline or mobile: the number itself doesn't tell them apart. |
| `VOIP` | Voice over IP number. |
| `TOLL_FREE` | Toll-free number. |
| `PREMIUM_RATE` | Premium-rate number. |
| `SHARED_COST` | Shared-cost number. |
| `PERSONAL_NUMBER` | Personal number, routed to a mobile or a landline. |
| `PAGER` | Pager. |
| `UAN` | Universal access number, routed to a company's offices. |
| `UNKNOWN` | The type couldn't be determined. |
---
## Document: Tools, prompts, and resources
Reference for the 28 tools, 12 prompts, and 3 resources of Tomba's remote MCP server, with parameters and validation rules, and the local server's differences.
URL: /llm/remote-mcp/tools
# Tools, prompts, and resources
The [remote MCP server](/llm/remote-mcp/introduction) has 28 tools, each of which uses your Tomba account. This page lists every tool with its parameters as the server validates them, followed by the prompts and resources. The [local server](/llm/local-mcp/introduction) differs in the ways listed under [Local server differences](#local-server-differences).
## Conventions
- Tool and parameter names are snake_case. Leave out optional parameters rather than sending `null`.
- The server checks arguments against each tool's input schema before it calls the API. Invalid arguments return a tool error; see [Errors](/llm/remote-mcp/introduction#errors).
- The parameter tables use these formats:
- Domain: a domain name such as `stripe.com`, without a scheme or path. It must match `^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`.
- Country: two uppercase letters, as in ISO 3166-1 alpha-2, such as `US`.
- Phone: an optional `+` and 2 to 15 digits, the first of which isn't 0, with no spaces, such as `+14155552671`.
- Public URL: an `http` or `https` URL whose host isn't `localhost` or a private IPv4 address.
- Each tool returns the API response as `structuredContent` in the form `{"data": }`, and the same JSON as text. The response fields are those of the API endpoint listed for the tool.
- A tool call is billed as the API request it makes; see [Credit costs](/usage-and-quotas#credit-costs). With OAuth, a tool needs the scope of its endpoint; see [OAuth 2.0](/authentication#oauth-20).
- `create_flag` and `create_lead` change data in your account. Every other tool carries the annotation `readOnlyHint: true`, and all tools carry `openWorldHint: true`. Clients that read annotations, such as ChatGPT, ask you to confirm the two write tools.
- Where a tool accepts `webhook_url`, the API also sends the result to that URL; see [Per-request callbacks](/webhook#per-request-callbacks).
A `tools/call` request looks like this. `limit` is a string, not a number:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "domain_search",
"arguments": { "domain": "stripe.com", "limit": "20" }
}
}
```
## Find email addresses
### `domain_search`
Finds the email addresses Tomba has for a company, by domain or company name. Calls [`GET /v1/domain-search`](/api/finder#domain-search).
| Parameter | Type | Required | Rules |
| --------------- | ------- | --------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `domain` | string | `domain` or `company` | Domain |
| `company` | string | `domain` or `company` | 3 to 75 characters |
| `limit` | string | No | `"10"`, `"20"`, or `"50"`. Default `"10"`. |
| `page` | number | No | 1 or more |
| `department` | string | No | A department, such as `executive`, `it`, or `marketing`. The `tomba://data/departments` resource lists common values. |
| `country` | string | No | Country |
| `enrich_mobile` | boolean | No | Accepted but not passed to the API, so it has no effect |
| `webhook_url` | string | No | Public URL |
### `email_finder`
Finds a person's email address from their name and company. Calls [`GET /v1/email-finder`](/api/finder#email-finder).
| Parameter | Type | Required | Rules |
| --------------- | ------- | ------------------------------------------------- | ---------------------------------------------------------- |
| `domain` | string | `domain` or `company` | Domain |
| `company` | string | `domain` or `company` | 3 to 75 characters |
| `full_name` | string | `full_name`, or both `first_name` and `last_name` | At least 1 character |
| `first_name` | string | See `full_name` | At least 1 character |
| `last_name` | string | See `full_name` | At least 1 character |
| `enrich_mobile` | boolean | No | `true` also looks up the phone number of the address found |
| `webhook_url` | string | No | Public URL |
### `author_finder`
Finds the email address of an article's author. Calls [`GET /v1/author-finder`](/api/finder#author-finder).
| Parameter | Type | Required | Rules |
| ------------- | ------ | -------- | ------------------------- |
| `url` | string | Yes | Public URL of the article |
| `webhook_url` | string | No | Public URL |
### `linkedin_finder`
Finds the email address that belongs to a LinkedIn profile. Calls [`GET /v1/linkedin`](/api/finder#linkedin-finder).
| Parameter | Type | Required | Rules |
| --------------- | ------- | -------- | ---------------------------------------------------------- |
| `url` | string | Yes | Public URL that contains `linkedin.com` |
| `enrich_mobile` | boolean | No | `true` also looks up the phone number of the address found |
| `webhook_url` | string | No | Public URL |
### `email_count`
Counts the email addresses Tomba has for a domain. Calls [`GET /v1/email-count`](/api/finder#email-count).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------ |
| `domain` | string | Yes | Domain |
### `email_format`
Returns the patterns a domain uses to build email addresses. Calls [`GET /v1/email-format`](/api/finder#email-format).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------ |
| `domain` | string | Yes | Domain |
### `email_sources`
Lists the web pages where Tomba found an email address. Calls [`GET /v1/email-sources`](/api/email#email-sources).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------------- |
| `email` | string | Yes | Email address |
## Verify
### `email_verifier`
Checks whether an email address can receive mail. Calls [`GET /v1/email-verifier`](/api/verifier#email-verifier). The `status` and `result` values are explained in [Email verifier response](/attributes/verifier).
| Parameter | Type | Required | Rules |
| --------------- | ------- | -------- | ---------------------------------------------------- |
| `email` | string | Yes | Email address |
| `enrich_mobile` | boolean | No | `true` also looks up the phone number of the address |
| `webhook_url` | string | No | Public URL |
### `domain_status`
Checks whether a domain belongs to a webmail or disposable email provider. Calls [`GET /v1/domain-status`](/api/domain#domain-status).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------ |
| `domain` | string | Yes | Domain |
## Enrich
### `email_enrichment`
Returns the person and company behind an email address. Calls [`GET /v1/enrich`](/api/finder#email-enrichment).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------------- |
| `email` | string | Yes | Email address |
### `person_enrichment`
Returns a person's profile from their email address. Calls [`GET /v1/people/find`](/api/enrichment#person-api).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------------- |
| `email` | string | Yes | Email address |
### `company_enrichment`
Returns a company's profile from its domain. Calls [`GET /v1/companies/find`](/api/enrichment#company-api).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------ |
| `domain` | string | Yes | Domain |
### `combined_enrichment`
Returns the person and company profiles for an email address in one call. Calls [`GET /v1/combined/find`](/api/enrichment#combined-api).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------------- |
| `email` | string | Yes | Email address |
## Phone numbers
### `phone_finder`
Finds phone numbers from an email address, a domain, or a LinkedIn profile. Calls `GET /v1/phone`, which behaves like [`GET /v1/phone-finder`](/api/phone#phone-finder).
| Parameter | Type | Required | Rules |
| ------------- | ------- | ------------------------------------------------ | ------------------------------------------------------ |
| `email` | string | At least one of `email`, `domain`, or `linkedin` | Email address |
| `domain` | string | See `email` | Domain |
| `linkedin` | string | See `email` | Public URL that contains `linkedin.com` |
| `full` | boolean | No | `true` returns every phone number found instead of one |
| `webhook_url` | string | No | Public URL |
### `phone_validator`
Validates a phone number. Calls [`GET /v1/phone-validator`](/api/phone#phone-validator).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ----- |
| `phone` | string | Yes | Phone |
## Companies and domains
### `companies_search`
Searches Tomba's company database with a text query and filters. Calls [`POST /v1/reveal/search`](/api/reveal#search-companies).
| Parameter | Type | Required | Rules |
| ------------- | ------ | -------- | ------------------------------------------------- |
| `query` | string | No | 8 to 100 characters |
| `filters` | object | No | See below |
| `page` | number | No | 1 to 1000. Default 1. |
| `webhook_url` | string | No | Public URL. The API ignores it for this endpoint. |
`filters` accepts the keys `location_country`, `location_city`, `location_state`, `industry`, `size`, `type`, `keywords`, `founded`, `technologies`, `similar`, `revenue`, `sic`, and `naics`. Each key takes an object with `include` and `exclude` arrays of strings. The accepted values are described in the request body of [Search companies](/api/reveal#search-companies).
```json
{
"filters": {
"location_country": { "include": ["US"] },
"industry": { "include": ["Computer Software"] }
},
"page": 1
}
```
### `similar_finder`
Finds companies similar to a domain. Calls [`GET /v1/similar`](/api/domain#similar).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------ |
| `domain` | string | Yes | Domain |
### `technology_finder`
Lists the technologies a website uses. Calls [`GET /v1/technology`](/api/domain#technology).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------ |
| `domain` | string | Yes | Domain |
### `autocomplete`
Suggests companies, with their domains and logos, for a partial company name. Calls [`GET /v1/domain-suggestions`](/api/domain-suggestions#get-domain-suggestions).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------------------- |
| `query` | string | Yes | 1 to 100 characters |
### `location`
Counts a company's employees by country. Calls [`GET /v1/location`](/api/finder#location).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ------ |
| `domain` | string | Yes | Domain |
## Account
`account_info` returns your API secret, in `secret_token`, and `list_keys`
returns the full value of every API key in your workspace. Like any tool
result, both reach the assistant and your model provider. Turn these tools
off in your client if the assistant doesn't need them.
### `account_info`
Returns your account details, plan, and usage. Calls [`GET /v1/me`](/api/account#get-account). It takes no parameters.
### `usage_info`
Returns your API usage. Calls [`GET /v1/usage`](/api/account#retrieve-api-usage). It takes no parameters. It needs key authentication and fails when you signed in with OAuth.
### `list_keys`
Lists the API keys of your workspace. Calls [`GET /v1/keys`](/api/keys#retrieve-api-keys). It takes no parameters.
### `get_logs`
Returns your recent API requests. Calls [`GET /v1/logs`](/api/account#retrieve-api-logs). It needs key authentication and fails when you signed in with OAuth.
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | ---------------- |
| `page` | number | No | Page of results |
| `limit` | number | No | Results per page |
## Leads and flags
### `list_leads`
Lists your leads. Calls [`GET /v1/leads`](/api/leads#retrieve-leads).
| Parameter | Type | Required | Rules |
| --------- | ------ | -------- | -------------------------------- |
| `page` | number | No | Page of results |
| `limit` | number | No | Results per page |
| `domain` | string | No | Return only leads at this domain |
### `create_lead`
Adds a lead to one of your lead lists. This tool changes data. Calls [`POST /v1/leads`](/api/leads#create-a-lead).
| Parameter | Type | Required | Rules |
| ------------ | ------- | -------- | ------------------------------ |
| `email` | string | Yes | Email address |
| `list_id` | integer | Yes | ID of the lead list, 1 or more |
| `first_name` | string | No | |
| `last_name` | string | No | |
| `company` | string | No | |
| `position` | string | No | Job title |
### `list_flags`
Lists the reports of incorrect data you've submitted, with their status. Calls [`GET /v1/flag`](/api/flag#list-my-flags).
| Parameter | Type | Required | Rules |
| --------- | ------- | -------- | --------- |
| `page` | integer | No | 1 or more |
| `limit` | integer | No | 1 to 100 |
### `create_flag`
Reports incorrect data, such as an address that bounced. This tool changes data. Calls [`POST /v1/flag`](/api/flag#flag-incorrect-data). Which reports earn a credit back is explained in [Refunds for incorrect data](/usage-and-quotas#refunds-for-incorrect-data).
| Parameter | Type | Required | Rules |
| ----------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flag_type` | string | Yes | `email`, `organization`, `phone`, `author_url`, or `website` |
| `value` | string | Yes | 2 to 255 characters: the email address, domain, phone number, or URL you report |
| `reason` | string | Yes | `hard_bounce`, `invalid_email`, `wrong_person`, `outdated`, `other`, `wrong_company`, `wrong_phone`, or `broken_url`. The API accepts only the reasons that apply to the `flag_type`; see [Flag incorrect data](/api/flag#flag-incorrect-data). |
| `comment` | string | No | Up to 1,000 characters |
## Prompts
Prompts are message templates that start a multi-step task with the tools. Clients show them as commands or templates, and ask for the arguments. Arguments are text.
| Prompt | Required arguments | Optional arguments | Task |
| ------------------------- | --------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------- |
| `lead-research` | `domain` | `targetDepartment` | Find, verify, and enrich contacts at one company |
| `find-person` | `firstName`, `lastName`, `company` | | Find, verify, and enrich one person's email address and phone number |
| `email-verification` | `emails` (comma-separated) | | Verify a list of addresses and report on each |
| `content-outreach` | `urls` (comma-separated article URLs) | | Find and verify the authors of articles |
| `competitor-analysis` | `domain` | `depth` | Find similar companies and compare their technologies and contacts |
| `account-based-marketing` | `domain`, `productService` | `targetPersonas`, `campaignGoal` | Plan an account-based campaign for one company |
| `investor-research` | `targetCompanyDomain`, `yourCompanyDescription`, `fundingStage` | `industryFocus` | Research an investment firm and its contacts |
| `hiring-outreach` | `targetCompanies`, `roleTitle`, `targetDepartment` | `yourCompanyDomain` | Find candidates for a role at target companies |
| `partnership-research` | `yourDomain`, `partnershipType` | `targetIndustry`, `targetSize` | Find potential partners and their contacts |
| `market-research` | `seedCompany`, `industryName` | `researchFocus`, `numberOfCompanies` | Analyze companies in a market, starting from one company |
| `sales-territory-mapping` | `accountDomains`, `productCategory` | `averageDealSize`, `quarterlyTarget` | Plan coverage of a list of target accounts |
| `due-diligence` | `targetDomain`, `purpose` | `depth` | Research a company before a decision such as an investment, an acquisition, or a partnership |
The server lists optional arguments that have a default value as required, so your client may ask for them. Arguments with a fixed set of values reject anything else, and the error lists the allowed values.
`numberOfCompanies` and `quarterlyTarget` are numbers in the server's schema, but clients send every prompt argument as text, and the server rejects text for them. Leave both empty; `numberOfCompanies` then defaults to 10.
## Resource URIs
| URI | Contents |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tomba://api/status` | Your account details from `GET /v1/me`, as JSON. Like `account_info`, it includes your API secret. |
| `tomba://data/departments` | Common values for the `department` parameter of `domain_search`, as JSON |
| `tomba://docs/api-reference` | A short Markdown overview of the tools and authentication methods. It covers only 12 tools and uses some outdated parameter names; use `tools/list` for the current schemas. |
## Local server differences
The [local MCP server](/llm/local-mcp/introduction), `@tomba-io/tomba-mcp-server`, has the same 28 tools, under the same names. This section describes version 1.2.0.
| | Remote server | Local server |
| ---------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Authentication | OAuth or API key, sent by the client | `TOMBA_API_KEY` and `TOMBA_SECRET_KEY` environment variables |
| Result format | `structuredContent` `{"data": }`, and the same JSON as text | Text only: JSON of the form `{"data": , "rateLimit": {...}}` |
| Rate limit information | Not passed to the client | In the `rateLimit` object of every result |
| Retries | Up to 3 attempts on `429` and `5xx` | None |
| Timeout | 120 seconds per attempt | 120 seconds |
| `webhook_url` | On the tools listed above | Not available |
| Prompts | 12, with kebab-case names | 16, with snake_case names |
| Resources | 3 | 7 |
Parameters that differ from the tables above:
- `email_finder` takes `fullName`, `firstName`, and `lastName` in camelCase. `firstName` and `lastName` accept only ASCII letters, spaces, hyphens, and apostrophes, up to 50 characters; use `fullName` for other names.
- `email_finder`, `email_verifier`, `email_enrichment`, and `linkedin_finder` list an `enrichMobile` parameter, which version 1.2.0 doesn't pass to the API, so it has no effect.
- `domain_search` accepts only the `department` values listed in its schema.
- `phone_validator` lists a `country` parameter, which isn't passed to the API.
- `companies_search` has no `query` parameter. Its schema marks `filters` as required, accepts no `technologies` filter, and limits `industry`, `type`, `size`, and `revenue` to the values listed in the schema.
- `list_flags`, `list_leads`, and `get_logs` default `page` to 1 and `limit` to 10.
The local server has these prompts:
| Prompt | Required arguments | Optional arguments |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------ |
| `find_contact` | `firstName`, `lastName`, `company` | |
| `verify_email_list` | `emails` | |
| `research_company` | `domain` | `department` |
| `enrich_lead` | | `email`, `linkedin` |
| `find_journalists` | `urls` | |
| `finder_phone` | `email` | `linkedin` |
| `validate_phone` | `phone` | `country` |
| `competitor_analysis` | `domain` | `include_technology` |
| `technology_audit` | `domain` | `include_similar` |
| `domain_insights` | `domain` | `include_samples` |
| `bulk_domain_research` | `domains` | `include_technology` |
| `find_target_companies` | | `location`, `industry`, `size`, `type`, `revenue` |
| `market_research` | `industry`, `location` | `size_range`, `revenue_range`, `company_type`, `include_contacts` |
| `lead_generation` | `company_query` | `target_department`, `contact_role` |
| `prospect_enrichment` | `company_name` | `location`, `get_technology`, `get_contacts` |
| `industry_analysis` | `industry`, `location` | `company_sizes`, `revenue_ranges`, `company_types`, `include_technology` |
And these resources. Reading one of the four with a `{domain}` or `{email}` part calls the API, and is billed like the matching tool:
| URI | Contents |
| ----------------------------- | ---------------------------------------------------------------- |
| `tomba://api/status` | A fixed status message from the server. It doesn't call the API. |
| `tomba://domain/{domain}` | A domain search for the domain, with `limit` `"10"` |
| `tomba://email/{email}` | Email enrichment for the address |
| `tomba://similar/{domain}` | Similar domains |
| `tomba://technology/{domain}` | The domain's technologies |
| `tomba://docs/api` | A short Markdown overview of the API |
| `tomba://docs/tools` | A Markdown list of the tools |
---
## Document: Remote MCP server
Tomba's hosted Model Context Protocol server at mcp.tomba.io: endpoint, transport, authentication methods, billing and limits, and errors.
URL: /llm/remote-mcp/introduction
# Remote MCP server
Tomba hosts a Model Context Protocol (MCP) server that lets AI assistants find and verify email addresses, look up phone numbers, enrich people and companies, and manage leads. You connect a client to one URL; there's nothing to install.
## Endpoint
| Property | Value |
| ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| URL | `https://mcp.tomba.io/mcp` |
| Transport | Streamable HTTP. Clients send JSON-RPC 2.0 messages with `POST`, and the server answers with server-sent events. |
| Protocol versions | `2024-11-05`, `2025-03-26`, `2025-06-18`, and `2025-11-25` |
| Sessions | None. The server doesn't issue an `Mcp-Session-Id`, so every request carries its own credentials. |
| Health check | `GET https://mcp.tomba.io/health`, no authentication |
The server exposes 28 tools, 12 prompts, and 3 resources, listed in [Tools, prompts, and resources](/llm/remote-mcp/tools). [Connect a client](/llm/remote-mcp/clients) has the setup for each assistant. To run a server on your own machine instead, see [Local MCP server](/llm/local-mcp/introduction).
## How it works
Each tool call becomes one request to the Tomba API (`https://api.tomba.io/v1`), made with your credentials. The server forwards the credentials with that request and keeps no state between requests.
## Authentication
The server accepts three kinds of credentials, each identifying a Tomba account.
| Method | What the client sends | Tools you can call |
| ------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------- |
| [OAuth 2.0](#oauth-20) | `Authorization: Bearer `, obtained when you sign in | All tools, except `usage_info` and `get_logs` |
| [API key as a bearer token](#api-key-as-a-bearer-token) | `Authorization: Bearer ` | All tools |
| [API key headers](#api-key-headers) | `X-Tomba-Key` and `X-Tomba-Secret` | All tools |
When a request carries none of them, the server answers `401` with the header `WWW-Authenticate: Bearer resource_metadata="https://mcp.tomba.io/.well-known/oauth-protected-resource"` and the JSON-RPC error `-32001` (`Authentication required`). The server doesn't check a key and secret when the client connects: wrong values show up as an error on the first tool call.
### OAuth 2.0
Clients that support MCP authorization read the metadata URL from the `401`, open a browser window where you sign in to Tomba and approve access, and then send the access token with every request. You don't handle keys. The authorization server is `https://api.tomba.io`; its endpoints, scopes, and token lifetimes are listed under [OAuth 2.0](/authentication#oauth-20).
- Each tool needs the scope of the API endpoint it calls; [Tools, prompts, and resources](/llm/remote-mcp/tools) lists the endpoint of every tool.
- `usage_info` and `get_logs` call endpoints that don't accept OAuth tokens. They fail under OAuth; use a key and secret for them.
- Review or revoke access under [Connected apps](https://app.tomba.io/settings/connected-apps) in the dashboard.
### API key as a bearer token
For clients that can send an `Authorization` header but not custom headers, encode your key and secret, separated by a colon, in base64. Remove line breaks, which `base64` adds to long input on Linux:
```bash
printf '%s' "$TOMBA_API_KEY:$TOMBA_SECRET_KEY" | base64 | tr -d '\n'
```
Send the result as `Authorization: Bearer `. To check the token, list the tools:
```bash
TOKEN=$(printf '%s' "$TOMBA_API_KEY:$TOMBA_SECRET_KEY" | base64 | tr -d '\n')
curl -X POST "https://mcp.tomba.io/mcp" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
The response is a server-sent event whose `data` line holds the JSON-RPC result with 28 tools.
### API key headers
Send your key and secret in the same headers as the [Tomba API](/authentication):
```http
X-Tomba-Key: ta_xxxx
X-Tomba-Secret: ts_xxxx
```
If a request also carries an `Authorization: Bearer` header, the server uses that header instead.
## Limits and billing
- A tool call is billed like the API request it makes, against your plan's credits: see [Credit costs](/usage-and-quotas#credit-costs).
- Tool calls count toward your plan's [rate limits](/rate-limits#limits-by-plan). The server has no limit of its own and doesn't pass rate limit headers to the client; see [Check your limits](/rate-limits#check-your-limits) for the current values.
- When the Tomba API answers `429` or a `5xx` status, the server tries the request up to 3 times in total, waiting 1 second and then 2 seconds between attempts. Each attempt times out after 120 seconds, and a timed-out attempt isn't retried.
## Errors
| Situation | What the client receives |
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| No credentials | HTTP `401` with `WWW-Authenticate` and JSON-RPC error `-32001` |
| Arguments that fail the tool's input schema | A tool result with `isError: true` and a message naming the invalid argument |
| An error from the Tomba API, such as invalid credentials, exhausted credits, or a `429` after the retries | A tool result with `isError: true` and the API's error message. [Errors](/error-handling#error-types) explains each error type. |
| An unknown path | HTTP `404` with JSON-RPC error `-32000` |
| An internal failure | HTTP `500` with JSON-RPC error `-32603` |
---
## Document: Connect a client
Connect Claude, ChatGPT, VS Code, Cursor, Windsurf, Zed, Gemini CLI, and Cline to Tomba's remote MCP server at mcp.tomba.io, with OAuth or API key headers.
URL: /llm/remote-mcp/clients
# Connect a client
Add the remote MCP server at `https://mcp.tomba.io/mcp` to your AI client. Clients that support MCP authorization sign you in to Tomba with OAuth; the others send your API key and secret in headers.
| Client | Where the setting lives | Authentication |
| --------------------------------- | ---------------------------------------------------- | ------------------------ |
| [Claude Desktop](#claude-desktop) | Customize → Connectors, in your Claude account | OAuth |
| [Claude Code](#claude-code) | `claude mcp add` | OAuth or API key headers |
| [ChatGPT](#chatgpt) | A developer mode app | OAuth |
| [VS Code](#vs-code) | `.vscode/mcp.json`, or the user configuration | OAuth or API key headers |
| [Cursor](#cursor) | `~/.cursor/mcp.json` or `.cursor/mcp.json` | API key headers |
| [Windsurf](#windsurf) | `mcp_config.json` | API key headers or OAuth |
| [Zed](#zed) | `settings.json` | OAuth or bearer token |
| [Gemini CLI](#gemini-cli) | `~/.gemini/settings.json` or `.gemini/settings.json` | OAuth or API key headers |
| [Cline](#cline) | `cline_mcp_settings.json` | API key headers |
`~` is your home directory: `/Users/` on macOS, `/home/` on Linux, and `C:\Users\` on Windows. For header authentication, copy your key and secret as described in [Get your key and secret](/authentication#get-your-key-and-secret). The [Authentication](/llm/remote-mcp/introduction#authentication) section of the server page describes each method.
After you connect, check the setup:
1. The client shows the Tomba server as connected. Where the client lists tools, it shows 28; each section below says where to look.
2. Ask the assistant to run the `list_flags` tool. A list of flags, even an empty one, means your credentials work. An authentication error means they don't: see [Troubleshooting](#troubleshooting).
## Claude Desktop
Claude Desktop adds remote servers as custom connectors, which live in your Claude account rather than in a local file. Connectors authenticate with OAuth and can't send API key headers; to use an API key with Claude Desktop, run the [local MCP server](/llm/local-mcp/introduction#claude-desktop) instead.
1. In Claude, open **Customize → Connectors**, select **+**, then **Add custom connector**.
2. Enter the URL `https://mcp.tomba.io/mcp`. Leave the OAuth fields under **Advanced settings** empty.
3. Select **Add**, then **Connect**. Sign in to Tomba in the browser window that opens, and approve access.
On a Team or Enterprise plan, an Owner adds the connector under **Organization settings → Connectors** (**Add**, then **Custom**, then **Web**). Each member then selects **Connect** on it under **Customize → Connectors**.
To verify, open the Tomba connector under **Customize → Connectors** and check its tool list. To use the tools in a conversation, turn the connector on from the **+** menu under **Connectors**.
## Claude Code
Add the server, then sign in:
```bash
claude mcp add --transport http --scope user tomba https://mcp.tomba.io/mcp
claude mcp login tomba
```
`claude mcp login` opens a browser window where you sign in to Tomba. You can also run `/mcp` in a session, select `tomba`, and authenticate from there.
To send your key and secret instead of signing in:
```bash
claude mcp add --transport http --scope user tomba https://mcp.tomba.io/mcp \
--header "X-Tomba-Key: $TOMBA_API_KEY" \
--header "X-Tomba-Secret: $TOMBA_SECRET_KEY"
```
The shell expands the variables when you run the command, so the configuration stores the values. `--scope user` saves the server in `~/.claude.json` for all your projects. The default scope, `local`, applies to the current project only, and `--scope project` writes a `.mcp.json` file meant to be committed, so don't use it with your secret.
To verify, run `claude mcp list`: `tomba` shows as connected. In a session, `/mcp` shows the server with 28 tools.
## ChatGPT
ChatGPT connects to remote MCP servers through developer mode, which is available on the web for Plus, Pro, Business, Enterprise, and Education accounts. It authenticates with OAuth and can't send API key headers.
1. Open **Settings → Security and login** and turn on **Developer mode**.
2. Open **ChatGPT Plugins**, select **+**, and create an app for the remote MCP server `https://mcp.tomba.io/mcp` with OAuth authentication. Sign in to Tomba and approve access.
3. In a conversation, choose **Developer mode** from the **+** menu and select the Tomba app.
ChatGPT asks you to confirm write actions before it runs them. Among the Tomba tools, only `create_flag` and `create_lead` change data.
To verify, open the app's details page from settings: it lists the tools. Use **Refresh** there to load changes after the server is updated.
## VS Code
Put the server in `.vscode/mcp.json` for one workspace, or run **MCP: Open User Configuration** from the Command Palette to add it for all workspaces:
```json
{
"servers": {
"tomba": {
"type": "http",
"url": "https://mcp.tomba.io/mcp"
}
}
}
```
When VS Code first connects, it opens a browser window where you sign in to Tomba.
To send your key and secret instead, declare them as inputs. VS Code asks for both values on first use and stores them securely:
```json
{
"inputs": [
{
"type": "promptString",
"id": "tomba-key",
"description": "Tomba API key",
"password": true
},
{
"type": "promptString",
"id": "tomba-secret",
"description": "Tomba secret",
"password": true
}
],
"servers": {
"tomba": {
"type": "http",
"url": "https://mcp.tomba.io/mcp",
"headers": {
"X-Tomba-Key": "${input:tomba-key}",
"X-Tomba-Secret": "${input:tomba-secret}"
}
}
}
}
```
To verify, run **MCP: List Servers**: `tomba` is running. In the Chat view, **Configure Tools** lists the 28 Tomba tools.
## Cursor
Add the server to `~/.cursor/mcp.json` for all projects, or to `.cursor/mcp.json` in a project:
```json
{
"mcpServers": {
"tomba": {
"url": "https://mcp.tomba.io/mcp",
"headers": {
"X-Tomba-Key": "${env:TOMBA_API_KEY}",
"X-Tomba-Secret": "${env:TOMBA_SECRET_KEY}"
}
}
}
}
```
Cursor replaces each `${env:…}` reference with an environment variable of the Cursor process. Set `TOMBA_API_KEY` and `TOMBA_SECRET_KEY` where Cursor inherits them, for example by starting Cursor from a terminal that has them, or write the values in the file.
To verify, open **Customize** in the Cursor sidebar: `tomba` is listed and enabled, with its tools.
## Windsurf
Windsurf is now called Devin Desktop. In the Cascade panel, open the **…** menu and select **Open MCP config file** in the MCPs section, then add:
```json
{
"mcpServers": {
"tomba": {
"serverUrl": "https://mcp.tomba.io/mcp",
"headers": {
"X-Tomba-Key": "${env:TOMBA_API_KEY}",
"X-Tomba-Secret": "${env:TOMBA_SECRET_KEY}"
}
}
}
}
```
`${env:…}` reads an environment variable. To sign in with OAuth instead, leave out `headers`.
This file configures the Cascade agent. The Devin Local agent, the default for new tabs, reads MCP servers from the Devin CLI configuration instead; see [Devin's MCP documentation](https://docs.devin.ai/desktop/cascade/mcp).
To verify, open the MCPs section of the **…** menu: it lists `tomba` with the number of tools it provides, 28.
## Zed
Run **zed: open settings file** from the command palette and add the server under `context_servers`:
```json
{
"context_servers": {
"tomba": {
"url": "https://mcp.tomba.io/mcp"
}
}
}
```
With no `Authorization` header configured, Zed signs you in with OAuth. To use your API key instead, add `"headers": { "Authorization": "Bearer " }`, with the token built as described in [API key as a bearer token](/llm/remote-mcp/introduction#api-key-as-a-bearer-token).
To verify, open **Settings → AI → MCP Servers**: the dot next to `tomba` is green, and its tooltip says "Server is active".
## Gemini CLI
Add the server for your user, then sign in from a Gemini CLI session:
```bash
gemini mcp add --transport http --scope user tomba https://mcp.tomba.io/mcp
```
In Gemini CLI, run `/mcp auth tomba` and sign in to Tomba in the browser window that opens.
To send your key and secret instead:
```bash
gemini mcp add --transport http -H "X-Tomba-Key: $TOMBA_API_KEY" -H "X-Tomba-Secret: $TOMBA_SECRET_KEY" --scope user tomba https://mcp.tomba.io/mcp
```
With `--scope user`, the server is saved in `~/.gemini/settings.json`; the default scope, `project`, uses `.gemini/settings.json` in the current directory. If you edit the file directly, write the entry the way `gemini mcp add` does, with `"type": "http"`. Older versions of Gemini CLI use `httpUrl` instead of `url` and `type`.
```json
{
"mcpServers": {
"tomba": {
"url": "https://mcp.tomba.io/mcp",
"type": "http"
}
}
}
```
To verify, run `gemini mcp list`: `tomba` shows as connected. In a session, `/mcp list` shows the server's tools.
## Cline
1. In the Cline panel, select the **MCP Servers** icon, then the **Remote Servers** tab.
2. Enter the server name `tomba` and the URL `https://mcp.tomba.io/mcp`, choose **Streamable HTTP**, and select **Add Server**.
3. Select **Configure MCP Servers** to open `cline_mcp_settings.json`, and add your key and secret as headers:
```json
{
"mcpServers": {
"tomba": {
"type": "streamableHttp",
"url": "https://mcp.tomba.io/mcp",
"headers": {
"X-Tomba-Key": "ta_xxxx",
"X-Tomba-Secret": "ts_xxxx"
}
}
}
}
```
Replace `ta_xxxx` and `ts_xxxx` with your key and secret. To verify, check that the Tomba tools appear under the server in the **MCP Servers** panel.
## Other clients
In any client that supports remote MCP servers, add a server with the Streamable HTTP transport (some clients call it HTTP) and the URL `https://mcp.tomba.io/mcp`. If the client supports MCP authorization, leave authentication empty and sign in when asked. Otherwise, send the `X-Tomba-Key` and `X-Tomba-Secret` headers, or `Authorization: Bearer` with the [encoded key and secret](/llm/remote-mcp/introduction#api-key-as-a-bearer-token).
## Troubleshooting
| Symptom | What to do |
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The client says the server needs authentication. | Finish the sign-in: `claude mcp login tomba` in Claude Code, `/mcp auth tomba` in Gemini CLI, or **Connect** on the Claude connector. With headers, check that both `X-Tomba-Key` and `X-Tomba-Secret` are set. |
| The tools are listed, but every call returns an authentication error. | The server only checks credentials when a tool calls the API. Check the key and secret, and whether the key has expired: see [Key expiry](/authentication#key-expiry). |
| `usage_info` and `get_logs` fail while other tools work. | You signed in with OAuth, which these two tools don't accept. Connect with your key and secret to use them. |
| Tool calls fail with a rate limit error. | Your plan's limit was reached; the server already retried twice. See [Handle 429 responses](/rate-limits#handle-429-responses). |
| You want to remove a client's access. | For OAuth, revoke the app under [Connected apps](https://app.tomba.io/settings/connected-apps). For headers, [rotate the key](/authentication#rotate-keys). |
---
## Document: Local MCP server
Run Tomba's MCP server on your own machine with npx: requirements, environment variables, stdio and HTTP transports, and client configuration.
URL: /llm/local-mcp/introduction
# Local MCP server
`@tomba-io/tomba-mcp-server` is an MCP server that runs on your machine and calls the Tomba API with your key and secret. It provides the same 28 tools as the remote server. The [remote MCP server](/llm/remote-mcp/introduction) needs no installation and also supports OAuth; [Local server differences](/llm/remote-mcp/tools#local-server-differences) lists how the two servers behave differently.
## Requirements
- Node.js 18.20 or later. Earlier versions stop at startup with `SyntaxError: Unexpected token 'with'`.
- A Tomba API key and secret. See [Get your key and secret](/authentication#get-your-key-and-secret).
## Run the server
Your MCP client usually starts the server for you, as shown in [Configure a client](#configure-a-client). To check that the package installs and runs, print its version:
```bash
npx -y @tomba-io/tomba-mcp-server --version
```
The server reads your credentials from two environment variables, and exits with `Error: TOMBA_API_KEY and TOMBA_SECRET_KEY environment variables are required` if either is missing:
| Variable | Value |
| ------------------ | -------------------- |
| `TOMBA_API_KEY` | Your API key, `ta_…` |
| `TOMBA_SECRET_KEY` | Your secret, `ts_…` |
It accepts these options:
| Option | Default | Description |
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| `--transport`, `-t` | `stdio` | `stdio` to talk to the client that started it, or `http` to serve Streamable HTTP. See [HTTP transport](#http-transport). |
| `--port`, `-p` | `3000` | Port for the HTTP transport |
| `--version`, `-v` | | Print the version and exit |
| `--help`, `-h` | | Print the options and exit |
## Configure a client
In each example, replace `ta_xxxx` and `ts_xxxx` with your key and secret. Keep configuration files that contain your secret out of version control.
### Claude Desktop
In Claude Desktop, open **Settings → Developer** and select **Edit Config**. The file is at `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS and `%APPDATA%\Claude\claude_desktop_config.json` on Windows. Add the server, then quit and restart Claude Desktop:
```json
{
"mcpServers": {
"tomba": {
"command": "npx",
"args": ["-y", "@tomba-io/tomba-mcp-server"],
"env": {
"TOMBA_API_KEY": "ta_xxxx",
"TOMBA_SECRET_KEY": "ts_xxxx"
}
}
}
}
```
The same `mcpServers` entry works in the configuration files of Cursor, Windsurf, Gemini CLI, and Cline; [Connect a client](/llm/remote-mcp/clients) says where each client keeps its file.
### Claude Code
```bash
claude mcp add --scope user tomba \
-e TOMBA_API_KEY="$TOMBA_API_KEY" \
-e TOMBA_SECRET_KEY="$TOMBA_SECRET_KEY" \
-- npx -y @tomba-io/tomba-mcp-server
```
### VS Code
Add the server to `.vscode/mcp.json`, or to the file that **MCP: Open User Configuration** opens. VS Code asks for the key and secret on first use and stores them securely:
```json
{
"inputs": [
{
"type": "promptString",
"id": "tomba-key",
"description": "Tomba API key",
"password": true
},
{
"type": "promptString",
"id": "tomba-secret",
"description": "Tomba secret",
"password": true
}
],
"servers": {
"tomba": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@tomba-io/tomba-mcp-server"],
"env": {
"TOMBA_API_KEY": "${input:tomba-key}",
"TOMBA_SECRET_KEY": "${input:tomba-secret}"
}
}
}
}
```
### Zed
Run **zed: open settings file** and add the server under `context_servers`:
```json
{
"context_servers": {
"tomba": {
"command": "npx",
"args": ["-y", "@tomba-io/tomba-mcp-server"],
"env": {
"TOMBA_API_KEY": "ta_xxxx",
"TOMBA_SECRET_KEY": "ts_xxxx"
}
}
}
}
```
## Verify the connection
1. The client lists 28 Tomba tools.
2. Ask the assistant to run the `list_flags` tool. A list of flags, even an empty one, means your credentials work.
When the server starts over stdio, it writes `Tomba MCP server running on stdio` to its error output. Claude Desktop saves that output in `mcp-server-tomba.log`, in `~/Library/Logs/Claude` on macOS and `%APPDATA%\Claude\logs` on Windows.
## HTTP transport
With `--transport http`, the server listens on the given port and serves Streamable HTTP at `/mcp`, with a health check at `/health`:
```bash
TOMBA_API_KEY="ta_xxxx" TOMBA_SECRET_KEY="ts_xxxx" \
npx -y @tomba-io/tomba-mcp-server --transport http --port 3000
```
Clients connect to `http://localhost:3000/mcp`. The server handles one MCP session per process: a second client, or a client that reconnects, gets HTTP `500` until you restart the server.
In HTTP mode, the server doesn't authenticate clients, listens on every
network interface, and accepts requests from any web page origin. Anyone who
can reach the port can use your Tomba credits. Run it only where the port is
closed to other machines, or use stdio.
## Troubleshooting
| Symptom | What to do |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SyntaxError: Unexpected token 'with'` at startup | Upgrade Node.js to 18.20 or later. The client runs the `node` found on its own `PATH`, which can differ from your terminal's. |
| `Error: TOMBA_API_KEY and TOMBA_SECRET_KEY environment variables are required` | Set both variables in the `env` block of the client configuration. |
| The tools are listed, but every call returns an authentication error | The server checks credentials only when a tool calls the API. Check the key and secret, and whether the key has expired: see [Key expiry](/authentication#key-expiry). |
| A call fails with a rate limit error | The local server doesn't retry failed requests. Wait, then call again; see [Handle 429 responses](/rate-limits#handle-429-responses). |
---
## Document: Use Tomba docs with AI assistants
Give an AI assistant the Tomba documentation as Markdown: the llms.txt index, llms-full.txt, a .md file for every page, and the API reference.
URL: /llm/editor/setup
# Use Tomba docs with AI assistants
The Tomba documentation is also published as Markdown files that AI assistants and coding tools can read. Give an assistant the files that cover your task when you ask it to write code against the Tomba API.
To let an assistant call Tomba instead of reading about it, connect the [remote MCP server](/llm/remote-mcp/introduction).
## Files
| File | Contents |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`llms.txt`](https://docs.tomba.io/llms.txt) | An index of the documentation: one line per page with its title, its description, and a link to its `.md` file. It doesn't contain the pages themselves. |
| [`llms-full.txt`](https://docs.tomba.io/llms-full.txt) | Every documentation page in one Markdown file. It doesn't include the API reference. |
| Page URL + `.md` | One page as Markdown. Append `.md` to the URL of any documentation page, for example [`https://docs.tomba.io/authentication.md`](https://docs.tomba.io/authentication.md). |
| [`openapi.md`](https://docs.tomba.io/openapi.md) | The API reference as Markdown: every endpoint with its parameters, request bodies, and response schemas. |
## Give the files to an assistant
1. Choose the files the task needs: the `.md` pages for the features you use, and `openapi.md` when the assistant needs exact parameters and response fields. Use `llms.txt` to find the right pages.
2. If the assistant can fetch URLs, paste the file URLs into your request. Otherwise, download the files and attach them, or add them to the files your tool keeps as project context.
3. Ask for the code. Name the endpoints you want and the language, and ask the assistant to send the key and secret in the `X-Tomba-Key` and `X-Tomba-Secret` headers, as described in [Authentication](/authentication).
`llms-full.txt` and `openapi.md` are large files, and many assistants can't load either one whole into a conversation. When that happens, give the assistant individual `.md` pages instead.
---
## Document: Apify
Run Tomba's actors on Apify without a Tomba account: costs, runs from the Console, the API or a schedule, and the real-time API.
URL: /integrations/apify/introduction
# Apify
Tomba publishes 15 actors on the [Apify Store](https://apify.com/tomba-io) under the `tomba-io` account. Each actor runs one Tomba lookup over a list of inputs and stores the results in a dataset; the [actor reference](/integrations/apify/actors) lists their inputs and outputs. Tomba builds and maintains the actors.
## Prerequisites
- An Apify account. You don't need a Tomba account or API key: the actors call Tomba with Tomba's own credentials.
- To start runs through the API or call the real-time API, an Apify API token, from **Settings > API & Integrations** in Apify Console.
## Costs
Apify charges your Apify account per event; the event price is on each actor's Store page. Tomba doesn't charge you anything.
- Each request charges one `tomba-request` event per Tomba credit it costs. The credits per request, including the extra credits for phone data, are the ones in [Credit costs](/usage-and-quotas#credit-costs). Technology finder charges one event per domain with technologies.
- A request is charged only when Tomba returns a result, including a negative answer such as an undeliverable verification or a domain with no email addresses.
- Error statuses, empty results, responses with an `errors` object, and results served from the actor's cache are free.
Each dataset item has a `charged` field, and `chargedCredits` where a request can cost more than one credit. To cap a run's cost, set **Max cost per run** in the run options; the actor stops when it reaches the cap.
## Run an actor
The actors run with limited permissions, so they need no approval before the first run.
- **Console:** open the actor, fill in the input form, and select **Start**. Results appear in the run's default dataset as they arrive, and you can export them from the run page.
- **API:** send the input as JSON to the actor's run endpoint, using the actor ID with a `~` in place of the `/`. See the [example](#example).
- **Schedule:** in **Schedules**, create a schedule with a cron expression and add the actor, or a saved task with its input. Apify schedules an actor only after it has run at least once.
- **Real-time API:** call the actor's Standby URL to get results in the HTTP response instead of a dataset. See [Real-time API](#real-time-api).
## Actors
| Actor | Actor ID | Calls |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------- |
| [Domain search](/integrations/apify/actors#domain-search) | [`tomba-io/domain-search`](https://apify.com/tomba-io/domain-search) | [`GET /v1/domain-search`](/api/finder#domain-search) |
| [Email finder](/integrations/apify/actors#email-finder) | [`tomba-io/email-finder`](https://apify.com/tomba-io/email-finder) | [`GET /v1/email-finder`](/api/finder#email-finder) |
| [Author finder](/integrations/apify/actors#author-finder) | [`tomba-io/author-finder`](https://apify.com/tomba-io/author-finder) | [`GET /v1/author-finder`](/api/finder#author-finder) |
| [Email enrichment](/integrations/apify/actors#email-enrichment) | [`tomba-io/email-enrichment`](https://apify.com/tomba-io/email-enrichment) | [`GET /v1/enrich`](/api/finder#email-enrichment) |
| [Email verifier](/integrations/apify/actors#email-verifier) | [`tomba-io/email-verifier`](https://apify.com/tomba-io/email-verifier) | [`GET /v1/email-verifier`](/api/verifier#email-verifier) |
| [LinkedIn finder](/integrations/apify/actors#linkedin-finder) | [`tomba-io/linkedin-finder`](https://apify.com/tomba-io/linkedin-finder) | [`GET /v1/linkedin`](/api/finder#linkedin-finder) |
| [Email count](/integrations/apify/actors#email-count) | [`tomba-io/email-count`](https://apify.com/tomba-io/email-count) | [`GET /v1/email-count`](/api/finder#email-count) |
| [Phone finder](/integrations/apify/actors#phone-finder) | [`tomba-io/phone-finder`](https://apify.com/tomba-io/phone-finder) | [`GET /v1/phone-finder`](/api/phone#phone-finder) |
| [Phone validator](/integrations/apify/actors#phone-validator) | [`tomba-io/phone-validator`](https://apify.com/tomba-io/phone-validator) | [`GET /v1/phone-validator`](/api/phone#phone-validator) |
| [Similar finder](/integrations/apify/actors#similar-finder) | [`tomba-io/similar-finder`](https://apify.com/tomba-io/similar-finder) | [`GET /v1/similar`](/api/domain#similar) |
| [Technology finder](/integrations/apify/actors#technology-finder) | [`tomba-io/technology-finder`](https://apify.com/tomba-io/technology-finder) | [`GET /v1/technology`](/api/domain#technology) |
| [Person enrichment](/integrations/apify/actors#person-enrichment) | [`tomba-io/clearbit-person`](https://apify.com/tomba-io/clearbit-person) | [`GET /v1/people/find`](/api/enrichment#person-api) |
| [Company enrichment](/integrations/apify/actors#company-enrichment) | [`tomba-io/clearbit-company`](https://apify.com/tomba-io/clearbit-company) | [`GET /v1/companies/find`](/api/enrichment#company-api) |
| [Combined enrichment](/integrations/apify/actors#combined-enrichment) | [`tomba-io/clearbit-combined`](https://apify.com/tomba-io/clearbit-combined) | [`GET /v1/combined/find`](/api/enrichment#combined-api) |
| [Company search](/integrations/apify/actors#company-search) | [`tomba-io/company-search`](https://apify.com/tomba-io/company-search) | [`POST /v1/reveal/search`](/api/reveal#search-companies) |
Person, Company, and Combined enrichment return Tomba's data in a Clearbit-compatible format, which is why their actor IDs start with `clearbit-`.
## Example
Search `stripe.com` with the Domain search actor and get the dataset in the same call:
```bash
curl -X POST "https://api.apify.com/v2/actors/tomba-io~domain-search/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domains": ["stripe.com"], "limit": "10"}'
```
The response is a JSON array with one item per domain, each holding `organization` and `emails`. This endpoint waits at most 300 seconds for the run. For longer runs, start the run with `POST /v2/actors/tomba-io~domain-search/runs` and read its dataset when it finishes.
## Real-time API
Every actor also runs in Apify Standby mode, an HTTP server that returns results in the response. Find the actor's Standby URL and its OpenAPI description on the actor's **API** tab, and authenticate with your Apify API token.
```bash
curl "https:///?domain=stripe.com" \
-H "Authorization: Bearer $APIFY_TOKEN"
```
- `GET /` with query parameters runs one lookup; each actor's query parameters are in the [actor reference](/integrations/apify/actors#real-time-api).
- `POST /` with the same JSON input as a run processes a list.
- The response is `{"items": [...]}`, with the same items as the dataset. Costs are the same as for a run.
## Troubleshooting
| Symptom | Cause | Fix |
| ------------------------------------------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Items have an `error` field with a Tomba error message | Tomba rejected the request, for example an invalid domain or email address. | Look up the message in [Errors](/error-handling#error-types). These items aren't charged. |
| The run stops before processing every input | The run reached **Max cost per run** or `maxResults`. | Raise the limit and run the input again. Lookups cached in the last 24 hours aren't charged again. |
| The run log shows `retrying` warnings | Tomba returned `429` or a `5xx` status, or the request timed out. | No action needed: the actor retries with backoff. Lower `maxConcurrency` if most requests are retried. |
| The run log shows `Cross-run cache is unavailable` | The actor couldn't open its cache store in your account. | No action needed: results are cached for that run only, and repeat lookups in later runs are charged again. |
| A real-time API call returns `400` | A query parameter or the JSON body is invalid. | Read the `error` field in the response. |
| `run-sync-get-dataset-items` returns `408` | The run took longer than 300 seconds. | Start the run asynchronously and read the dataset when it finishes. |
---
## Document: Apify actors
Reference for Tomba's Apify actors: inputs, run options, dataset items, charges, and real-time API parameters for each actor.
URL: /integrations/apify/actors
# Apify actors
Each actor calls one Tomba endpoint for every entry in its input list and writes the results to the run's default dataset. How to start a run and what a run costs are on [Apify](/integrations/apify/introduction).
## How the actors run
The actors call Tomba with Tomba's own credentials, so no input takes a Tomba API key and you don't need a Tomba account.
- **Parallel requests.** Up to `maxConcurrency` requests run at once, with no pacing between them. When Tomba's [rate limits](/rate-limits) answer `429`, the request is retried.
- **Retries.** A request that fails with `429`, a `5xx` status, a network error, or a timeout is retried up to `maxRetries` times, with exponential backoff starting at 1 second and capped at 30 seconds. A `Retry-After` header sets the wait instead. Other errors aren't retried; the input produces an item with an `error` field.
- **Cache.** Successful responses are stored in a named key-value store in your Apify account, `tomba-cache-`, one per actor, and reused for identical requests (same endpoint and parameters) for `cacheTtlHours`. A cached result costs nothing and has `cached: true`. If the store can't be opened, the run continues and caches results for that run only.
- **Resume.** Processed inputs are recorded in the `TOMBA_STATE` record of the run's default key-value store. When Apify migrates or restarts the run, it skips those inputs.
- **Max cost per run.** Before each request, the actor checks that the run's **Max cost per run** leaves room for the request's charge. When it doesn't, the actor starts no new requests, lets the running ones finish, and exits. Inputs it didn't reach produce no item.
- **Input clean-up.** Domains are trimmed, lowercased, and stripped of `http://`, `https://`, `www.`, and any path. Email addresses are trimmed and lowercased. Duplicate inputs are removed before any request.
- **Items as they arrive.** Each input's items are written to the dataset as soon as its request finishes, so an aborted run keeps the items it already produced.
### Run options
Every actor accepts these inputs, under **Run options** in the input form.
| Input | Type | Default | Description |
| ---------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `maxConcurrency` | integer, 1 to 50 | `10` | Requests that run at the same time. |
| `maxRetries` | integer, 0 to 10 | `3` | Retries after a `429`, a `5xx` status, a network error, or a timeout. |
| `useCache` | boolean | `true` | Reuse cached results. `false` sends every request to Tomba. |
| `cacheTtlHours` | integer, 0 to 720 | `24` | How long a cached result stays valid. `0` turns off the cross-run cache; results are reused within a run. |
### Charges
Apify charges the `tomba-request` event to your Apify account; the event price is on each actor's Store page.
- A request is charged when Tomba returns a JSON response with non-empty `data` and no `errors` object. A negative answer is a result, so an undeliverable verification or a domain with no email addresses is charged. The "Charged when" column in [Credit costs](/usage-and-quotas#credit-costs) doesn't apply to the actors.
- A charged request costs one event per Tomba credit in [Credit costs](/usage-and-quotas#credit-costs), including the extra credits for phone data. Technology finder charges one event per domain with technologies, although the endpoint itself costs no credits.
- Error statuses, responses with empty or `null` data, responses with an `errors` object, responses that aren't JSON, and cached results aren't charged.
Every item has `charged` and `cached`. Items from actors whose requests can cost more than one credit also have `chargedCredits`, the events charged for the item, and actors that return phone data add `phoneNumbers`, the number of phone numbers in the item.
## Real-time API
Every actor also runs in Apify Standby mode, an HTTP server that returns the items in the response instead of a dataset. Find the actor's Standby URL on its **API** tab in Apify, and send your Apify API token.
```bash
curl "https:///?email=jane.doe@stripe.com" \
-H "Authorization: Bearer $APIFY_TOKEN"
curl -X POST "https:///" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"emails": ["jane.doe@stripe.com", "john.doe@shopify.com"]}'
```
- `GET /` with query parameters builds the input from them; each actor's parameters are listed in its section. A list parameter can be repeated (`?domain=stripe.com&domain=shopify.com`) or comma-separated (`?domains=stripe.com,shopify.com`). Boolean parameters take `true`, `false`, `1`, `0`, `yes`, or `no`.
- `POST /` takes the same JSON input as a run, up to 1 MB.
- `GET /` without query parameters returns `{"status": "ready", "usage": "…"}`.
- The response is `{"items": [...]}`, with the same items a run writes to its dataset. Charges are the same as for a run.
| Status | When |
| ------ | ---------------------------------------------------------------------------------------- |
| `200` | The request was processed. Inputs without a result are items with an `error` field. |
| `400` | A query parameter, the JSON body, or the input is invalid. The `error` field says which. |
| `402` | The Standby run reached its maximum charge. Every later request gets this status. |
| `404` | The path isn't `/`. |
| `405` | The method isn't `GET` or `POST`. |
Run options can't be set per request: every request uses the defaults in [Run options](#run-options). Standby requests don't use the resume state.
## Domain search
[`tomba-io/domain-search`](https://apify.com/tomba-io/domain-search) calls [`GET /v1/domain-search`](/api/finder#domain-search) for each domain or company name, one results page at a time. Searches run in parallel; the pages of one search are requested one after another, starting at `page`, until the actor has `maxEmailsPerDomain` addresses, a page returns fewer than `limit` addresses, or the last page is reached.
| Input | Type | Required | Default | Description |
| -------------------- | ------------------------------ | ------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `domains` | array of strings | `domains` or `companies` | — | Domains to search, such as `stripe.com`. |
| `companies` | array of strings | `domains` or `companies` | — | Company names to search, such as `Stripe`. Tomba resolves each name to a domain. |
| `maxEmailsPerDomain` | integer, 1 to 100 | No | `10` | Addresses to collect per domain or company name. |
| `limit` | string: `10`, `20`, or `50` | No | `10` | Addresses per page. Each page is charged by its `limit`, so a `limit` above `maxEmailsPerDomain` pays for addresses the actor drops. |
| `page` | integer, 1 or more | No | `1` | First page to request. |
| `department` | string | No | — | Return only people in one department, such as `engineering` or `sales`. The input form lists the accepted values. |
| `country` | string | No | — | Two letters, such as `US` or `gb`; lowercase is converted to uppercase. Returns only people in that country. |
| `enrichMobile` | boolean | No | `false` | Return each person's phone numbers in `emails[].phone_data`. Addresses with phone data cost extra; see [Credit costs](/usage-and-quotas#credit-costs). |
| `webhookUrl` | string | No | — | URL starting with `http://` or `https://`, sent to Tomba as `webhook_url` on every page request; see [Webhooks](/webhook). Cached results aren't sent again. |
| `includeCompanyInfo` | boolean | No | `true` | Include `organization` in the item. |
| `outputFormat` | string: `detailed` or `simple` | No | `detailed` | Saved in the `SUMMARY` record. It doesn't change the output. |
Each domain or company name produces one item with the input (`domain` or `company`), `organization` (see [Company](/attributes/company)), `emails` (see [Person](/attributes/person)), `meta` from the last page, `pages`, `chargedRequests` (pages charged), `chargedCredits`, `phoneNumbers`, `charged`, and `cached` (`true` when every page came from the cache). For a company name, the item also has the `domain` Tomba resolved, when the response reports one. When the first page returns no result or fails, the item has the input and an `error` field; when a later page does, the item keeps the addresses already collected. Batch runs also save the input counts, request counts, and settings as a `SUMMARY` record in the default key-value store.
Real-time API parameters: `domain`, `domains`, `company`, `companies`, `maxEmailsPerDomain`, `includeCompanyInfo`, `page`, `limit`, `department`, `country`, `enrichMobile`, `webhookUrl`.
## Email finder
[`tomba-io/email-finder`](https://apify.com/tomba-io/email-finder) calls [`GET /v1/email-finder`](/api/finder#email-finder) once per request object.
| Input | Type | Required | Default | Description |
| -------------- | ---------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `requests` | array of objects, 1 to 1,000 | Yes | — | Each object has `domain` or `company` (a company name), and `fullName` or both `firstName` and `lastName`. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Requests to process. Requests after this number are skipped. |
| `enrichMobile` | boolean | No | `false` | Return the person's phone numbers in `phone_data`. A result with phone data costs extra; see [Credit costs](/usage-and-quotas#credit-costs). |
| `webhookUrl` | string | No | — | URL starting with `http://` or `https://`, sent to Tomba as `webhook_url`; see [Webhooks](/webhook). |
Each request produces one item: the response's `data` fields (see [Person](/attributes/person)) plus the input as `domain`, `firstName`, `lastName`, `fullName`, and `inputCompany` (the input `company`, since `company` is a response field), `source` set to `tomba_email_finder`, `phoneNumbers`, `chargedCredits`, `charged`, and `cached`. When no address is found or the request fails, the item has the input fields and an `error` field instead of the data fields. A request without the required fields isn't sent to Tomba and produces an item with an `error` field.
Real-time API parameters: `domain`, `company`, `firstName` or `first_name`, `lastName` or `last_name`, `fullName` or `full_name`, `enrichMobile`, `webhookUrl`, `maxResults`. A `GET` request makes one search.
## Author finder
[`tomba-io/author-finder`](https://apify.com/tomba-io/author-finder) calls [`GET /v1/author-finder`](/api/finder#author-finder) once per URL.
| Input | Type | Required | Default | Description |
| ------------ | ------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------- |
| `urls` | array of strings | Yes | — | Article URLs. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | URLs to process. URLs after this number are skipped. |
| `webhookUrl` | string | No | — | URL starting with `http://` or `https://`, sent to Tomba as `webhook_url`; see [Webhooks](/webhook). |
Each URL produces one item. When an author is found, it is the response's `data` object (see [Author finder](/attributes/person#author-finder)) plus `input_url`, `source` set to `tomba_author_finder`, `chargedCredits`, `charged`, and `cached`. When no author is found or the request fails, the item has `input_url`, `email` set to `null`, and an `error` field.
Real-time API parameters: `url`, `urls`, `webhookUrl`, `maxResults`.
## Email enrichment
[`tomba-io/email-enrichment`](https://apify.com/tomba-io/email-enrichment) calls [`GET /v1/enrich`](/api/finder#email-enrichment) once per email address.
| Input | Type | Required | Default | Description |
| -------------- | ---------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `emails` | array of strings, 1 to 1,000 | Yes | — | Email addresses to enrich. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Addresses to process. Addresses after this number are skipped. |
| `enrichMobile` | boolean | No | `false` | Return the person's phone numbers in `phone_data`. A result with phone data costs extra; see [Credit costs](/usage-and-quotas#credit-costs). |
| `webhookUrl` | string | No | — | URL starting with `http://` or `https://`, sent to Tomba as `webhook_url`; see [Webhooks](/webhook). |
Each address produces one item: the response's `data` fields (see [Person](/attributes/person)) plus `email`, `source` set to `tomba_email_enrichment`, `phoneNumbers`, `chargedCredits`, `charged`, and `cached`. When no person is found or the request fails, the item has `email`, `source`, and an `error` field instead of the data fields.
Real-time API parameters: `email`, `emails`, `enrichMobile`, `webhookUrl`, `maxResults`.
## Email verifier
[`tomba-io/email-verifier`](https://apify.com/tomba-io/email-verifier) calls [`GET /v1/email-verifier`](/api/verifier#email-verifier) once per email address.
| Input | Type | Required | Default | Description |
| -------------- | ---------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `emails` | array of strings, 1 to 1,000 | Yes | — | Email addresses to verify. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Stop after this many addresses have a verification result. Items with an `error` field don't count, and requests already running still produce items. |
| `enrichMobile` | boolean | No | `false` | Return the phone numbers linked to the address in `phone_data`. Each phone number costs extra; see [Credit costs](/usage-and-quotas#credit-costs). |
| `webhookUrl` | string | No | — | URL starting with `http://` or `https://`, sent to Tomba as `webhook_url`; see [Webhooks](/webhook). |
Each address produces one item with the response's `email` object (see [Email verifier attributes](/attributes/verifier)), `sources`, `input` (the address you sent), `phoneNumbers`, `chargedCredits`, `charged`, and `cached`; with `enrichMobile`, also `phone_data`. When the request fails or returns no data, the item has `input` and an `error` field.
Real-time API parameters: `email`, `emails`, `enrichMobile`, `webhookUrl`, `maxResults`.
## LinkedIn finder
[`tomba-io/linkedin-finder`](https://apify.com/tomba-io/linkedin-finder) calls [`GET /v1/linkedin`](/api/finder#linkedin-finder) once per profile URL.
| Input | Type | Required | Default | Description |
| -------------- | ----------------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `linkedinUrls` | array of unique strings, 1 to 1,000 | Yes | — | Profile URLs that contain `linkedin.com/in/`, such as `https://www.linkedin.com/in/jane-doe`, `https://linkedin.com/in/jane-doe`, or `https://uk.linkedin.com/in/jane-doe?trk=public`. Company pages are rejected. |
| `maxResults` | integer, 1 to 10,000 | No | `50` | URLs to process. URLs after this number are skipped. |
| `enrichMobile` | boolean | No | `false` | Return the profile's phone numbers in `phone_data`. A result with phone data costs extra; see [Credit costs](/usage-and-quotas#credit-costs). |
| `full` | boolean | No | `false` | Return every address Tomba has stored for the profile instead of the best one, one item per address. Each address is charged; see [Credit costs](/usage-and-quotas#credit-costs). |
| `webhookUrl` | string | No | — | URL starting with `http://` or `https://`, sent to Tomba as `webhook_url`; see [Webhooks](/webhook). |
When an address is found, each address produces one item: the response's data (see [Person](/attributes/person)) plus `linkedin_url`, `source` set to `tomba_linkedin_finder`, `phoneNumbers`, `chargedCredits`, `charged`, and `cached`. With `full`, the request's charge is split across its items' `chargedCredits`. When no address is found or the request fails, the item has `email` set to an empty string, `linkedin_url`, `source`, and an `error` field.
Real-time API parameters: `url`, `urls`, `linkedinUrl`, `linkedinUrls`, `enrichMobile`, `full`, `webhookUrl`, `maxResults`.
## Phone finder
[`tomba-io/phone-finder`](https://apify.com/tomba-io/phone-finder) calls [`GET /v1/phone-finder`](/api/phone#phone-finder) once per search object.
| Input | Type | Required | Default | Description |
| ------------ | ------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `searches` | array of objects | Yes | — | Each object has `email`, `domain`, or `linkedin` (a LinkedIn profile URL). All fields in an object are sent together, and a search that includes `domain` costs fewer credits; see [Credit costs](/usage-and-quotas#credit-costs). Objects with none of the three fields are skipped without an item. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Stop after this many valid phone numbers are found. Requests already running still produce items. |
| `full` | boolean | No | `false` | Sent to Tomba as `full=true` on every search. |
| `webhookUrl` | string | No | — | URL starting with `http://` or `https://`, sent to Tomba as `webhook_url` on every search; see [Webhooks](/webhook). |
Each search produces one item: the input `email`, `domain`, and `linkedin` (`null` when not set), the response's `data` fields (see [Phone](/attributes/phone)), `source`, `chargedCredits`, `charged`, and `cached`. Here `source` is an object with `search_type` (`email`, `linkedin`, or `domain`, in that order of precedence) and `search_value`. When the number isn't valid, the item also has an `error` field. When the request fails or returns no data, the item has the input fields, `valid` set to `null`, and an `error` field.
Real-time API parameters: `email`, `domain`, `linkedin`, `full`, `webhookUrl`, `maxResults`. A `GET` request makes one search.
## Phone validator
[`tomba-io/phone-validator`](https://apify.com/tomba-io/phone-validator) calls [`GET /v1/phone-validator`](/api/phone#phone-validator) once per number.
| Input | Type | Required | Default | Description |
| -------------- | ---------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `phoneNumbers` | array of objects, 1 to 1,000 | Yes | — | Each object has `phoneNumber` (10 to 20 characters, required) and `countryCode` (two letters, such as `GB`). Set `countryCode` for numbers written without a `+` country prefix, or the request fails. Numbers with the same digits and country code are validated once. |
| `maxResults` | integer, 1 to 10,000 | No | `50` | Stop after this many numbers have a validation result. Requests already running still produce items. |
Each number produces one item with `phone_number` (your input), `input_country_code`, `source` set to `tomba_phone_validator`, `charged`, and `cached`, plus these fields from the response (see [Phone](/attributes/phone)): `valid`, `country_code`, `carrier`, `line_type`, `national_format` (from `local_format`), `international_format` (from `intl_format`), `e164_format`, `rfc3966_format`, `location` (the region's `name`), and `timezone` (from `timezones`). The `country_name` column stays empty because the response has no country name. When the request fails or returns no data, the item has `valid` set to `false` and an `error` field.
Real-time API parameters: `phone` (repeated or comma-separated; a leading `+` left unencoded in the URL is accepted), `countryCode` or `country_code` (applies to every number), `maxResults`.
## Email count
[`tomba-io/email-count`](https://apify.com/tomba-io/email-count) calls [`GET /v1/email-count`](/api/finder#email-count) once per domain.
| Input | Type | Required | Default | Description |
| ------------ | ------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `domains` | array of strings | Yes | — | Domains to count addresses for, such as `stripe.com`. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Stop after this many domains have a count. Items with an `error` field don't count, and requests already running still produce items. |
Each domain produces one item: the response's `data` fields (see [Email count](/attributes/email-count)) plus `domain`, `source` set to `tomba_email_count`, `charged`, and `cached`. When the request fails or returns no data, the item has `domain`, `source`, and an `error` field.
Real-time API parameters: `domain`, `domains`, `maxResults`.
## Similar finder
[`tomba-io/similar-finder`](https://apify.com/tomba-io/similar-finder) calls [`GET /v1/similar`](/api/domain#similar) once per domain and reads the first page of results.
| Input | Type | Required | Default | Description |
| ------------ | ----------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `domains` | array of unique strings, 1 to 1,000 | Yes | — | Domains, such as `stripe.com`, `bbc.co.uk`, or `blog.stripe.com`. URLs such as `https://www.stripe.com/pricing` are reduced to the domain. |
| `maxResults` | integer, 1 to 10,000 | No | `50` | Maximum number of items. A domain's results are cut at the limit, and a domain whose request finishes after the limit is reached produces no item. |
Each similar domain produces one item with `input_domain`, `similar_domain` and `website_url` (both the similar company's domain), `company_name`, `industries`, `source` set to `tomba_similar_finder`, `charged`, and `cached` (see [Similar domains](/attributes/similar)). A domain with no results, or a failed request, produces one item with `similar_domain` set to `null` and an `error` field.
Real-time API parameters: `domain`, `domains`, `maxResults`.
## Technology finder
[`tomba-io/technology-finder`](https://apify.com/tomba-io/technology-finder) calls [`GET /v1/technology`](/api/domain#technology) once per domain.
| Input | Type | Required | Default | Description |
| ------------ | ----------------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `domains` | array of unique strings, 1 to 1,000 | Yes | — | Domains, such as `stripe.com`, `bbc.co.uk`, or `blog.stripe.com`. URLs such as `https://www.stripe.com/pricing` are reduced to the domain. |
| `maxResults` | integer, 1 to 10,000 | No | `50` | Maximum number of items. A domain's results are cut at the limit, and a domain whose request finishes after the limit is reached produces no item. |
Each technology produces one item with `input_domain`, `technology_slug`, `technology_name`, `technology_icon`, `technology_website`, `technology_description`, `category_id`, `category_slug`, and `category_name` (from the first category), `categories` (the names of all its categories), `source` set to `tomba_technology_finder`, `charged`, and `cached` (see [Technology](/attributes/technology)). A domain with no technologies, or a failed request, produces one item with `technology_name` set to `null` and an `error` field.
Real-time API parameters: `domain`, `domains`, `maxResults`.
## Person enrichment
[`tomba-io/clearbit-person`](https://apify.com/tomba-io/clearbit-person) calls [`GET /v1/people/find`](/api/enrichment#person-api) once per email address. The response comes from Tomba's data, in a Clearbit-compatible format.
| Input | Type | Required | Default | Description |
| ------------ | ------------------- | -------- | ------- | -------------------------------------------------------------- |
| `emails` | array of strings | Yes | — | Email addresses to enrich. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Addresses to process. Addresses after this number are skipped. |
Each address produces one item. With a match, it is the response's `data` object (see [Person fields](/attributes/enrichment#person-fields)) plus `email`, `source` set to `tomba_person_enrichment`, `charged`, and `cached`. Without a match, or when the request fails, the item has `email`, `source`, and an `error` field.
Real-time API parameters: `email`, `emails`, `maxResults`.
## Company enrichment
[`tomba-io/clearbit-company`](https://apify.com/tomba-io/clearbit-company) calls [`GET /v1/companies/find`](/api/enrichment#company-api) once per domain. The response comes from Tomba's data, in a Clearbit-compatible format.
| Input | Type | Required | Default | Description |
| ------------ | ------------------- | -------- | ------- | ---------------------------------------------------------- |
| `domains` | array of strings | Yes | — | Company domains, such as `stripe.com`. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Domains to process. Domains after this number are skipped. |
Each domain produces one item. With a match, it is the response's `data` object (see [Company fields](/attributes/enrichment#company-fields)) plus `domain`, `source` set to `tomba_company_enrichment`, `charged`, and `cached`. Without a match, or when the request fails, the item has `domain`, `source`, and an `error` field.
Real-time API parameters: `domain`, `domains`, `maxResults`.
## Combined enrichment
[`tomba-io/clearbit-combined`](https://apify.com/tomba-io/clearbit-combined) calls [`GET /v1/combined/find`](/api/enrichment#combined-api) once per email address and returns the person and their company together. The response comes from Tomba's data, in a Clearbit-compatible format.
| Input | Type | Required | Default | Description |
| ------------ | ------------------- | -------- | ------- | -------------------------------------------------------------- |
| `emails` | array of strings | Yes | — | Email addresses to enrich. |
| `maxResults` | integer, 1 to 1,000 | No | `50` | Addresses to process. Addresses after this number are skipped. |
Each address produces one item. With a match, it is the response's `data` object, with `person` and `company` (see [Combined fields](/attributes/enrichment#combined-fields)), plus `email`, `source` set to `tomba_enrichment`, `chargedCredits`, `charged`, and `cached`. Without a match, or when the request fails, the item has `email`, `source`, and an `error` field.
Real-time API parameters: `email`, `emails`, `maxResults`.
## Company search
[`tomba-io/company-search`](https://apify.com/tomba-io/company-search) calls [`POST /v1/reveal/search`](/api/reveal#search-companies) once per results page. Pages are requested one after another, starting at `page`, and each page with companies is charged one event. Paging stops when the actor has `maxResults` companies, reaches the last page, or gets a page shorter than the page size, an empty page, or an error.
| Input | Type | Required | Default | Description |
| ------------ | --------------------- | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `query` | string | `query` or `filters` | — | Natural-language search, such as `software companies in Berlin`. |
| `filters` | object | `query` or `filters` | — | Filters keyed by `company`, `location_country`, `location_city`, `location_state`, `industry`, `size`, `type`, `keywords`, `founded`, `technologies`, `similar`, `revenue`, `sic`, or `naics`, each with `include` and `exclude` arrays. See the [endpoint reference](/api/reveal#search-companies) for accepted values. |
| `source` | array of strings | No | — | Company fields to return. Empty returns all fields. |
| `page` | integer, 1 or more | No | `1` | First results page to request. |
| `maxResults` | integer, 1 to 100,000 | No | `100` | Maximum number of companies. |
Each company produces one item with the company fields from the response, `source` set to `tomba_company_search`, `charged`, and `cached`. When the first page returns no companies or fails, the run produces one item with `query`, `page`, and an `error` field; when a later page does, the actor keeps the companies it already has. Batch runs record their progress in the `COMPANY_SEARCH_PROGRESS` record of the default key-value store, so a migrated or restarted run continues from the next page.
Real-time API parameters: `query`, `filters` (a JSON object, URL-encoded), `source` (repeated or comma-separated), `page`, `maxResults`.
- [Tomba OpenAPI specification](/openapi.md): The complete API specification as Markdown.