# 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/<profile>`. 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.                                                                                                                                                                                 |

<Callout type="caution">
    `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).
</Callout>

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