# B2B Contact Enrichment API - Email, Company, Job Title (`nabeelbaghoor/b2b-contact-enrichment-api`) Actor

Enrich people and companies, or find new contacts at a company by seniority and department. Returns work email, phone, job title, seniority, department, profile URL, previous employer, plus company size, revenue, industry and address. Pay per result.

- **URL**: https://apify.com/nabeelbaghoor/b2b-contact-enrichment-api.md
- **Developed by:** [Nabeel Hassan](https://apify.com/nabeelbaghoor) (community)
- **Categories:** Business, Developer tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$12.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## B2B Contact Enrichment API - Email, Company, Job Title

Fill in the people and companies you already have, or find the ones you do not. One flat row per record, or per contact found.

### What this actor does

- **Does the asynchronous part properly.** Enrichment at this provider is a job, not a lookup: submitting a record answers with an id and a status of scheduled, and nothing else. Reading only that response is the easiest mistake to make against this API and it writes every record up as empty. This actor submits, waits, and fetches the finished record, so what lands in the dataset is the data rather than the receipt.
- **Submits in a wave rather than one at a time.** A job takes far longer than a request does. Everything goes out first and is then polled together, so the run is not sitting idle waiting for record one before it sends record two.
- **Finds people you did not have.** Contact discovery takes a company and returns up to ten people at it, filtered by seniority band and department, using the provider's own vocabularies. The inclusive bands are handled properly: VP and above means C-level and VP, director and above adds directors, and so on.
- **Stops the records that cannot resolve before they cost anything.** Each enrichment type has its own minimum: a person needs a profile URL, or a first and last name together with something identifying the company. A record short of that spends a request and returns nothing, so it is reported here with the reason instead of being sent.
- **Keeps the provider's own field names.** The columns are `contact_email`, `org_annual_revenue`, `contact_seniority` and the rest, exactly as the provider's documentation, dashboard and webhook write them. Reconciling a run against an invoice is a great deal easier when nothing has been renamed for tidiness.
- **Never loses a job to a short wait.** If the wait runs out, the requests are still running on the provider's side. The run prints their ids and marks the rows, and the list mode fetches them later.
- **Reads your own account back.** List mode returns the enrichment requests already run, filtered by id, status, type or creation date, so a run that timed out yesterday can be collected today at no extra cost at the provider. Usage mode returns the call counts per endpoint.
- **Paces itself under the documented ceiling.** The limit is fifty requests a minute and polling counts against it, so the default pace sits below the limit rather than at it.
- **Charges nothing for a miss.** Only rows carrying data are billed. Usage rows are never billed.

### Input

| Field | What it does |
| --- | --- |
| `mode` | Enrich contacts, enrich companies, find contacts, list requests, usage. |
| `records` | One object per record, using the provider's own field names. |
| `contactLinkedinUrls` | Shortcut: one profile URL per line. |
| `companyDomains` | Shortcut: one company website per line. |
| `apiKey` | Your own API key. |
| `contactsPerCompany` | How many people to find per company, one to ten. |
| `seniority` | Which seniority band to find. |
| `department` | Which function to find. |
| `filterRequestId` | List mode: one request by id. |
| `filterStatus` | List mode: scheduled, in progress, erred, cancelled, ok. |
| `filterEnrichmentType` | List mode: contact, account, get\_contacts. |
| `filterCreatedFrom` / `filterCreatedTo` | List mode: creation date range. |
| `requireEmail` / `requirePhone` / `requireLinkedin` | Keep only rows carrying that field. |
| `pollTimeoutSeconds` | How long to wait for the jobs to finish. |
| `skipNotFound` | Leave misses out of the dataset. |
| `baseUrl` | API host override. |
| `maxResults` | Row ceiling, and therefore the cost ceiling. |
| `requestsPerMinute` | Pacing, under the documented fifty a minute. |

### Example output

```json
{
  "mode": "getContacts",
  "enrichmentType": "get_contacts",
  "requestId": 4,
  "requestCreated": "2026-04-07T18:36:34Z",
  "requestStatus": "OK",
  "requestProgress": 100,
  "inputSummary": "org_website=example.com",
  "found": true,
  "notFoundReason": null,
  "contactIndex": 1,
  "contact_first_name": "John",
  "contact_last_name": "Doe",
  "contact_job_title": "VP Engineering",
  "contact_seniority": "VP",
  "contact_department": "Engineering",
  "contact_email": "doe@example.com",
  "contact_phone_number": null,
  "contact_linkedin_url": "linkedin.com/in/johndoe",
  "contact_previous_company": "Example Systems",
  "org_company_name": "Example Inc",
  "org_website": "example.com",
  "org_industry": "Software",
  "org_num_employees": "201-500",
  "org_annual_revenue": "$50M-$100M",
  "org_city": "Oakland",
  "org_state": "CA",
  "org_country": "United States"
}
```

### FAQ

#### Why does this actor wait instead of returning immediately?

Because the API is asynchronous. A submission is accepted and queued, and the response says so: an id, a creation time and a status of scheduled. The enriched values are attached to that request later and have to be fetched by id. An integration that reads the submission response gets a row of nulls for every record, every time, and nothing about the response makes that obvious. The waiting is the work.

#### What happens if the wait runs out before my records finish?

Nothing is lost. The requests carry on running at the provider, the run prints the ids that were still going, and those rows are marked as unfinished rather than as empty. Run the list mode afterwards, filtered by status or by creation date, and the finished records come back without being submitted again.

#### What is the minimum I have to supply for a record to work?

For a person: either a professional profile URL on its own, or a first name and a last name together with one of the company website, the company profile URL or the company name. For a company: at least one of the website, the profile URL or the company name. Records short of that are reported with the reason and not sent, because sending them spends a request and returns nothing.

#### How do the seniority bands work?

Three of the values are inclusive bands rather than single levels. VP and above covers C-level and VP. Director and above covers C-level, VP and Director. Manager and above adds Manager to those. The remaining values, C-level, VP, Director, Manager, Senior and Entry level, mean exactly the level named and nothing else.

#### Why are the column names not tidied up?

Because they are the provider's own names, and they are what its documentation, its dashboard sample code and its webhook payloads all use. Keeping them means a row here can be matched against a row there without a translation table, which matters most at exactly the moment you care about it: reconciling what you were charged for against what you received.

#### How many rows does finding contacts produce?

One per contact, not one per company. A run over fifty companies asking for five contacts each can produce up to two hundred and fifty rows, and each one is a unit of work at the provider as well as a row here. The contacts-per-company setting is the multiplier to watch.

#### Does the list mode cost anything?

It reads requests your account has already run, so it does not create new enrichment work at the provider. Rows carrying data are still charged for on this side, because they are results delivered. Usage rows are never charged for at all.

#### Can I run this without an API key?

No, and the actor will tell you so rather than failing. Every call is made with your own account's key, which you paste into the API key field or set as a secret. This provider issues keys on an API subscription, so a key that authenticates for the dashboard is not automatically one that authenticates here.

### Keyword map

B2B contact enrichment API, company enrichment API, work email finder API, contact data append, find contacts at a company, seniority and department filter, job title enrichment, firmographic enrichment API, lead enrichment automation, CRM data append, account enrichment, prospect list building API, revenue and headcount data, professional profile enrichment, asynchronous enrichment API.

# Actor input Schema

## `mode` (type: `string`):

The three enrichment modes send records and wait for the answers. Find contacts is the only one that returns people you did not already know about. The last two read your own account: what you have already run, and how many calls it has used.

## `records` (type: `array`):

One object per record, using this provider's own field names. Company keys are org\_company\_name, org\_website, org\_linkedin\_url, org\_city, org\_state, org\_country, org\_street, org\_zip\_code, org\_phone, org\_industry, org\_annual\_revenue, org\_num\_employees, org\_num\_employees\_exact and org\_record\_id. Person keys are contact\_first\_name, contact\_last\_name, contact\_linkedin\_url, contact\_email, contact\_job\_title, contact\_department, contact\_seniority and contact\_record\_id.

## `contactLinkedinUrls` (type: `array`):

A shortcut for contact enrichment: one professional profile URL per line. A profile URL on its own is enough to identify a person at this provider, which is why it is the easiest way in.

## `companyDomains` (type: `array`):

A shortcut for company enrichment and for finding contacts: one company website per line.

## `apiKey` (type: `string`):

Your own API key for this provider. The actor sends it in the header as the word Token followed by the key, which is what this provider expects rather than a bearer token. Bring your own key: nothing is shared between runs and the key is never written to the dataset.

## `contactsPerCompany` (type: `integer`):

In find contacts mode, how many people to return for each company, from one to ten. Every contact found is a row here and a unit of work at the provider.

## `seniority` (type: `string`):

In find contacts mode, how senior the people should be. The three values ending in a plus sign are inclusive bands: VP+ covers C-level and VP, Director+ adds Director, Manager+ adds Manager. The rest are exactly the level named.

## `department` (type: `string`):

In find contacts mode, which function the people should sit in. These are the provider's own department values and anything else is refused.

## `filterRequestId` (type: `string`):

In list mode, fetch one specific enrichment request by its id.

## `filterStatus` (type: `string`):

In list mode, keep only requests in this state. Only requests that finished as ok carry data.

## `filterEnrichmentType` (type: `string`):

In list mode, keep only requests of this kind.

## `filterCreatedFrom` (type: `string`):

In list mode, the earliest creation date to include. Either a date such as 2026-01-02 or a full timestamp such as 2026-01-03T18:03:11Z.

## `filterCreatedTo` (type: `string`):

In list mode, the latest creation date to include, in the same two forms.

## `requireEmail` (type: `boolean`):

Drops people the provider returned without a work email.

## `requirePhone` (type: `boolean`):

Drops people the provider returned without a phone number.

## `requireLinkedin` (type: `boolean`):

Drops people the provider returned without a professional profile URL.

## `pollTimeoutSeconds` (type: `integer`):

Enrichment here is asynchronous: records are submitted, then fetched once they finish. This is how long the run waits before giving up on the ones still running. Nothing is lost when it runs out; the requests keep going at the provider and the list mode fetches them later by id.

## `skipNotFound` (type: `boolean`):

Records that resolved to nothing, were refused, or had not finished when the wait ran out are written as rows with the reason, so you can see what happened to every input. Turn this on to keep only the ones that carry data. Misses are never charged for either way.

## `baseUrl` (type: `string`):

The host serving the API. Change it only if your account is served from somewhere other than the default.

## `maxResults` (type: `integer`):

Stop after this many rows. Rows are charged individually, so this is the ceiling on what a run can cost.

## `requestsPerMinute` (type: `integer`):

How fast to call the provider. The documented ceiling is fifty a minute and polling counts towards it, so the default sits below the limit rather than at it.

## Actor input object example

```json
{
  "mode": "enrichContact",
  "records": [],
  "contactLinkedinUrls": [],
  "companyDomains": [],
  "contactsPerCompany": 1,
  "requireEmail": false,
  "requirePhone": false,
  "requireLinkedin": false,
  "pollTimeoutSeconds": 300,
  "skipNotFound": false,
  "baseUrl": "https://leadgenius.com",
  "maxResults": 100,
  "requestsPerMinute": 40
}
```

# Actor output Schema

## `results` (type: `string`):

One row per record enriched or contact found, using the provider's own field names, alongside the request id and its terminal status.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "records": [],
    "contactLinkedinUrls": [],
    "companyDomains": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("nabeelbaghoor/b2b-contact-enrichment-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "records": [],
    "contactLinkedinUrls": [],
    "companyDomains": [],
}

# Run the Actor and wait for it to finish
run = client.actor("nabeelbaghoor/b2b-contact-enrichment-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "records": [],
  "contactLinkedinUrls": [],
  "companyDomains": []
}' |
apify call nabeelbaghoor/b2b-contact-enrichment-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nabeelbaghoor/b2b-contact-enrichment-api"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/xS8YTeogq6LL5HAFX/builds/Hm5wP2MEc3DuM4ljq/openapi.json
