# Facebook Pages Contact Scraper: Email, Phone, WhatsApp (`scrapegeist/facebook-pages-contact-scraper`) Actor

Turn Facebook Page URLs into contact rows: email, phone, WhatsApp, website, address, Instagram, followers, rating. Paste a city page to get every event organizer and venue there. Logged-out, no cookies, no browser. Pages only, personal profiles are skipped and never billed.

- **URL**: https://apify.com/scrapegeist/facebook-pages-contact-scraper.md
- **Developed by:** [Scrapegeist](https://apify.com/scrapegeist) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 facebook page scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Facebook Pages Contact Scraper: Email, Phone, WhatsApp, Instagram

> Paste Facebook Page URLs, get one contact row per Page: email, phone, WhatsApp, website, address, Instagram, followers, rating, category. Paste a city page, get every organizer and venue that runs events there, with activity signals. Logged-out, public data only, no cookies, no browser. Pages only: personal profiles are skipped and never billed.

![Pages view: one contact row per Facebook Page with email, phone, website, address, followers and rating](https://api.apify.com/v2/key-value-stores/AGUDYcJLNDmUeIjNd/records/pages-view.png)

***

### What you get, and how often

Measured on 2026-09-10 over 48 Pages across 9 countries (local businesses, venues, event organizers and large brands, mixed):

| Field | Filled |
|---|---|
| Category | 100 % |
| Followers | 100 % |
| Website | 92 % |
| Email | 52 % overall, 77 to 80 % for Pages under 1M followers |
| Phone | 52 % |
| Address | 46 % |
| Rating (recommend % and review count) | 48 % |
| Instagram | 10 % |
| WhatsApp | 4 % |
| **Any of email, phone, WhatsApp** | **71 % overall, 80 to 92 % for Pages under 1M followers** |

The split is not random: large brands route contact through a support flow and publish nothing on the Page, while local businesses, venues and event organizers publish an email and a phone number because they want to be called. On a sample of 10 Detroit event organizers, 8 of 10 had an email and 6 of 10 had a phone number.

Two fields a logged-out visitor cannot see are therefore not returned: the likes count (Facebook serves followers only) and opening hours (only an open or closed status is served, never the hours themselves).

### What it costs

| Event | Price | When it is charged |
|---|---|---|
| `page-scraped` | $4.00 / 1,000 | Every Page row that reaches your dataset |
| `contact-found` | +$6.00 / 1,000 | The row carries an email, a phone number or a WhatsApp number |
| `activity-scored` | +$2.00 / 1,000 | Only with `activity: "full"`, and only when the organizer's calendar was resolved |

These are the Free-tier rates. Bronze, Silver and Gold plans pay less per 1,000; the Store header shows the Gold rate.

**Worked example.** 100 venue Pages, 80 of them with a contact:

```
100 pages scraped x $4 / 1,000  = $0.40
 80 with contact  x $6 / 1,000  = $0.48
────────────────────────────────────────
                          total = $0.88
```

A Page without contact data costs the base price only. Skipped and failed inputs cost nothing: dead vanities, personal profiles, HTTP 404s and login walls produce a row you can filter on and no charge. Compute and the residential proxy are included in the prices above and are never billed on top.

`maxPages` is a hard ceiling. It is checked before each fetch, not after the row lands, so the number you type is the most Page rows you can be charged for.

There is a second, unbilled cap behind it: a fetch budget of `max(20, 3 x maxPages)` page fetches per run. It exists so a list that is mostly dead URLs cannot keep spending proxy traffic forever. When the budget runs out, the remaining inputs get a `skipped` row that says so, and raising `maxPages` raises the budget with it.

### Quickstart

Paste into **Facebook Page URLs, vanities or ids**, one per line. All of these work:

```
https://www.facebook.com/FillmoreDetroit
https://www.facebook.com/FillmoreDetroit/about
https://www.facebook.com/pg/FillmoreDetroit/about
TEDxAmsterdam
100064012345678
https://www.facebook.com/profile.php?id=100064012345678
https://www.facebook.com/people/Some-Page/100064012345678/
```

A Page's own tabs (`/about`, `/events`, `/photos` and the rest) resolve to the Page itself, so you can paste whatever tab you were looking at. Objects of the shape `{ "url": "..." }` are accepted too, so the output of another Apify crawler can be piped in without reshaping. A missing or mangled `https://` is repaired.

Event, group and place URLs are not Pages. Put them in the `places` field if they are city or place URLs, otherwise they come back as an error row that names the accepted shapes.

No URL list? Paste a city into **City / place pages for organizer discovery**:

```
https://www.facebook.com/places/Things-to-do-in-Detroit-Michigan/114586701886732/
https://www.facebook.com/events/explore/Things-to-do-in-Detroit-Michigan/114586701886732/
```

Both forms of the same city URL work. The two lists run in one run, and a Page reached by both is emitted once.

### City mode: from a city to its organizers

The actor reads Facebook's public event-discovery feed for the city, walks up to `maxEventsPerPlace` events of it, collects the organizer and venue Pages behind those events, and scrapes one contact row per organizer. Roughly one organizer per event in practice.

The city feed does not name the host, so each event costs one event-detail fetch to resolve it. Those detail fetches are not billed: you pay for the Page rows that come out, not for the walk.

![Organizer leads view: Detroit organizers with contact fields, events in feed, next event, responses and ticketing platform](https://api.apify.com/v2/key-value-stores/AGUDYcJLNDmUeIjNd/records/leads-view.png)

`activity` decides what the organizer rows carry on top of the contact fields:

| `activity` | Adds | Extra cost |
|---|---|---|
| `"feed"` (default) | Events in feed, next event (date, name, URL), attendee total across those events, ticketing platforms detected on their ticket links | none |
| `"full"` | The above, plus the upcoming event count and the number of events in the last 90 days, read from the organizer's own calendar | +$2 / 1,000 |
| `"none"` | Nothing. Contact fields only, `activity` stays `null` | none |

`maxEventsPerPlace` bounds the walk (default 100, range 1 to 1,000). `maxPages` (default 100, range 1 to 10,000) still bounds the whole run on top of it, and the actor stops walking as soon as it has as many distinct organizers as that cap allows.

What this is: Facebook's own curated ranking of suggested events for that city. It is high-quality discovery of who is active there, not a complete index of everything happening in the city.

### Dataset views

The run writes one row per Page, plus a row for every skipped or failed input. `type` tells you which kind: `page`, `skipped` or `error`. One city input can therefore produce many Page rows, and a Page reached twice (pasted directly and found again in a city feed) is emitted once.

Two table views ship with the actor:

- **Pages**: the contact columns for every Page row.
- **Organizer leads**: the same contact columns plus the activity columns (events in feed, next event, responses, ticketing, upcoming count, past 90 days). A view selects columns, it cannot filter rows, so rows that came from direct Page input appear here too, with the activity columns empty.

**All rows** exports everything untransformed, including the skipped and error rows. Export as JSON, CSV, Excel, or through the Apify API.

![One Page row as JSON with every field](https://api.apify.com/v2/key-value-stores/AGUDYcJLNDmUeIjNd/records/json-row.png)

### Field reference

Every `page` row:

| Field | Type | Description |
|---|---|---|
| `type` | string | `"page"` for a scraped Page. |
| `id` | string | Stable Facebook Page id. |
| `profileId` | string or null | Profile id used in `/<id>` URLs and in event host references. |
| `name` | string | Page name. |
| `vanity` | string or null | Vanity handle, when the Page has one. |
| `url` | string | Canonical Facebook URL for the Page. |
| `category` | string or null | Facebook's own category, for example "Live Music Venue". |
| `isVerified` | boolean | Blue badge shown on the profile. |
| `followers` | number or null | Follower count as a number. |
| `followersText` | string or null | Follower line as Facebook rendered it, for example "8.2K followers". |
| `rating` | object or null | `{ percent, reviews }`: share of recommendations and how many reviews it is based on. |
| `email` | string or null | Contact email from the intro card. |
| `phone` | string or null | Phone number, digits with a leading plus. |
| `phoneText` | string or null | Phone number as printed on the Page. |
| `whatsapp` | string or null | WhatsApp number, normalised like `phone`. |
| `website` | string or null | Website URL, unwrapped from Facebook's `l.facebook.com` redirect. |
| `websiteHost` | string or null | Host as the Page displays it, for example "thefillmoredetroit.com". |
| `address` | string or null | Street address from the intro card. |
| `addressMapsUrl` | string or null | Facebook's map link for that address. |
| `instagram` | string or null | Instagram handle linked from the Page. |
| `otherAccounts` | array | Every other linked account: `{ platform, handle, url }`. |
| `priceRange` | string or null | Price band the Page publishes, for example "$$". |
| `description` | string or null | Page description text. |
| `contextualName` | string or null | Location line Facebook shows under the name, for example "Cologne, Germany". |
| `profilePicture` | object | `{ small, medium, large }` image URLs. |
| `coverPhoto` | object or null | `{ imageUri, accessibilityCaption }`. |
| `hasContact` | boolean | True when email, phone or WhatsApp is present. This is the field that decides the `contact-found` charge. |
| `likelyNaturalPerson` | boolean | Category heuristic, see below. |
| `activity` | object or null | Activity block for rows discovered through a city feed, null otherwise. |
| `discoveredVia` | string | `"input"` for a Page you pasted, `"place:<id>"` for one found in a city feed. |
| `inputUrl` | string | The exact input this row came from, so it joins back to your source list. |
| `scrapedAt` | string | ISO timestamp of the fetch. |

The `activity` object holds `eventsInFeed`, `nextEventAt`, `nextEventName`, `nextEventUrl`, `usersRespondedTotal`, `ticketing`, `upcomingEventsCount` and `pastEvents90d`. The last two are null unless `activity: "full"` was set.

Flat columns are written next to the nested objects so the table views can show them, and so a CSV export stays readable:

| Column | Mirrors |
|---|---|
| `avatarUri` | `profilePicture.medium`, falling back to small then large |
| `ratingPercent`, `ratingReviews` | `rating.percent`, `rating.reviews` |
| `eventsInFeed` | `activity.eventsInFeed` |
| `nextEventAt`, `nextEventName`, `nextEventUrl` | the matching `activity` fields |
| `usersRespondedTotal` | `activity.usersRespondedTotal` |
| `ticketingLabels` | `activity.ticketing`, joined with commas |
| `upcomingEventsCount`, `pastEvents90d` | the matching `activity` fields |

A `skipped` row carries `inputUrl`, `reason` (`personal-profile`, `not-found` or `cap-reached`), `message` and `discoveredVia`. An `error` row carries `inputUrl`, `message` and `discoveredVia`, and that is where a rejected input shape or a failed fetch lands. Neither is billed.

### `likelyNaturalPerson`

Facebook Pages are business objects, but some of them belong to one human being: a DJ, a photographer, a solo artist. Outreach rules treat those as personal data of a natural person, which is a stricter regime than a company mailbox.

`likelyNaturalPerson` flags them. It is a heuristic and it uses one signal: Facebook's own category. Categories such as Artist, Musician/band, Public figure, DJ, Digital creator and Athlete set the flag; Restaurant, Nonprofit Organization or Live Music Venue do not.

It is a category match, so it inherits Facebook's category choices. A running club that picked "Athlete" as its category is flagged even though it is a club, not a person. Read the flag as "check this one before you contact it", not as a verdict.

Recommendation: exclude flagged rows from outreach, or review them by hand first.

### Legal and responsible use

This actor reads only what a logged-out visitor sees on a public Facebook Page. No login, no cookies, no account, no circumvention of a technical access control.

The output is research, enrichment and verification data. It is not an opt-in marketing list, and it does not become one because it is in a dataset. If you contact anyone in it, you are the controller for that processing, and you are responsible for the rules that apply to you: GDPR, UWG § 7 in Germany, CAN-SPAM in the US, PECR in the UK, and the equivalents elsewhere. Business-to-business contact by email generally needs a lawful basis and, in several of these regimes, prior consent. Check before you send.

Every row carries `inputUrl` and `scrapedAt`, so you can show where a record came from and when.

#### Data notice for Page owners

What is collected: the public contact block of a Page (email, phone, WhatsApp, website, address, linked accounts), its name, category, follower count, rating summary, description and images, and for city pages the public event feed that names the hosting Pages. Personal profiles are detected and skipped. Friend lists, group members and attendee lists are never read.

Who processes it and why: the customer who starts a run chooses the input and receives the output, and is the controller for any use of it. The operator of this actor processes the data only to deliver it to that customer and keeps no copy beyond the run storage the customer configures. Legal basis for the collection is legitimate interest (GDPR Art. 6(1)(f)): the details are published by the Page owner for the purpose of being contacted, the data is limited to business contact details, and the `likelyNaturalPerson` flag lets customers exclude sole traders and personal-name Pages.

Your rights: to have your Page excluded from future runs, to learn whether it was included, or to have data erased on the operator's side, open an issue on this actor's Store page (the **Issues** tab) with the subject "Exclude my Page" and the Page URL. Exclusions are applied within 30 days and checked on every run. You also have the right to complain to a supervisory authority.

### FAQ

**Do I need cookies or a Facebook login?**
No. The actor reads public Page data as a logged-out visitor would.

**Why a residential proxy?**
Facebook blocks datacenter IPs. The default `RESIDENTIAL` group of Apify Proxy is enough and needs no setup. You can pin a country in the proxy editor if you want a specific region.

**A Page shows an email in my browser, but the row has none. Why?**
Facebook hides the intro card on some very large Pages when you are logged out, so there is nothing public to read. Try the Page's numeric id instead of its vanity: it sometimes renders a different variant of the profile. If the field is still empty, open an issue with the URL and we will look at that Page.

**Can you find a Page by name?**
No. Facebook has no public page search for logged-out visitors, so there is nothing to paginate. Paste URLs, vanities or ids, or paste a city page and let the event feed surface the active organizers there.

**What about opening hours or the likes count?**
Neither is served to a logged-out visitor. Facebook shows an open or closed status instead of the hours, and a follower count instead of likes. The actor returns followers and does not guess at the rest.

**What happens to personal profiles in my list?**
They are detected on the fetched document, emitted as a `skipped` row with reason `personal-profile`, and never billed. The same holds for dead vanities and Pages that 404.

**A run failed, what now?**
Re-run it. Failed and skipped rows are never billed, so a failed run costs nothing. If every page fetch in a run fails, the actor stops early and says so in the log with `[ALERT][BLOCKED]`, which usually means the proxy pool needs a moment rather than that your input is wrong.

### Support

Use the **Issues** tab on this actor's Apify Store page for bugs and feature requests. Include the Page URL when a field is missing, that is what makes a fix possible. Failed rows are never billed, so a broken run costs you nothing while we investigate.

# Actor input Schema

## `pages` (type: `array`):

Page URLs (https://www.facebook.com/FillmoreDetroit), bare vanities (TEDxAmsterdam), numeric page ids, profile.php?id= URLs, or /people/<name>/<id>/ URLs. One row per Page with email, phone, WhatsApp, website, address, Instagram, followers and rating. Personal profiles are skipped and never billed.

## `places` (type: `array`):

Paste a Facebook city page (https://www.facebook.com/places/Things-to-do-in-Detroit-Michigan/114586701886732/ or the /events/explore/ form). The actor walks the city's event feed, collects every organizer and venue behind those events, and returns one contact row per organizer with activity signals (events in feed, next event, attendee total, ticketing platform).

## `maxPages` (type: `integer`):

Hard cap on billed page rows. Checked before each fetch, so the run cannot overshoot this number.

## `maxEventsPerPlace` (type: `integer`):

How many events of a city feed are read to discover organizers. Each event costs one detail fetch (not billed). Around 1 organizer per event in practice.

## `activity` (type: `string`):

Only for rows discovered via a city page. "feed": events in feed, next event, attendee total, ticketing platforms (free). "full": also upcoming event count and events in the last 90 days from the organizer's own calendar (+$2 per 1,000). "none": contact fields only.

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

Residential proxy is required. Facebook blocks datacenter IPs.

## Actor input object example

```json
{
  "pages": [
    "https://www.facebook.com/FillmoreDetroit",
    "TEDxAmsterdam"
  ],
  "places": [],
  "maxPages": 100,
  "maxEventsPerPlace": 100,
  "activity": "feed",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `pages` (type: `string`):

One row per Page with contact fields.

## `leads` (type: `string`):

Pages discovered via a city feed, with activity signals.

## `allItems` (type: `string`):

Every dataset row, untransformed.

# 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 = {
    "pages": [
        "https://www.facebook.com/FillmoreDetroit",
        "TEDxAmsterdam"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapegeist/facebook-pages-contact-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 = { "pages": [
        "https://www.facebook.com/FillmoreDetroit",
        "TEDxAmsterdam",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("scrapegeist/facebook-pages-contact-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 '{
  "pages": [
    "https://www.facebook.com/FillmoreDetroit",
    "TEDxAmsterdam"
  ]
}' |
apify call scrapegeist/facebook-pages-contact-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapegeist/facebook-pages-contact-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/ThJXFkYw3cSf7aNHt/builds/4BbJGdusRU3SoQ6Mu/openapi.json
