# Facebook B2b Lead Scraper (`scraper-engine/facebook-b2b-lead-scraper`) Actor

Extract targeted business prospects from Facebook using the Facebook B2B Lead Scraper. Collect company pages, descriptions, emails, websites, phone numbers, and follower metrics automatically. Ideal for B2B outreach, lead generation, and market research.

- **URL**: https://apify.com/scraper-engine/facebook-b2b-lead-scraper.md
- **Developed by:** [Scraper Engine](https://apify.com/scraper-engine) (community)
- **Categories:** Lead generation, Social media, Automation
- **Stats:** 29 total users, 0 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: 4.00 out of 5 stars

## Pricing

$19.99/month + usage

To use this Actor, you pay a monthly rental fee to the developer. The rent is subtracted from your prepaid usage every month after the free trial period. You also pay for the Apify platform usage, which gets cheaper the higher Apify subscription plan you have.

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

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

### Facebook B2B Lead Scraper 🏢

Find **companies** on Facebook and get their **real business email**.

You give it industry keywords (`marketing agency`, `consulting firm`, `freight forwarder`). It finds
public Facebook **business Pages** that match, opens each Page's **public About tab**, and reads the
contact details the business itself published there — company name, category, website, and the email
address it uses for enquiries.

The result is a corporate contact list: `info@`, `sales@`, `contact@`, `hello@` on the company's own
domain — not personal profiles, not Marketplace listings, not group chatter.

```json
{
  "network": "facebook.com",
  "keyword": "consulting firm",
  "title": "KEP Consulting Firm (@kepconsultingfirm) - About",
  "description": "KEP Consulting Firm, Pretoria. 164 likes. We are an audit and consultancy firm offering services to the public and private sectors.",
  "url": "https://www.facebook.com/kepconsultingfirm",
  "email": "info@kepconsulting.co.za",
  "companyName": "KEP Consulting Firm",
  "category": "Financial service",
  "website": "http://www.kepconsulting.co.za/",
  "emailSource": "facebook_page_about"
}
```

***

### 🔎 Where the email actually comes from

This section is deliberately blunt, because "where did this address come from?" is the only question
that matters when you are about to email a stranger. Every row carries an **`emailSource`** field
naming its origin. There are exactly three, in priority order:

| `emailSource` | What it means | Typical share |
| --- | --- | --- |
| `facebook_page_about` | The business published this address in the **Email** field of its public Facebook Page About tab. Highest confidence — the company put it there to be contacted on. | Most rows |
| `company_website:<host>` | The Page listed a website but no email, so the actor fetched that company's own site (home, `/contact`, `/contact-us`, `/about`, `/about-us`) and took the best corporate mailbox found, preferring `mailto:` links. | Some rows |
| `google_search_snippet` | Neither surface yielded an address, but one was visible in the search-result snippet for that Page. Lowest confidence — kept only as a last resort. | Rare |

Nothing else is ever emitted. The actor **never** guesses an address from a pattern
(`firstname.lastname@company.com`), never invents sample rows, and never writes a placeholder into a
field. If a source yields nothing, the run log says so and the row is dropped.

#### What is NOT reachable (tested, reported honestly)

These were all tested live before this actor was built. They do not work and are not used:

- **Unauthenticated Graph API** — `graph.facebook.com/<page>?fields=emails` returns
  `{"error":{"message":"(#200) Provide valid app ID"}}` (HTTP 403). No app token, no data.
- **Page oEmbed** — `facebook.com/plugins/page/oembed.json/` returns HTTP 404. Retired.
- **The Page plugin iframe** — `facebook.com/plugins/page.php` returns HTTP 200 but carries only the
  name, cover photo and a like button. No contact fields.
- **`mbasic.facebook.com`** — still resolves, but with a plain client it serves the
  "Log in or sign up to view" wall. It is not a usable bypass and is not relied on.
- **Google's result links** — Google now wraps every organic result as
  `/goto?url=CAES<encrypted>`. The payload is encrypted server-side; following it returns HTTP 400.
  It is not decodable, and this actor does not pretend to decode it (see the fix below).
- **Google's `<cite>` breadcrumb** — useless for Facebook specifically: Facebook results render their
  cite line as `"40+ comments · 2 years ago"`, not as a URL.

#### What IS reachable

`https://www.facebook.com/<page>/about` returns **HTTP 200 with the full logged-out markup** —
`og:` metadata plus a `profile_fields` payload containing Email, Website, Phone, Address, Category and
opening hours — *provided* the request goes through a **residential** proxy and carries a complete
browser navigation header set. The identical URL with a minimal `user-agent` + `accept` header pair
returns **HTTP 400 and a 1.5 KB stub**. That header detail is the whole ballgame, and it is why this
actor sends the full Chrome header set on every Facebook request.

***

### 🛠️ What was broken, and what fixed it

This actor previously returned **zero rows on every run**. Two independent causes, both fixed.

#### 1. Every search result was silently discarded

The old code walked the anchors on Google's results page and called `_unwrap_google_url()` on each
`href`. Google has since replaced direct result links with an opaque wrapper:

```
href="https://apify.com/goto?url=CAESfgHrOzAVYCzaRlwZ4cm6ikcGdtCqPD3psODc011WCUlw..."
```

`_unwrap_google_url()` returned that literal relative string, which then failed the
`url.startswith(("http://", "https://"))` guard, so **every single result was skipped**. Measured
live on this actor's own query: 19 anchors, 0 direct, 19 wrapped → 0 rows.

**The fix.** The same page still embeds the real destination in an inline JSON data island a few
hundred bytes after each wrapper:

```
["https://www.facebook.com/ProknowaraConsulting/about/",
 "Proknowara Consulting Firm ...",
 "... Consulting agency in Dhaka ... marketing@proknowara.com ...", 1, "en", ...]
```

`google_blob_results()` scans for `["http` and lets `json.JSONDecoder.raw_decode` consume the array.
This reads the **data payload rather than the rendered DOM**, so it is immune to Google's rotating
obfuscated CSS class names. Three tiers are tried in order — JSON island, classic anchor walk
(for direct and legacy `/url?q=` layouts), then a raw-text URL sweep — and `_unwrap_google_url()` now
returns `""` for a `/goto?` wrapper so callers fall through cleanly. Locale subdomains
(`it-it.facebook.com/...`) are accepted and normalised back to `www.facebook.com`.

#### 2. The email was never fetched, only hoped for

The old pipeline regex-scanned the **search-result snippet** and nothing else. An email is almost
never in a snippet, so even with cause 1 fixed the yield would have stayed near zero.

**The fix.** Discovery and extraction are now separate stages. Search finds the *Page*; the email
comes from actually fetching that Page's About tab, and from the company's own website when the Page
lists one but no address.

***

### 🏢 Why this actor and not the other Facebook scrapers

There are sibling actors for general Facebook search, for Pages, and for personal profiles. This one
is the **B2B** member of the family, and it is built differently on purpose.

**It searches for companies, not for emails.** The sibling actors query
`site:facebook.com "@gmail.com" "<keyword>"` — which only ever surfaces pages where an address is
already visible in Google's snippet. In practice that means group threads and job ads: consumer and
community content. This actor instead targets the Page About tab directly:

```
site:facebook.com/*/about "<keyword>"
```

Measured live on `consulting firm`: **10/10** results were company Page `/about` URLs, versus 6/10
group threads for the old query — and **9 of those 10 Pages published a real corporate email**.

**It rejects the consumer surfaces outright.** Group threads, Marketplace listings, personal
timelines, events, reels, photo and post permalinks are all filtered out by path. Those carry
personal and classified-ad contacts, which is what the sibling actors are for.

**It ranks for commercial relevance.** Candidates are scored and the best contact wins:

| Score | Address |
| --- | --- |
| 100 | Role mailbox on the company's own domain — `info@acme.co.uk` |
| 90 | Any address on the company's own domain — `jane@acme.co.uk` |
| 70 | Role mailbox on another corporate domain |
| 55 | Any other corporate domain |
| 35 | Role mailbox on free webmail — `sales.acme@gmail.com` |
| 10 | Plain free webmail — `joe@gmail.com` |

**It returns firmographics, not just an address.** `companyName`, `category` and `website` come out
alongside the email, so a row is usable in a CRM without a second enrichment pass.

#### ⚠️ One deliberate default change: `emailDomains` now starts empty

The input schema used to pre-fill `emailDomains` with `["@gmail.com"]`. For a B2B tool that default
is actively harmful: genuine business contacts overwhelmingly sit on the company's *own* domain, so
pre-filtering to Gmail discards nearly every real lead the actor finds. In the verified sample above,
**9 of 10** addresses would have been thrown away.

The **filter semantics are unchanged** — `emailDomains` still keeps only the domains you list. Only
the *prefill* changed, from `["@gmail.com"]` to empty (= keep every domain). Type `@gmail.com` back
in whenever you specifically want sole traders on free webmail.

***

### 🧩 Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `keywords` | array | `["consulting firm"]` | **Required.** Industry/trade keywords. Trade terms beat generic words. |
| `platform` | string | `Facebook` | Network to mine. Facebook only for now. |
| `location` | string | `""` | Optional city/region/country filter. Narrow values cut results sharply. |
| `emailDomains` | array | `[]` | Keep only these email domains. Empty = all domains (recommended for B2B). |
| `maxEmails` | integer | `20` | Leads per keyword (1–5000). |
| `engine` | string | `legacy` | Processing engine. |
| `enrichFromWebsite` | boolean | `true` | *Advanced.* Also fetch the company's own site when the Page lists no email. |
| `requireBusinessEmail` | boolean | `false` | *Advanced.* Drop free-webmail leads; keep only corporate domains. |
| `maxSearchPages` | integer | `6` | *Advanced.* Search-result pages to walk per keyword (1–20). |
| `proxyConfiguration` | object | RESIDENTIAL | *Network.* Proxy for Facebook and website fetching. |

```json
{
  "keywords": ["consulting firm", "marketing agency"],
  "location": "London",
  "emailDomains": [],
  "maxEmails": 20,
  "requireBusinessEmail": true,
  "platform": "Facebook",
  "engine": "legacy"
}
```

#### About the proxy

Two proxies are used, and they are not interchangeable:

- **Google search** always runs through the Apify **GOOGLE\_SERP** group. That group can *only* reach
  Google's own hosts — it cannot fetch `facebook.com` at all — so it is not configurable here.
- **Facebook Pages and company websites** are fetched through `proxyConfiguration`, defaulting to
  **RESIDENTIAL**. This is what makes Facebook serve its logged-out Page markup.

Setting `proxyConfiguration` to a datacenter group will degrade Facebook fetching. Leave the default
unless you know why you are changing it.

***

### 📤 Output

Every field carries a real value on every row. A lead missing any field is **dropped**, not padded
with a blank — an empty column is worse than one fewer row, and the run log names what was skipped.

| Field | Description |
| --- | --- |
| `network` | Facebook host the result matched (`facebook.com`, `fb.com`, …) |
| `keyword` | Keyword that surfaced this company |
| `title` | Page title as indexed |
| `description` | The Page's public description |
| `url` | Canonical Facebook Page URL |
| `email` | The business contact address |
| `companyName` | Business name, cleaned of `\| City` and `(@slug)` noise |
| `category` | Facebook business category — `Marketing Agency`, `Business Consultant`, … |
| `website` | The company's own website |
| `emailSource` | Where the email came from — see the table at the top |

***

### 🎯 Use cases

- **Outbound sales prospecting** — build a list of agencies, consultancies or contractors in a city,
  each with a monitored `info@`/`sales@` inbox.
- **Partner and supplier sourcing** — find companies in a trade category with a working contact route.
- **CRM enrichment** — match an existing account list against Facebook Pages to fill in category,
  website and a contact address.
- **Market mapping** — enumerate every business Page in a category and location to size a segment.

***

### ⚖️ Legal and ethical use

This actor reads **only publicly available information**: pages Facebook serves to a logged-out
visitor, and companies' own public websites. It does not log in, does not bypass any authentication,
and does not touch private profiles or non-public fields.

The addresses it returns are ones businesses published for the purpose of being contacted. That does
not exempt you from the rules that apply to using them. You are responsible for complying with GDPR,
CAN-SPAM, PECR and the equivalent laws in your market — including having a lawful basis for contact,
identifying yourself, and honouring opt-outs. Use a reasonable send volume and do not resell scraped
contact data as a list product.

***

### Support

- **Bug reports**: open a ticket in the repository Issues section
- **Custom features**: dev.scraperengine@gmail.com

### FAQ

**Where exactly does the email come from?**
The `emailSource` field on every row says so explicitly. See the table at the top of this README.

**Why did I get fewer rows than `maxEmails`?**
Not every business Page publishes an email. The actor drops incomplete leads rather than padding the
dataset. Broaden the keyword, clear the `location` filter, or raise `maxSearchPages`.

**Can it scrape personal profiles?**
No — by design. Personal timelines, group posts and Marketplace listings are filtered out. Use the
sibling profile/page/general Facebook actors for those.

**Can it get emails hidden behind Facebook's login?**
No, and it does not claim to. Only what Facebook serves publicly is read.

**Why is `emailDomains` empty by default now?**
Because pre-filtering to `@gmail.com` discarded roughly 9 out of every 10 genuine B2B leads. The
filter itself works exactly as before; only the starting value changed.

**Does it use the Facebook API?**
No. The unauthenticated Graph API returns `(#200) Provide valid app ID` and is unusable without an
app token. Public page markup is used instead.

# Actor input Schema

## `keywords` (type: `array`):

🎯 The words that power your B2B lead search! Add one or more industry/trade keywords (e.g. \['marketing agency', 'consulting firm', 'freight forwarder', 'HVAC contractor']) and the actor finds public Facebook **business Pages** matching them, then 📧 reads the company's own contact email from the Page's public About tab. 💡 Tip: trade and industry terms ('logistics company', 'law firm') beat generic words — they map to how businesses actually name themselves.

## `platform` (type: `string`):

📱 The social network to mine for leads. Currently locked to Facebook — more platforms coming soon! 🚀

## `location` (type: `string`):

🗺️ Optional geo-targeting! Add a city, region or country (e.g. 'London' 🇬🇧, 'New York' 🗽, 'Dubai' 🏙️) to narrow leads down to business Pages mentioning that place. ✨ Leave empty to search the whole globe 🌍. ⚠️ A very narrow location can cut your result count sharply.

## `emailDomains` (type: `array`):

🎛️ Keep only leads whose email sits on the domains you list. 🏢 **Left empty by default on purpose**: genuine B2B contacts are overwhelmingly corporate addresses on the company's *own* domain (info@acme.co.uk, sales@acme.com), so pre-filtering to free webmail would throw away almost every real business lead. 🕳️ Empty = collect every domain (recommended for B2B). ✍️ Add entries only when you specifically want them — e.g. 🏢 @yourclient.com, or 📮 @gmail.com · 📨 @outlook.com · 💌 @hotmail.com · 💜 @yahoo.com if you are deliberately after sole traders on free webmail.

## `maxEmails` (type: `integer`):

🎚️ How many leads to collect per keyword before moving on. ⚡ Higher = more leads but longer runs, since every Page's About tab is fetched individually. Default is 20 ✅ (range 1 – 5,000).

## `engine` (type: `string`):

🔧 The processing engine. Currently 'legacy' 🛡️ — a reliable, battle-tested extraction pipeline. More engines on the roadmap! 🚧

## `enrichFromWebsite` (type: `boolean`):

🏢 When a Facebook business Page lists a website but no email, also fetch that company's own site (home, /contact, /about) and take the corporate mailbox from there. ✅ Recommended — it is where most of the remaining genuine B2B addresses live. ⏱️ Turn off for faster, Facebook-only runs.

## `requireBusinessEmail` (type: `boolean`):

🎯 Drop any lead whose email is on free consumer webmail (Gmail, Outlook, Yahoo, iCloud, …) and keep only addresses on a company's own domain. 💼 Turn on for a strictly corporate list; leave off to also capture sole traders and small firms that run on free webmail.

## `maxSearchPages` (type: `integer`):

🔎 How many search-result pages to walk per keyword while hunting for business Pages. Higher = more companies discovered, at the cost of a longer run. Default 6 ✅ (range 1 – 20).

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

🌐 Controls the proxy used to fetch Facebook Pages and company websites. 🆓 Leave it as-is and the actor uses the Apify RESIDENTIAL group, which is what makes Facebook serve its logged-out Page markup. 🔎 Google search always runs through the dedicated GOOGLE\_SERP group regardless of this setting — that group can only reach Google's own hosts, so it cannot be used for page fetching. 🔄 On any failed or blocked response the actor rotates to a fresh proxy URL and retries. 🧠 Advanced users can plug in their own groups or external URLs here.

## Actor input object example

```json
{
  "keywords": [
    "consulting firm"
  ],
  "platform": "Facebook",
  "location": "",
  "emailDomains": [],
  "maxEmails": 20,
  "engine": "legacy",
  "enrichFromWebsite": true,
  "requireBusinessEmail": false,
  "maxSearchPages": 6,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# 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 = {
    "keywords": [
        "consulting firm"
    ],
    "emailDomains": [],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scraper-engine/facebook-b2b-lead-scraper").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 = {
    "keywords": ["consulting firm"],
    "emailDomains": [],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("scraper-engine/facebook-b2b-lead-scraper").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 '{
  "keywords": [
    "consulting firm"
  ],
  "emailDomains": [],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call scraper-engine/facebook-b2b-lead-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/HNkJI09YIkIOa1J0j/builds/dprt0wHvI3hQkTT0g/openapi.json
