# Facebook Page Validator & Brand Slug Checker (`fanndev/facebook-page-validator`) Actor

Bulk-check Facebook slugs: which resolve to a real page, and whether each page actually belongs to the brand you expected. Facebook returns HTTP 200 for pages that do not exist, so status-code checks pass every dead slug. Returns followers, category and public contacts. No login, no proxy.

- **URL**: https://apify.com/fanndev/facebook-page-validator.md
- **Developed by:** [Faisal Ahdan naufal](https://apify.com/fanndev) (community)
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.30 / 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.
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?

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 Page Validator & Brand Slug Checker

Check a list of Facebook slugs and get back, for each one, whether it resolves to a real page — and whether that page actually belongs to the brand you expected.

**Facebook answers HTTP 200 for pages that do not exist.** A slug you invented, a slug from a stale CRM export and a slug belonging to a real company all come back `200 OK`. Any checker built on status codes marks all three as valid. This actor distinguishes them.

### The problem it solves

Two different failures hide in a list of Facebook URLs, and both look fine from the outside:

**The slug is dead.** `facebook.com/some-company-that-rebranded` returns 200 with an empty shell. Your outreach tool records a valid link, your team clicks it in three weeks and finds nothing.

**The slug is alive but belongs to somebody else.** This is the one that costs you. `facebook.com/bukalapak` is not the Indonesian marketplace — it is an app page called *Eoddata beeip.com* with 124 followers. Guessing brand slugs from company names produces these constantly, and an existence check waves every one of them through.

Give this actor an expected brand per slug and it reports `EXACT`, `STRONG`, `WEAK` or `MISMATCH` against the page's real name.

### What you get per slug

| Field | What it tells you |
|---|---|
| `exists` / `status` | Does the slug resolve to a page at all |
| `name` / `pageId` | Who the page actually is |
| `brandMatch` / `brandMatchScore` | Whether that is who you expected |
| `followers` / `followersLabel` | Audience size, plus the label Facebook rendered |
| `primaryCategory` / `categories` | How Facebook classifies the page |
| `phones` / `websites` / `emails` / `addresses` | Public contact details from the about tab |
| `payloadTier` / `postsAreTrustworthy` | How complete Facebook's answer was — see below |

### Quick start

Existence only:

```json
{ "slugs": ["nike", "starbucks", "some-slug-you-are-unsure-about"] }
```

Existence plus brand verification:

```json
{
  "pages": [
    { "slug": "bukalapak", "expectedBrand": "Bukalapak" },
    { "slug": "tokopedia", "expectedBrand": "Tokopedia" }
  ],
  "onlyBrandMismatch": true
}
```

`nike`, `facebook.com/nike` and `https://www.facebook.com/nike/` all reduce to the same slug, so you can paste URLs straight from a spreadsheet.

#### Cleaning a list

Set `onlyNotFound: true` to get just the dead slugs — the rows to delete. Set `onlyBrandMismatch: true` to get just the ones pointing at the wrong company — the rows to re-research. The `RUN_SUMMARY` record carries the totals either way, including `mismatchedSlugs`.

### The payload tier, and why it is on every record

Facebook returns one of three response sizes, and the boundaries were measured rather than assumed (20 pages × 3 repeats, deterministic):

| Tier | Size | Meaning |
|---|---|---|
| `HOLLOW` | ~330 KB | No such page. A known-fake control slug returns exactly this. |
| `REDUCED` | ~0.7–1.4 MB | The page exists and the header is present, but Facebook withheld the post feed. |
| `FULL` | ≥1.5 MB | Header plus roughly ten posts. |

`REDUCED` is the tier that misleads people. On it, `postsInPayload` is **0 for pages that post every day** — NASA came back reduced on three consecutive fetches, Coca-Cola carried two posts. It is sticky per page, not a random throttle, so retrying does not clear it.

That is why `postsAreTrustworthy` exists and why this actor does not sell you a posting-frequency metric. A zero post count on a `REDUCED` payload is an absence of evidence, not evidence of absence, and any tool that averages the two together will quietly tell you an active competitor has gone dormant.

Follower count, name, category and contact details are read correctly on `REDUCED` payloads, so those fields are unaffected.

### Limits worth knowing

- **Approximate follower counts.** Facebook renders `39M`, not `39,214,880`. `followers` is the parsed integer; `followersLabel` preserves what was actually shown. For small pages the count is exact.
- **Only public data.** Whatever a logged-out visitor cannot see, this cannot see. Many pages publish no phone or email at all.
- **No post feed, no engagement metrics.** Paginating posts requires a `doc_id` that Facebook no longer ships in page HTML, so it is out of scope by design rather than by omission.
- **Personal profiles are not pages.** `profile.php` and the other reserved paths are rejected rather than guessed at.
- **Brand matching is a signal, not a verdict.** `WEAK` means "a human should look at this". Regional pages like `Kopi Kenangan | South Jakarta` score as `STRONG` against `Kopi Kenangan`, which is usually right but not always what you want.

### Cost and proxy

One request per slug, no login, no cookies, no token handshake. **Leave the proxy off** — this surface applies no TLS fingerprint gate (all 16 profiles tested return 200) and no IP gate, so a proxy adds latency and cost for nothing.

Pages are large, up to ~20 MB each, and Facebook sends no `content-length` and ignores `Range` requests, so the full body must be downloaded to classify it. That is fast in practice (~1s for an 8 MB page) and the actor fetches concurrently; raise `concurrency` to go faster at the cost of memory.

### Output shape

Every record carries `_input`, `_source`, `_scrapedAt` and `recordType`. `recordType` is `PAGE` for a checked slug, `RUN_SUMMARY` once per run, and `ERROR` for a slug that could not be fetched. Three dataset views are provided: **Overview**, **Brand check** and **Contacts**. Optional `exportFormats` also writes JSON, NDJSON, CSV or XLSX to the key-value store.

### Development

```bash
pip install -r requirements.txt
python -m src            # needs an Apify runtime
python test_local.py     # runs the checker against live pages, no Apify needed
```

`CRAWLING_METHOD.md` documents where each field comes from and which markers the parser keys off.

# Actor input Schema

## `slugs` (type: `array`):

One vanity slug or profile URL per line. 'nike', 'facebook.com/nike' and 'https://www.facebook.com/nike/' are all accepted and reduce to the same slug.

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

Use this instead of 'slugs' when you know which brand each slug is supposed to belong to. Each entry is {"slug": "nike", "expectedBrand": "Nike"}. Only entries given here are brand-checked; the others are existence-checked only.

## `startUrls` (type: `array`):

Facebook page URLs, for pasting a list straight out of another actor's dataset.

## `onlyExisting` (type: `boolean`):

Drop every slug that does not resolve to a page.

## `onlyNotFound` (type: `boolean`):

Keep only the dead slugs - the list-cleaning view. Cannot be combined with 'Only pages that exist'.

## `onlyBrandMismatch` (type: `boolean`):

Keep only slugs whose page belongs to a different brand than expected, or matches it only weakly. Needs 'pages' with an expectedBrand.

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

Drop pages below this follower count. Pages whose follower count could not be read are dropped too, since an unknown count cannot be shown to clear the bar.

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

How many slugs to fetch at once. Facebook pages are large (up to ~20 MB each), so raising this speeds the run up but costs memory.

## `emitSummary` (type: `boolean`):

Append one RUN\_SUMMARY record with the totals: how many exist, how many are dead, how many brand mismatches.

## `exportFormats` (type: `array`):

Also write the results to the key-value store in these formats. The dataset is always produced regardless.

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

Optional. This surface applies no TLS or IP gate, so the default direct connection works and a proxy only adds latency and cost. Enable it only if your own network is restricted.

## Actor input object example

```json
{
  "slugs": [
    "nike",
    "bukalapak",
    "zzqq-not-a-real-page-9876543"
  ],
  "pages": [
    {
      "slug": "bukalapak",
      "expectedBrand": "Bukalapak"
    },
    {
      "slug": "tokopedia",
      "expectedBrand": "Tokopedia"
    }
  ],
  "onlyExisting": false,
  "onlyNotFound": false,
  "onlyBrandMismatch": false,
  "concurrency": 5,
  "emitSummary": true,
  "exportFormats": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every slug checked, the run summary and any error records from this run.

# 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 = {
    "slugs": [
        "nike",
        "bukalapak",
        "zzqq-not-a-real-page-9876543"
    ],
    "pages": [
        {
            "slug": "bukalapak",
            "expectedBrand": "Bukalapak"
        },
        {
            "slug": "tokopedia",
            "expectedBrand": "Tokopedia"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fanndev/facebook-page-validator").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 = {
    "slugs": [
        "nike",
        "bukalapak",
        "zzqq-not-a-real-page-9876543",
    ],
    "pages": [
        {
            "slug": "bukalapak",
            "expectedBrand": "Bukalapak",
        },
        {
            "slug": "tokopedia",
            "expectedBrand": "Tokopedia",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("fanndev/facebook-page-validator").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 '{
  "slugs": [
    "nike",
    "bukalapak",
    "zzqq-not-a-real-page-9876543"
  ],
  "pages": [
    {
      "slug": "bukalapak",
      "expectedBrand": "Bukalapak"
    },
    {
      "slug": "tokopedia",
      "expectedBrand": "Tokopedia"
    }
  ]
}' |
apify call fanndev/facebook-page-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fanndev/facebook-page-validator"
        }
    }
}
```

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/t3ESkAAjsGsVY67rb/builds/RhzF75pVJQqc34mlB/openapi.json
