# Facebook Page Contacts Scraper - Emails, Phones, Followers (`snow_leo_data/facebook-pages-scraper`) Actor

Facebook page followers, contacts and reviews: 53 columns per page, measured on 22 live pages - website 22/22, phone 12/22, email 10/22, plus ratings, address and ad transparency. A login wall is a cooldown, not a verdict: 9 of 9 pages delivered through 34 walls.

- **URL**: https://apify.com/snow\_leo\_data/facebook-pages-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Social media, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.99 / 1,000 result rows

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

## Facebook Pages Scraper

Scrape public Facebook pages **without a login, without cookies, without a
proxy and without a browser**. Give it a page name, a page URL, a
`profile.php?id=` link or a numeric page id and get back the page's contacts,
location, rating, transparency record and recent posts as one flat row.

Python standard library only. Nothing is written to Facebook, nothing is logged
in to, no account is used.

### What one row carries

**53 columns.** Measured on 2026-09-17 across 22 live pages:
22 of the 24 addresses passed in resolved to a real page, and the other two did
not exist - they were detected and not charged for.

| group | columns | filled on the 22 live pages |
|---|---|---|
| identity | `page_name`, `page_id`, `profile_id`, `username`, `page_url`, `is_verified`, `categories`, `intro` | name 22/22, page id 22/22, categories 22/22, intro 21/22 |
| audience | `followers`, `following`, `talking_about`, `checkins`, `likes` | followers 22/22, talking about 20/22, check-ins 11/22 |
| contacts | `email`, `emails`, `phone`, `phones`, `website`, `websites`, `messenger_url`, `social_links` | email 10/22, phone 12/22, website 22/22, other socials 5/22 |
| location | `address`, `address_street`, `address_city`, `address_region`, `address_country`, `address_postal_code`, `latitude`, `longitude`, `service_area` | address 8/22, coordinates 7/22 |
| business | `price_range`, `services`, `hours_status`, `rating_recommend_percent`, `rating_stars`, `review_count`, `founded` | price 5/22, hours 7/22, rating 9/22, reviews 10/22, founded 3/22 |
| transparency | `page_created_date`, `confirmed_owner`, `is_running_ads` | created 21/22, owner 18/22, ad status 21/22 |
| media | `profile_picture_url`, `cover_photo_url` | 22/22 each |
| posts | `latest_posts`, `latest_post_url`, `latest_post_time`, `post_count_returned` | 189 posts across the 22 pages, 183 of them with text |
| run | `input`, `source_urls`, `scraped_at`, `page_exists`, `change_type`, `changed_fields`, `about_fields` | always |

`about_fields` carries every About-tab field that has no column of its own, so
a field Facebook adds tomorrow still reaches you.

### The post ceiling, stated plainly

Facebook embeds a limited number of posts in a logged-out page and there is no
way past it without a session token. Measured on 2026-09-17:

- a page that publishes reels returns **10 posts** - measured on 18 of the 22
  live pages;
- a page that posts only to its timeline returns **1 post** - NASA, Tesla;
- `/api/graphql/` answers **HTTP 200 with zero bytes** without a session
  token, so the `end_cursor` embedded in the page cannot be spent. Tested from
  a laptop and from an Apify cloud run - same answer;
- the Facebook Ad Library answers **HTTP 403** from both, so ad creatives are
  not available here either;
- `mbasic.facebook.com` redirects to the login page.

**Reaction and comment counts come with the newest post only.** Measured: 21 of
the 22 pages carried them, and in every case on the first post in the list.
Older posts arrive with text, time, permalink and type, and with
`reactions`, `comments` and `shares` as `null`. That is what the source publishes, not a shortcut.

If you need every post of a page rather than the most recent ones, this is not
the Actor for that.

### Other honest limits

- **Like counts no longer exist.** Facebook stopped printing a Like count on
  logged-out pages. Measured across 22 live pages: zero carried one. The
  `likes` column is kept so pipelines do not break, and it is `null`.
- **Opening hours come as a status, not a weekly table.** A logged-out page
  publishes `Open now`, `Closing Soon`, `Temporarily Closed` - never the
  Monday-to-Sunday rows. `hours_status` is that word, unchanged.
- **Ratings come as "% recommend", not stars.** That is how Facebook shows
  them today; across 22 live pages none carried a star rating. Both columns
  exist, and `rating_stars` fills in if a page still has one.
- **Admin countries are not published** on the transparency tab of a logged-out
  page. What is published - and what this Actor returns - is the page creation
  date, the confirmed legal owner and the ad status.
- **Facebook answers HTTP 200 for a page that does not exist.** Such a row is
  detected, not written to the dataset and counted in the REPORT record, so you
  are not charged for it.

### Four ways to name a page

All four were tested live, from a laptop and from an Apify cloud run:

| written as | example |
|---|---|
| page name | `katzsdeli` |
| page URL | `https://www.facebook.com/katzsdeli/` |
| profile.php link | `https://www.facebook.com/profile.php?id=100064806517961` |
| numeric page id | `100064806517961` |

A bare numeric id is rewritten into the `profile.php?id=` form before the
request, because the bare form answers from a home address and is sent to the
**login wall from Apify's** - measured in cloud run `8jRZ5YqcNeUBX9xAx`.

Addresses of Facebook's own surfaces - `/groups/`, `/events/`, `/watch/`,
`/marketplace/`, `/reel/` - are not pages. They are rejected, listed back in
the REPORT record and never silently scraped as something else.

### Monitor mode

Turn on **Only new or changed pages** and the Actor remembers what it has
already delivered. Every row is then labelled `NEW`, `UPDATED` or `UNCHANGED`,
and an `UPDATED` row carries `changed_fields` with the value before and after.

Watched: name, categories, intro, verified badge, website, email, phone,
address, price range, hours status, services, rating, review count, ad status,
confirmed owner, page creation date, newest post, and the follower count **in
1% buckets** - a page gaining a handful of followers overnight is not a change,
or a daily run would charge you for every page every morning.

Two things are deliberately NOT treated as changes, because Facebook varies
them by itself: the exact follower count, compared in 1% buckets, and the
spelling of a phone number - the same page published `+1 212-254-2246` and
`(212) 254-2246` in two cloud runs an hour apart, and comparing the strings
would have charged for a change that never happened.

Memory lives in a named key-value store and survives between runs. Verified on
the platform under the Limited permission level: after a monitor run the store
`facebook-pages-state` held three page fingerprints and the next run read them
back.

### Empty results always say why

A run that returns nothing names the reason in the REPORT record
(`empty_reason`) and in the log, with one phrase per cause. Fifteen of them
exist; nine are decided from your input **before a single byte is
downloaded**, so a self-contradicting input costs you nothing:

`no-target-given`, `input-dataset-unreadable`, `all-targets-invalid`,
`posts-filter-with-posts-turned-off`,
`creation-date-filter-with-transparency-turned-off`,
`ads-filter-with-transparency-turned-off`,
`category-filter-is-empty`, `follower-range-is-empty`,
`charge-limit-reached`, `filtered-out`, `nothing-changed`,
`pages-not-found`, `source-asked-for-login`, `source-rate-limited`,
`all-targets-unreachable`.

`Max pages in the result` limits **delivered** rows, not fetched ones - a
narrow filter plus a small limit still returns rows instead of an empty
dataset.

### Speed and traffic

A page with autoplaying reels ships megabytes of video manifests after the data
that matters. Measured: KFC's About tab is **21 157 880 bytes**, and the last
field this Actor reads sits at byte **576 576**. The Actor reads the first
900 000 bytes and hangs up - **4% of the bytes for the same data**. With posts
turned on the About tab is read to 1 700 000 bytes, because the tenth reel post
completes at byte 1 400 000 at the worst measured.

Turning **Include latest posts** off drops one request per page and reads 47%
fewer bytes of the About tab.

#### Facebook rations what one anonymous address may look at

It does not say so with HTTP 429 - it redirects to the login page. Four cloud
runs on 2026-09-17 measured the shape of it:

- run `acOmd7YZmASDlaMKT`: 15 pages through, then a wall on every request;
- run `KM9AVgJkSdOZbheCg`: the same 15, and hammering the wall every 2 seconds
  kept it shut through 60, 120 and 180 seconds of waiting;
- run `Zqpa3PjCTZ1bTYbfa`: 18 pages through with no wall at all, and one
  minute of silence was enough afterwards;
- the Actor itself, run `ZluwxVKMzuIGdOswF`: all 9 pages delivered - 54
  requests, 34 walls, 26 retries, 345 seconds.

The ration belongs to the outbound address, which a cloud container does not
choose: one run lands on a fresh address and never sees a wall, the next lands
on a spent one. From a home address the same code made 68 requests four at a
time without a single redirect.

So the Actor treats a login redirect as a **cooldown, never a verdict**: it
waits a minute and tries again, and it does not hammer, because hammering is
what kept the wall shut for six minutes while one minute of silence cleared it.
Defaults are two pages at a time, 1.5 seconds apart. If the report says
`source-asked-for-login`, lower **Pages at a time** or raise **Seconds between
requests** - and expect a large list of pages to take minutes rather than
seconds.

### Input

| field | what it does |
|---|---|
| Facebook pages | one page per line, in any of the four spellings |
| Include latest posts | fetch the main tab too and return recent posts |
| Include page transparency | fetch creation date, confirmed owner, ad status |
| Max posts per page | keep at most this many posts per row |
| Max pages in the result | stop after this many delivered rows |
| Only pages with an email / phone / website / address | contact filters |
| Only verified pages | keep pages with the badge |
| Only pages currently running ads | needs transparency on |
| Only pages with at least one post | needs posts on |
| Category contains | keep a page whose category contains any of these words |
| Minimum / Maximum followers | follower range; a page with no count is kept |
| Page created on or after / before | `YYYY-MM-DD`; needs transparency on |
| Monitor mode / Emit unchanged | only new or changed, or everything with labels |
| Compact output | drop empty fields - smaller payload for AI agents |
| Pages at a time, Seconds between requests | pace |
| Read pages from a dataset | chain this Actor after another one |

### Output

```json
{
  "page_name": "Joe's Stone Crab",
  "page_url": "https://www.facebook.com/JoesStoneCrab/",
  "page_id": "404077308008",
  "profile_id": "100064737608769",
  "is_verified": false,
  "categories": ["Seafood Restaurant"],
  "intro": "For more than 100 years, no visit to Miami has been complete without stopping here.",
  "followers": 44529,
  "talking_about": 433,
  "checkins": 170687,
  "email": "marketing@joesstonecrab.com",
  "phone": "+1 305-673-0365",
  "website": "http://joesstonecrab.com/",
  "social_links": [
    {"platform": "instagram", "url": "https://www.instagram.com/joesstonecrab",
     "handle": "joesstonecrab", "label": "Instagram"}
  ],
  "address": "11 Washington Ave, Miami Beach, FL, United States, 33139",
  "address_street": "11 Washington Ave",
  "address_city": "Miami Beach",
  "address_region": "FL",
  "address_country": "United States",
  "address_postal_code": "33139",
  "latitude": 25.76885,
  "longitude": -80.13503,
  "price_range": "$$",
  "services": ["Dine-in", "Outdoor seating", "Online booking"],
  "hours_status": "Temporarily Closed",
  "rating_recommend_percent": 94,
  "review_count": 14553,
  "page_created_date": "March 8, 2010",
  "confirmed_owner": "JOES STONE CRABS INC",
  "is_running_ads": false,
  "founded": "October 15, 1913",
  "messenger_url": "https://m.me/JoesStoneCrab",
  "post_count_returned": 10,
  "latest_posts": [
    {"post_id": "1518368763664363",
     "url": "https://www.facebook.com/JoesStoneCrab/posts/pfbid03618inXBNsvUPevcWyGNHmFLQLnHEW6qeM1agoYpofNwmx3ij3mYEWM3NCvgRoSTzl",
     "created_at": "2026-09-15T15:18:27Z",
     "text": "Joe's Stone Crab and Joe's Take Away will be closed beginning...",
     "reactions": 290, "comments": 12, "shares": 17, "type": "post"}
  ]
}
```

### FAQ

#### Do I need a Facebook account, a cookie or a proxy?

No. Every request is what an anonymous visitor's browser sends, and the only
thing that matters is the header set: with a plain `Accept: */*` Facebook
answers **HTTP 400**, with a full browser header set it answers 200 and the
data. Measured from a laptop and from an Apify cloud run - same result both
times.

#### How many posts do I get per page?

Ten on a page that publishes reels, one on a page that only posts to its
timeline. That is the whole of what Facebook embeds in a logged-out page, and
`/api/graphql/` answers 200 with an empty body without a session token, so
there is no honest way past it. See "The post ceiling, stated plainly".

#### Can it scrape a personal profile?

Yes, when the profile is public - a public profile uses the same page shell and
the same About tab. Fields a profile does not have simply arrive empty.

#### Why is the `likes` column always empty?

Because Facebook no longer prints a Like count on a logged-out page. Measured
across 22 live pages on 2026-09-17: none of them carried one. The column is
kept so a pipeline built on it does not break.

#### Can I get the weekly opening hours?

No. A logged-out page publishes the status word only - `Open now`,
`Closing Soon`, `Temporarily Closed` - not the Monday-to-Sunday table.

#### Can I get the ads a page is running?

You get whether it is running ads at all, from the Page transparency tab. The
creatives live in the Facebook Ad Library, which answers **HTTP 403** to a
logged-out request from a laptop and from Apify alike.

#### What happens if I pass a page that does not exist?

Facebook answers HTTP 200 with a stub. The Actor detects it, does not write the
row and counts it in the REPORT record, so it is not charged for. Only the
About tab is spent on such an address, never all three.

#### How do I run this on a schedule without paying for the same page twice?

Turn on **Only new or changed pages**. Every page already delivered comes back
only when a watched field moved, and follower counts are compared in 1% buckets
so ordinary growth is not a change.

#### Can I feed it the output of another Actor?

Yes. Put that run's dataset id in **Read pages from a dataset** and name the
field holding the page address.

### Legal

Only public pages are read, exactly as an anonymous visitor sees them. No
login, no cookies, no account, nothing behind a permission wall. You are
responsible for using the data in line with Facebook's terms and with the
privacy law that applies to you.

# Actor input Schema

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

One page per line. Four spellings work: a page name (katzsdeli), a full page URL (https://www.facebook.com/katzsdeli/), a profile.php?id= link, or a bare numeric page id. Addresses of Facebook's own surfaces - /groups/, /events/, /watch/, /marketplace/ - are not pages and are listed back as rejected.

## `includePosts` (type: `boolean`):

Fetch the page's main tab as well and return the recent posts embedded in the page: text, time, permalink, reactions and comment count. Measured ceiling without login: 10 posts on a page that publishes reels, 1 on a page that does not. Turning this off drops one request per page and reads 47% fewer bytes of the About tab.

## `includeTransparency` (type: `boolean`):

Fetch the Page transparency tab: the date the page was created, the legal entity Facebook names as responsible for it, whether it is currently running ads, and the Facebook Page ID.

## `maxPostsPerPage` (type: `integer`):

Keep at most this many posts in each row. 0 keeps everything the page carried.

## `maxItems` (type: `integer`):

Stop after this many rows are DELIVERED. Filters below run after the page is fetched, so the limit counts delivered rows, not scraped ones. 0 means no limit.

## `onlyWithEmail` (type: `boolean`):

Keep only pages that publish a contact email.

## `onlyWithPhone` (type: `boolean`):

Keep only pages that publish a phone number.

## `onlyWithWebsite` (type: `boolean`):

Keep only pages that link a website.

## `onlyWithAddress` (type: `boolean`):

Keep only pages that publish a street address.

## `onlyVerified` (type: `boolean`):

Keep only pages that carry the verified badge.

## `onlyRunningAds` (type: `boolean`):

Needs 'Include page transparency' on - that is where the ad status lives.

## `onlyWithPosts` (type: `boolean`):

Needs 'Include latest posts' on. Asked for with posts turned off, the run stops before the first request and says so instead of returning an empty dataset.

## `categoryContains` (type: `array`):

Keep a page when any of its categories contains one of these words. Case is ignored.

## `minFollowers` (type: `integer`):

Keep pages with at least this many followers. A page whose follower count Facebook did not print is kept.

## `maxFollowers` (type: `integer`):

Keep pages with at most this many followers. A page whose follower count Facebook did not print is kept.

## `createdAfter` (type: `string`):

YYYY-MM-DD. Needs 'Include page transparency' on. A page whose creation date is missing is kept.

## `createdBefore` (type: `string`):

YYYY-MM-DD. Needs 'Include page transparency' on.

## `onlyNewOrChanged` (type: `boolean`):

Remember what was delivered and return only pages that are new or whose watched fields moved. Every row is labelled NEW, UPDATED or UNCHANGED and an UPDATED row lists what changed. Follower counts are compared in 1% buckets so a page gaining a handful overnight does not count as changed.

## `emitUnchanged` (type: `boolean`):

Keep the NEW / UPDATED / UNCHANGED labels but return every page, including the unchanged ones.

## `compactOutput` (type: `boolean`):

Drop empty fields from every row. Smaller payload for AI agents and for pipelines that do not want null columns.

## `concurrency` (type: `integer`):

How many pages to scrape in parallel. Measured from an Apify cloud run: three pages at a time 0.6 s apart got 18 requests through and then every request was redirected to Facebook's login page. Higher is faster and more likely to meet that.

## `requestGapSeconds` (type: `string`):

Minimum pause between two requests to Facebook. Leave empty for the measured default of 1.5 s.

## `inputDatasetId` (type: `string`):

Id of an Apify dataset whose rows carry page addresses - the output of another Actor, for instance.

## `pageField` (type: `string`):

Which field of that dataset to read. Default page\_url.

## Actor input object example

```json
{
  "pages": [
    "katzsdeli"
  ],
  "includePosts": true,
  "includeTransparency": true,
  "maxPostsPerPage": 10,
  "maxItems": 0,
  "onlyWithEmail": false,
  "onlyWithPhone": false,
  "onlyWithWebsite": false,
  "onlyWithAddress": false,
  "onlyVerified": false,
  "onlyRunningAds": false,
  "onlyWithPosts": false,
  "categoryContains": [],
  "onlyNewOrChanged": false,
  "emitUnchanged": false,
  "compactOutput": false,
  "concurrency": 2,
  "pageField": "page_url"
}
```

# Actor output Schema

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

All collected rows

# 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": [
        "katzsdeli"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/facebook-pages-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": ["katzsdeli"] }

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/facebook-pages-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": [
    "katzsdeli"
  ]
}' |
apify call snow_leo_data/facebook-pages-scraper --silent --output-dataset

```

## MCP server setup

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