# B2B Contact Enrichment API - Accounts, People, Discovery (`nabeelbaghoor/b2b-contact-enrichment-discovery-api`) Actor

Enrich B2B accounts and contacts with firmographics, technographics, seniority, department, verified email and direct phone, or work the other way and discover the people inside an account who match a persona. Flags contacts who have left. Pay per result.

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

## Pricing

$18.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 - Accounts, People, Discovery

Fill in the records you have, or find the people you do not have yet. Enrich a contact or an account into a full firmographic and technographic profile, or hand over a list of accounts and get back the people inside them who match a persona. One flat row per person.

### What this actor does

- **Enriches accounts and contacts in one pass.** A record carrying a name and a company comes back with the job title, standard seniority level, department, job functions, email, phone and public professional profile for the person, and the industry, sub-industry, headcount, revenue, ownership, SIC and NAICS codes, ticker, social profiles and full address for the company.
- **Discovers contacts inside an account.** Give it accounts rather than people and it returns the individuals inside them, ranked against personas configured on your account, with the seniority levels, departments, job functions and countries you asked for. One account can yield up to a hundred contacts.
- **Filters at the provider, not just afterwards.** The persona criteria, the seniority levels and the requirement for a verified email or a direct dial are sent with the discovery request, so they narrow what the provider returns and what it charges for. The remaining filters run after the answer and trim the dataset instead.
- **Flags the people who have left.** Every enriched person carries a verification status saying whether they are still where the record says, along with how that was established. On an eighteen-month-old list this is worth more than any other field, because a moved contact is a guaranteed bounce and a wasted touch. One switch drops them.
- **Reports what it validated on the way in.** The email and phone you supplied come back with their own validation verdicts, and the company address comes back normalised, so an enrichment pass doubles as a hygiene pass on the data you already had.
- **Returns technographics as columns.** Installed technologies, website technologies and custom technology categories arrive as flat lists rather than buried in a nested analytics object, which is the difference between a filterable table and a JSON blob.
- **Runs at whichever pace fits.** The single record mode answers in the request and starts writing rows immediately, at the provider's published thirty transactions per second. The bulk modes submit five hundred records at a time, take a polling location, and wait, which is the only sensible way to move tens of thousands of records.
- **Follows the polling location the provider gives it.** That location arrives as a path carrying the API version the job was accepted under, which is not always the version it was submitted to. The actor reads it rather than rebuilding it, and refuses anything that is not a plain relative path.
- **Waits properly rather than guessing.** A job still running answers with a non-final status and no body at all. That is never read as an empty result.
- **Drops records that cannot match before spending on them.** The provider's own minimum is a company name for an account, or a first and last name plus a company or an email for a person. Records missing that are dropped locally rather than sent, refused and charged for.
- **Never charges for a miss.** Only rows carrying a resolved person or account are billed.

### Input

| Field | What it does |
| --- | --- |
| `mode` | Enrich one at a time, enrich in bulk, or discover contacts. |
| `records` | The records to work on, one JSON object per line. |
| `companies` | Shorthand for account runs: one website or name per line. |
| `personas` | Up to five personas to rank discovered contacts by. |
| `minimumPersonaScore` | How well a contact must match before it is returned. |
| `resultsPerAccount` | Contacts returned per account, up to one hundred. |
| `contactLevels` | Seniority levels to find. Sent with the request. |
| `contactDepartments` | Departments to find. Sent with the request. |
| `contactJobFunctions` | Job functions to find. Sent with the request. |
| `contactCountries` | Countries to find contacts in. |
| `contactStates` | States or provinces to find contacts in. |
| `contactRegions` | Regions to find contacts in. |
| `requireVerifiedEmail` | Only return contacts with a verified address. |
| `requireDirectPhone` | Only return contacts with a direct dial. |
| `requireCompanyPhone` | Only return contacts with a company phone. |
| `enrichmentStatuses` | Keep only rows enriched to this degree. |
| `keepJobLevels` | Keep only rows at these seniority levels. |
| `matchConfidence` | Keep only rows matched with this confidence. |
| `industries` | Keep only rows in these industries. |
| `minCompanySize` | Keep only companies at or above this headcount. |
| `minCompanyRevenue` | Keep only companies at or above this revenue. |
| `requireEmail` | Drop rows with no email resolved. |
| `requirePhone` | Drop rows with no phone resolved. |
| `requireLinkedin` | Drop rows with no professional profile resolved. |
| `requireStillEmployed` | Drop people who have left the company on record. |
| `batchSize` | Records per request in the bulk modes, up to five hundred. |
| `requestsPerMinute` | Pace the run under your plan's rate limit. |
| `skipNotFound` | Leave unmatched records out of the dataset. |
| `maxResults` | Hard cap on rows, and therefore on spend and run time. |
| `apiKey` | Your own API token. Stored as a secret. |

### Example output

```json
{
  "found": true,
  "mode": "discover",
  "query": "example.com",
  "externalId": "row-0",
  "enrichmentStatus": "Person & Company Enriched",
  "personFullName": "Jane Doe",
  "personTitle": "VP of Demand Generation",
  "personDepartment": "Marketing",
  "personLevel": "VP Level",
  "personEmail": "jane.doe@example.com",
  "personEmailValidation": "VALID",
  "personPhone": "+1 805 555 1234",
  "personPhoneValidation": "VALID",
  "personLinkedinUrl": "https://www.linkedin.com/in/example",
  "personVerificationStatus": "Verified",
  "personVerificationSource": "Social & Email",
  "personCountry": "United States",
  "personState": "California",
  "personJobFunctions": ["Demand Generation", "Marketing Operations"],
  "personScore": 82.5,
  "personScoreProfile": "Marketing Decision Maker",
  "companyName": "Example Corp",
  "companyId": "ls-000000",
  "companyWebsite": "example.com",
  "companyIndustry": "Software",
  "companySubIndustry": "Marketing Software",
  "companySizeRange": "201-500",
  "companySizeExact": 340,
  "companyRevenueRange": "$50-100 Million",
  "companyRevenueExact": 72000000,
  "companyCountry": "United States",
  "companyMatchLevel": "HIGH",
  "companyInstalledTechnologies": ["Salesforce", "Marketo"],
  "error": null,
  "raw": { }
}
```

### Frequently asked questions

#### What is the difference between enrichment and discovery?

Enrichment takes a record you already have and fills in what is missing. Discovery runs in the opposite direction: you supply accounts and it returns the people inside them, so one input row can produce up to a hundred output rows. Enrichment is what you run over a CRM export; discovery is what you run when you know which companies you want to reach and not who at them.

#### How do personas work?

Personas are configured on your account with the provider, and you name them here. Discovery ranks every contact it finds inside an account against those personas and returns a score, and the minimum persona score decides how weak a match may be before it is not returned at all. Raising that score returns fewer, better contacts and spends fewer credits, which is usually the right trade when the alternative is paying for a hundred rows and keeping five.

#### Which filters save me money and which just tidy the output?

The discovery criteria are sent with the request: personas, minimum score, seniority levels, departments, job functions, locations, the contacts-per-account cap, and the requirements for a verified email or a direct dial. Those change what the provider returns and therefore what it charges for. Everything else runs after the answer arrives and trims the dataset and your Apify spend but not your provider credits. When cost matters, tighten the first group.

#### What does the verification status tell me?

Whether the person is still at the company the record says they are. It comes back as verified, not verified, or moved, along with the source that established it. On any list older than a year or so this is the highest-value field in the response, because the alternative to knowing is discovering it through bounces and unanswered calls. The drop-people-who-have-moved-on switch removes them in one step.

#### Why does the country filter behave differently from the others?

Because sending it empty is not the same as not sending it. This provider treats an explicitly blank country as a request for contacts with no country on file, which returns nearly nothing and looks like a broken filter. The actor therefore leaves the property off entirely unless a run actually named a country.

#### When should I use bulk instead of single?

Single sends one record per request and gets the answer back in the response, so rows start appearing straight away and a short run finishes in seconds. It is limited to the provider's thirty transactions per second. Bulk submits up to five hundred records at once and waits for a job to finish, which is far fewer requests and the only sensible approach past a few thousand records, at the cost of nothing appearing until each batch completes. Discovery is always submitted as a job.

#### What minimum input does a record need?

An account needs a company name or website. A person needs a first and last name together with one of a company name, a company website or an email address, and a bare email with no company will not match unless the address is on the provider's personal-email matching list. Records that fall short of that are dropped before the request rather than sent, refused and charged for. Supplying a company website rather than a company name raises the match confidence considerably, since names are ambiguous and domains are not.

#### Do I need my own API key?

Yes. This actor does not include data access. You use your own API token from Leadspace, which is the provider whose B2B enrichment API this actor calls, issued to you with a program id. Your own plan, credit allocation and terms apply. Paste the token into the `apiKey` field, where it is stored as an Apify secret. Note that these tokens are rotated on a schedule and expire, so a token that worked last year may simply have aged out rather than been revoked.

#### What happens when a record matches nothing?

It is written with `found: false` and an explanation, and never billed. Records the provider refused for want of required fields are written the same way, carrying the provider's own reason. Records in a submitted batch that the provider never mentions at all are also written as misses, so the dataset stays a faithful account of what was asked rather than only of what came back. Set `skipNotFound` to true to leave all of them out.

#### How much does a run cost?

Pricing is pay per result: you are charged for each person or account resolved into the dataset, and never for misses, for rows dropped by the filters, or for duplicates. Watch the cap in the discovery mode in particular, since one account expanded into a hundred contacts is a hundred rows and a hundred charges. Apify platform usage is included in the per-result price. Your own API provider's credit allocation is separate and billed by them.

### Keyword map

B2B data enrichment API, contact enrichment API, account enrichment, lead enrichment API, firmographic data API, technographic data API, company data API, contact discovery API, buyer persona targeting, ideal customer profile, ICP scoring, job title enrichment, seniority level data, department data, verified business email, direct dial phone number, LinkedIn profile enrichment, job change detection, contact still employed, CRM data hygiene, lead routing data, account based marketing data, sales intelligence API, prospect list building, bulk enrichment API

# Actor input Schema

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

Enrich fills in records you already have. Discover works the other way and returns the people inside an account who match a persona, so one input row can produce many output rows.

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

The records to work on, one JSON object per line. Recognised fields: firstName, lastName, email, title, linkedinUrl, personId, phone, personCountry, companyName, companyWebsite, companyLinkedinUrl, companyId, companyUgId, companyCountry, companyState, companyCity, companyStreet, companyPostalCode, externalId, label. Company enrichment needs a company name or website. Person enrichment needs a first and last name plus one of company name, website or email. Discovery needs a company name or website. Records that cannot possibly match are dropped before the request rather than sent and charged for.

## `companies` (type: `array`):

Shorthand for account-level runs: one company website or name per line, so a discovery run does not have to be written as JSON. A value containing a dot is treated as a website and anything else as a company name. Added to whatever the records list already holds.

## `personas` (type: `array`):

Up to five persona names configured on your account, used by the discovery mode to rank the contacts it finds. These are sent with the request, so unlike the filters below they change what the provider returns rather than what is kept.

## `minimumPersonaScore` (type: `integer`):

How well a contact must match the persona before the provider returns them at all. Raising this returns fewer, better contacts per account and spends fewer credits.

## `resultsPerAccount` (type: `integer`):

How many contacts the discovery mode may return for each account, from one to one hundred. Every returned contact is a row, so this multiplies both the dataset and the spend.

## `contactLevels` (type: `array`):

Restrict discovery to these standard job levels. Sent with the request, so it narrows what the provider returns. Leave empty for every level.

## `contactDepartments` (type: `array`):

Restrict discovery to these departments, one per line. Sent with the request.

## `contactJobFunctions` (type: `array`):

Restrict discovery to these job functions, one per line. Sent with the request.

## `contactCountries` (type: `array`):

Restrict discovery to contacts in these countries, one per line. Sent with the request. Leave empty to accept every location, which is not the same as sending an empty country: an empty value would restrict the results to people with no country on file at all.

## `contactStates` (type: `array`):

Restrict discovery to contacts in these states or provinces, one per line. Sent with the request.

## `contactRegions` (type: `array`):

Restrict discovery to contacts in these regions, one per line. Sent with the request.

## `requireVerifiedEmail` (type: `boolean`):

Tell the provider not to return a discovered contact unless it holds a verified email address for them. Sent with the request, so it reduces both the results and the credits spent.

## `requireDirectPhone` (type: `boolean`):

Tell the provider not to return a discovered contact unless it holds a direct phone number for them. Sent with the request.

## `requireCompanyPhone` (type: `boolean`):

Tell the provider not to return a discovered contact unless it holds a company phone number for them. Sent with the request.

## `enrichmentStatuses` (type: `array`):

Only keep rows where the provider filled in this much. Restricting to the fully enriched outcome is how you get a clean table with no half-filled rows in it. Leave empty to keep every outcome. Applied after the provider answers.

## `keepJobLevels` (type: `array`):

Only keep rows whose person sits at one of these standard job levels. Applied after the provider answers, which makes it the right filter for enrichment runs, where seniority is discovered rather than requested.

## `matchConfidence` (type: `array`):

Only keep rows where the provider was this sure it matched the right company. Worth setting when the input is company names rather than websites, since names are far more ambiguous than domains.

## `industries` (type: `array`):

Only keep rows whose industry or sub-industry contains one of these words, one per line. Matching is loose on purpose, since industry labels vary.

## `minCompanySize` (type: `integer`):

Only keep rows whose company has at least this many employees, using the exact count rather than the band. Rows with no exact count are dropped when this is set. Leave at zero to keep every size.

## `minCompanyRevenue` (type: `integer`):

Only keep rows whose company reports at least this much revenue, as a plain number with no currency symbol. Rows with no exact figure are dropped when this is set. Leave at zero to keep every revenue.

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

Drop rows where no email address was resolved for the person.

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

Drop rows where no phone number was resolved for the person.

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

Drop rows where no public professional profile was resolved for the person.

## `requireStillEmployed` (type: `boolean`):

Drop rows the provider marked as having left the company on the record. This is the single most useful hygiene filter on an old list, since a moved contact is a bounce and a wasted touch.

## `batchSize` (type: `integer`):

How many records to submit in one request in the bulk and discovery modes, up to the provider's ceiling of five hundred. Smaller batches finish sooner and start writing rows earlier; larger batches make fewer requests.

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

Pace the run so it stays under whatever rate your plan allows. The single record mode is capped at the provider's published thirty per second regardless of what is set here.

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

Leave records the provider returned nothing for out of the dataset. Off by default, because knowing which records failed to match is usually the point of running an enrichment pass. Misses are never charged for either way.

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

Stop after this many rows. This is the cap on both spend and run time, and it matters most in the discovery mode, where one account can produce a hundred rows.

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

Your own API token for the B2B enrichment API, issued by the provider along with a program id. Sent as a bearer token on every request. Note that these tokens are rotated periodically and expire, so a token that used to work may simply have aged out. Stored as a secret.

## Actor input object example

```json
{
  "mode": "enrich",
  "records": [
    "{\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"companyWebsite\":\"example.com\"}"
  ],
  "minimumPersonaScore": 30,
  "resultsPerAccount": 10,
  "contactLevels": [],
  "requireVerifiedEmail": false,
  "requireDirectPhone": false,
  "requireCompanyPhone": false,
  "enrichmentStatuses": [],
  "keepJobLevels": [],
  "matchConfidence": [],
  "minCompanySize": 0,
  "minCompanyRevenue": 0,
  "requireEmail": false,
  "requirePhone": false,
  "requireLinkedin": false,
  "requireStillEmployed": false,
  "batchSize": 500,
  "requestsPerMinute": 600,
  "skipNotFound": false,
  "maxResults": 100
}
```

# Actor output Schema

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

One row per resolved person or account, carrying the job title, seniority, department, email, phone and profile for the person, and the industry, headcount, revenue, technologies and location for the company.

# 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": [
        "{\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"companyWebsite\":\"example.com\"}"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nabeelbaghoor/b2b-contact-enrichment-discovery-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": ["{\"firstName\":\"Jane\",\"lastName\":\"Doe\",\"companyWebsite\":\"example.com\"}"] }

# Run the Actor and wait for it to finish
run = client.actor("nabeelbaghoor/b2b-contact-enrichment-discovery-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": [
    "{\\"firstName\\":\\"Jane\\",\\"lastName\\":\\"Doe\\",\\"companyWebsite\\":\\"example.com\\"}"
  ]
}' |
apify call nabeelbaghoor/b2b-contact-enrichment-discovery-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-discovery-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/CcBbt24IPc3fbDtwl/builds/RzsdnQan2zZv91MZn/openapi.json
