# B2B Lead Enrichment (`teodor_banea/b2b-lead-enrichment`) Actor

Turn a simple list of company domains into rich, actionable company profiles.
Upload a CSV with company domains, and get back firmographic data, technology stacks, contact emails, social links, and industry classification. No paid API keys required. Everything runs on publicly available data.

- **URL**: https://apify.com/teodor_banea/b2b-lead-enrichment.md
- **Developed by:** [Banea Teodor](https://apify.com/teodor_banea) (community)
- **Categories:** Automation, Lead generation, Jobs
- **Stats:** 71 total users, 4 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.20 / 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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## B2B Lead Enrichment

Turn a plain list of company domains into sales-ready B2B profiles — company details, technology stack, email infrastructure, social links and contact emails.

Upload a CSV with a `domain` column and get back one enriched row per company. No API keys required, no external data vendors, no per-record data fees.

### What does this Actor do?

For every company in your CSV, the Actor reads the company's own website, its DNS records, its TLS certificate and its public GitHub presence, then merges everything into a single consistent row.

It follows each site's **own navigation** to the pages that carry real information — contact, about, team, imprint — rather than guessing at fixed URLs, which is why it finds contacts that simpler scrapers miss.

### What you get

| Data | Example | Source |
|------|---------|--------|
| Company name & legal name | Baremetrics / Baremetrics, Inc. | schema.org, SSL certificate |
| Description | Subscription analytics for SaaS businesses | schema.org, meta tags |
| Industry | Data & Analytics | Classified from the site's own copy |
| Employee count | 113 | schema.org, stated figures on the site |
| Founded year | 2014 | schema.org, "founded in…", copyright range |
| Location | Austin, TX, US | schema.org, `<address>`, SSL certificate |
| Named contacts | Luke Marshall — CEO — luke@baremetrics.com | Team & contact pages |
| Shared inboxes | hello@, sales@ | Contact pages, ranked by sales value |
| Phone | +1 512 555 0142 | `tel:` links, schema.org |
| Tech stack | Cloudflare, Next.js, HubSpot, Mailchimp | Headers, scripts, meta tags, SPF |
| Email provider | Google Workspace | MX records |
| Email security | SPF, DMARC, DKIM | TXT records |
| Programming languages | TypeScript, Ruby | Public GitHub repositories |
| Social links | LinkedIn, X, Facebook, Crunchbase, GitHub | schema.org `sameAs`, page links |
| SSL issuer & validity | Let's Encrypt | TLS certificate |

Every field records **where it came from** in a `provenance` object, and every row carries an **`enrichmentScore`** (0–100) so you can filter out thin records before importing into a CRM.

### Quick start

1. Prepare a CSV with a `domain` column:
   ```
   domain
   baremetrics.com
   basecamp.com
   buffer.com
   ```
2. Paste it into **CSV content**, or point the Actor at a public URL or a key-value store record.
3. Click **Start**.

Column names are auto-detected, so `website`, `company_domain` or `url` work just as well as `domain`.

### Pricing

You are charged **once per company enriched** — one input row produces exactly one billable result, no matter how many contacts are found for it.

That means your cost is predictable before you start: 500 companies in your CSV is 500 results. Use **Maximum companies to process** to cap a run, and note that **Max contacts per company** does *not* affect what you pay — extra contacts ride along in the same row.

Domains that return nothing — dead, parked, expired or for sale — are not charged; they are listed in the run log (and in the `errors` dataset where the run is allowed to write one). A **maximum cost per run** set in the run options is respected: once it is reached, the remaining companies are skipped rather than enriched and discarded.

### Output

One row per company. The best contact is flattened onto the row so CSV and spreadsheet exports stay usable, and the complete list is in `contacts`.

```json
{
  "companyId": "8f14e45fceea167a",
  "companyDomain": "baremetrics.com",
  "companyName": "Baremetrics",
  "companyLegalName": "Baremetrics, Inc.",
  "companyDescription": "Subscription analytics and insights for SaaS businesses",
  "companyIndustry": "Data & Analytics",
  "companySizeEmployees": 42,
  "companyFoundedYear": 2014,
  "companyCity": "Austin",
  "companyRegion": "TX",
  "companyCountry": "US",
  "companyWebsiteUrl": "https://baremetrics.com",
  "companyLogoUrl": "https://baremetrics.com/og-image.png",
  "companyLinkedinUrl": "https://www.linkedin.com/company/baremetrics",
  "companyGithubUrl": "https://github.com/baremetrics",
  "companyEmails": ["hello@baremetrics.com", "marketing@baremetrics.com"],
  "companyPhone": "+1 512 555 0142",
  "techStack": ["Cloudflare", "jQuery", "HubSpot", "Mailchimp"],
  "techCategories": ["Web Server", "Email Marketing"],
  "programmingLanguages": ["TypeScript", "Ruby"],
  "emailProvider": "Google Workspace",
  "emailSecurity": ["SPF", "DMARC"],
  "emailMarketingTools": ["Mailchimp"],
  "dnsProvider": "Cloudflare DNS",
  "sslIssuer": "Let's Encrypt",
  "primaryContactName": "Luke Marshall",
  "primaryContactEmail": "luke@baremetrics.com",
  "primaryContactTitle": "CEO",
  "primaryContactSeniority": "C-Level",
  "primaryContactDepartment": "Executive",
  "contacts": [
    {
      "personFullName": "Luke Marshall",
      "personEmail": "luke@baremetrics.com",
      "personTitle": "CEO",
      "personSeniority": "C-Level",
      "personDepartment": "Executive"
    }
  ],
  "contactCount": 5,
  "enrichmentScore": 77,
  "fieldsFilled": 11,
  "enrichedAt": "2026-09-13T14:30:00.000Z"
}
```

Four dataset views are provided in the Console: **Overview**, **Contacts**, **Tech Stack** and **Social & Web**.

### About data quality

This Actor reports only what the company publishes about itself, and it is deliberately conservative about what it is willing to call a fact:

- **Documentation placeholders are filtered out.** Addresses that appear in API examples and code samples on a company's own domain are excluded — a fake contact is worse than no contact.
- **Shared inboxes are labelled, not disguised.** `sales@` is returned as a shared inbox, never dressed up as a person.
- **Names are only claimed when the page says so.** A name is taken from a team card, or derived from an unambiguous `first.last@` address — never guessed from initials.
- **Decision-makers from team pages.** Founders, partners and managers listed on team, about and leadership pages are returned with their job title and LinkedIn profile, even when the site publishes no email for them. Customers quoted in testimonials and reviews are never mistaken for staff.
- **Emails are typed by how they were found.** When a company publishes some personal addresses (`jane.doe@`), the same format is applied to its other named staff and marked `type: "pattern"` with a confidence. Without an observed format, no address is invented.
- **Bot walls and parked domains are not data.** A Cloudflare challenge page, a "domain for sale" page or an expired-hosting notice is recognised as such instead of being scraped into the company's name and phone number. Parked and expired domains are never charged.
- **`techStack` contains technologies only.** Certificate authorities, GitHub topics and source languages have their own fields, so the column stays useful for filtering.
- **Fields are `null` rather than wrong.** "Remote", "Worldwide" and lists of every office are not stored as a city. `companyIndustry` is left empty when no classification was confident enough.

Missing data is normal and expected: a company that publishes no address genuinely has no address to find. `enrichmentScore` tells you how much was recovered for each record.

### Tips

- **Add a free GitHub token** to raise the GitHub rate limit from 60 to 5,000 requests/hour. Without it, GitHub data stops after roughly 60 companies in a run. Create one at [github.com/settings/tokens](https://github.com/settings/tokens) — no scopes needed.
- **Add a free Google Knowledge Graph key** for richer descriptions on well-known companies. Get one in the Google Cloud Console under APIs & Services → Knowledge Graph Search API.
- **Raise "Pages per company"** to find more contacts. The Actor follows the site's navigation, so more pages usually means reaching the team page as well as the contact page.
- **Raise "Max concurrency"** for large runs. Most sites are read over plain HTTP, so 10–30 parallel companies is comfortable.
- **Turn on proxies** for runs of several hundred companies or more; some sites block datacenter IPs.
- **Filter on `enrichmentScore`** before importing to a CRM — scores below about 30 are usually companies with a thin or parked website.

### How it works

Sites are fetched over plain HTTP first. A real browser is launched only when a page turns out to render entirely in JavaScript, which is a small minority of company websites. You can disable that fallback under **Website Scraper Settings** for the lowest possible cost, at the price of missing data on JavaScript-only sites.

DNS lookups fall back to DNS-over-HTTPS when the system resolver is unavailable, so email provider and email security data stay populated on restricted networks.

### Integrations

The dataset exports to CSV, JSON, Excel and Google Sheets, and can be pushed to Zapier, Make, Airbyte or any HTTP endpoint through Apify's integrations. Use `companyId` as a stable join key across runs.

### Limitations

- Data comes from public sources only. Companies that publish little will return sparse rows.
- Employee counts and founding years are reported when stated; they are not estimated.
- Contact discovery depends on the company publishing addresses on its website. Sites that use only a contact form will return no email.
- This Actor does not verify that a mailbox accepts mail.

# Changelog

This Actor's version history is a separate document: https://apify.com/teodor_banea/b2b-lead-enrichment/changelog.md

# Actor input Schema

## `inputCsvSource` (type: `string`):

Where to read the input CSV from. 'Paste CSV' lets you paste content directly. 'Public URL' downloads from a URL. 'Key-Value Store' reads from the run's default store.

## `inputCsvContent` (type: `string`):

Paste your CSV content here. Must have a header row with at least a 'domain' column.

## `inputCsvUrl` (type: `string`):

Public URL of the input CSV file. Used when Input CSV source is set to 'Public URL'.

## `inputCsvKey` (type: `string`):

Key in the default key-value store where the CSV is stored.

## `columnMapping` (type: `object`):

Map your CSV column headers to logical fields. Leave empty for auto-detection (looks for common names like 'domain', 'company', 'email').

## `csvDelimiter` (type: `string`):

Character used to separate columns in your CSV.

## `enableFirmographics` (type: `boolean`):

Enrich company data: name, description, location, founding year, and more.

## `enableContacts` (type: `boolean`):

Find contact emails from company websites and generate email pattern candidates.

## `enableTechnographics` (type: `boolean`):

Detect the technology stack from website headers, scripts, DNS records, and GitHub repos.

## `googleKgApiKey` (type: `string`):

Optional. Free API key (100,000 requests/day) for richer company descriptions. Get one at console.cloud.google.com → APIs & Services → Knowledge Graph Search API.

## `githubToken` (type: `string`):

Optional. Free token increases GitHub rate limit from 60 to 5,000 requests/hour. Create one at github.com/settings/tokens.

## `enableGoogleKg` (type: `boolean`):

Use Google Knowledge Graph for company descriptions and industry data. Requires an API key above.

## `enableGithub` (type: `boolean`):

Look up company GitHub organizations for tech stack and company info. Disable to avoid rate limit issues.

## `websiteScraperMaxPages` (type: `integer`):

How many pages to read per company website. The Actor follows the site’s own navigation to its contact, about and team pages, so more pages mostly means more contacts.

## `websiteScraperTimeoutSecs` (type: `integer`):

Maximum time to wait for each page to load.

## `enableBrowserFallback` (type: `boolean`):

Most sites are read over plain HTTP, which is fast and cheap. When a page turns out to render entirely in JavaScript, the Actor retries it in a real browser. Turn this off for the lowest possible cost, at the price of missing data on JS-only sites.

## `maxRows` (type: `integer`):

Limit how many companies to enrich. You are billed once per company enriched. Set to 0 for unlimited (capped at 5,000 for safety).

## `maxConcurrency` (type: `integer`):

Number of companies to enrich in parallel. Higher is faster and cheaper per company; lower it only if target sites rate-limit you.

## `providerTimeoutSecs` (type: `integer`):

Maximum time to wait for each data source before skipping it.

## `maxContactsPerCompany` (type: `integer`):

How many contacts to return per company. All contacts ride along in one row, so this does not change what you are billed.

## `includeRawResponses` (type: `boolean`):

Attach raw data from each provider to the output. Useful for debugging but increases output size.

## `proxyConfiguration` (type: `object`):

Proxies for website scraping. Recommended for large runs — some company websites block datacenter IPs.

## `enableWebsiteScraper` (type: `boolean`):

Scrape company websites for firmographic data, contacts, and tech stack.

## `enableDnsMx` (type: `boolean`):

Analyze DNS records to detect email providers and marketing tools.

## `enableSslCert` (type: `boolean`):

Read SSL certificates for company legal name and location.

## `enableEmailPattern` (type: `boolean`):

Generate candidate email addresses from person names and company domains.

## `firmographicProviderOrder` (type: `array`):

Order in which providers are queried for firmographic data.

## `contactProviderOrder` (type: `array`):

Order in which providers are queried for contact data.

## `technographicProviderOrder` (type: `array`):

Order in which providers are queried for technology stack data.

## Actor input object example

```json
{
  "inputCsvSource": "paste",
  "inputCsvContent": "domain\nstripe.com\nnotion.so\ncloudflare.com",
  "inputCsvKey": "INPUT.csv",
  "columnMapping": {
    "companyDomain": "domain",
    "companyName": "company",
    "personEmail": "email"
  },
  "csvDelimiter": ",",
  "enableFirmographics": true,
  "enableContacts": true,
  "enableTechnographics": true,
  "enableGoogleKg": true,
  "enableGithub": true,
  "websiteScraperMaxPages": 4,
  "websiteScraperTimeoutSecs": 10,
  "enableBrowserFallback": true,
  "maxRows": 10,
  "maxConcurrency": 10,
  "providerTimeoutSecs": 15,
  "maxContactsPerCompany": 5,
  "includeRawResponses": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "enableWebsiteScraper": true,
  "enableDnsMx": true,
  "enableSslCert": true,
  "enableEmailPattern": true,
  "firmographicProviderOrder": [
    "website-scraper",
    "google-kg",
    "ssl-cert"
  ],
  "contactProviderOrder": [
    "website-scraper",
    "email-pattern"
  ],
  "technographicProviderOrder": [
    "website-scraper",
    "dns-mx",
    "github"
  ]
}
```

# 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 = {
    "inputCsvContent": `domain
stripe.com
notion.so
cloudflare.com`,
    "columnMapping": {
        "companyDomain": "domain",
        "companyName": "company",
        "personEmail": "email"
    },
    "maxRows": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("teodor_banea/b2b-lead-enrichment").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 = {
    "inputCsvContent": """domain
stripe.com
notion.so
cloudflare.com""",
    "columnMapping": {
        "companyDomain": "domain",
        "companyName": "company",
        "personEmail": "email",
    },
    "maxRows": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("teodor_banea/b2b-lead-enrichment").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 '{
  "inputCsvContent": "domain\\nstripe.com\\nnotion.so\\ncloudflare.com",
  "columnMapping": {
    "companyDomain": "domain",
    "companyName": "company",
    "personEmail": "email"
  },
  "maxRows": 10
}' |
apify call teodor_banea/b2b-lead-enrichment --silent --output-dataset

```

## MCP server setup

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

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/zMfJcg3smgiLIeetH/builds/LBCkFHMC2J9jsyc2F/openapi.json
