Skip to content

ChocoData-com/facebook-profile-scraper

Repository files navigation

Facebook Profile Scraper

Facebook Profile Scraper

Facebook Profile Scraper for extracting profile names, usernames, profile IDs, likes and profile pictures from Facebook.com. This repo has a free Facebook profile web scraping script you can run right now, and a Facebook profile data API that turns a handle into structured JSON.

Last updated: 2026-07-20. Working against Facebook.com as of July 2026, and re-verified whenever Meta changes their markup.

This repo reads public organisation profiles only, and collects no personal data. Every capture, screenshot and example here targets an organisation's own public Facebook presence (NASA, National Geographic, BBC News, the British Museum, Nike). Nothing in this repo reads, stores or renders a private individual's profile, name, photo or comments. The endpoint resolves any Facebook handle, so pointing it at a person is technically possible and deliberately out of scope here: keep it on brand, business and organisation profiles, which is what every sample, script default and image in this repo uses. Facebook Pages, posts, groups and events have their own reference in the Facebook Scraper repo.

Every JSON block on this page was captured from the live API on 2026-07-20. Long CDN image URLs are truncated where marked and each block says exactly what was cut; every field shown is verbatim. Full uncut samples are committed in facebook_profile_scraper_api_data/, so you can diff this page against them. Every code example calls the actual API and is runnable from facebook_profile_scraper_api_codes/.

pip install requests
export CHOCODATA_API_KEY="your_key"     # free: 1,000 requests, one-time, no card
python facebook_profile_scraper_api_codes/profile.py NASA

Those three lines return this, live from Facebook.com:

{
  "id": "100044561550831",
  "username": "NASA",
  "name": "NASA - National Aeronautics and Space Administration",
  "category": null,
  "about": "NASA - National Aeronautics and Space Administration. 28,656,732 likes · 20,516 talking about this. Explore the universe and discover our home planet. \nThere's space for everybody. ✨",
  "is_verified": null
}

...that is 6 of the 11 fields on a profile; the full 11 are below. The 28.6 million likes are in there, as text, which is the thing to know about this surface. Point it at a list of handles and you get a row each:

Retrieved Facebook profile data

That is the whole point of this repo. The rest of this page is how it works, starting with the free Facebook profile scraper: one HTTP request, no key, real profile data.


Contents


Free Facebook Profile Scraper

Facebook server-renders a public organisation profile's name, about text and picture into its OpenGraph <meta> tags, so you can extract profile data without a headless browser, JavaScript rendering, or a login. No key, no cost:

python free_scraper/facebook_profile_free_scraper.py NASA

Source: free_scraper/facebook_profile_free_scraper.py. It reads og:title, og:description, og:image and og:url, emits username, url, name, about, profile_image, and runs parse_about() over the about text to recover the likes and talking-about figures. It parses first and only reports "no data" when the OpenGraph block is genuinely absent, so a profile that renders is never miscounted as a block.

After running the command, your terminal should look something like this:

Free Facebook profile scraper

That works, and it kept working: 25 plain HTTP requests across the 5 public profiles sampled here, 5 per profile, 1.5 seconds apart, from one ordinary home internet connection on 2026-07-20, returned HTTP 200 with the og block present on 5 of 5 every time. Response sizes moved between requests (463,175 to 466,950 bytes on one profile), so those were 25 real fetches rather than one cached page served back 25 times. This surface is not walled, and nothing on this page needed a browser, a login or a proxy to reach.

What does bite is more specific, and the next section is what it is.

Avoid getting blocked when scraping Facebook profiles

The one thing that gets refused outright is the thing most scraping guides tell you to do.

Facebook refuses a browser User-Agent on a plain HTTP client

A desktop Chrome User-Agent sent from a plain HTTP client came back 400, with a 1,542-byte page titled Error, on 3 of 3 requests. In the same run, curl/8.4.0, python-requests/2.34.2 and this repo's own self-describing agent each got the real ~463 KB document with all 7 og: tags, on 3 of 3. Reproduce it in about twenty seconds:

python free_scraper/ua_test.py 3

The four User-Agents, measured

That is why the script here sends its own name, and it is worth knowing before you go looking for a proxy problem you do not have. If you "fixed" a scraper by pasting in a browser User-Agent, you broke it.

Here is what genuinely bites you, none of which a proxy fixes:

What bites you Why What it costs you
A browser User-Agent is refused 3 of 3 requests with a Chrome 126 agent returned 400 and 1,542 bytes titled Error. Three self-describing agents got the real document 3 of 3 in the same run. The one piece of standard scraping advice that backfires here.
HTTP 200 is not proof you got a profile A group URL sent to this endpoint answers 200, with username: "groups" and the group's name in name. It is not a 404, a 400 or a redirect. A crawler fed a mixed URL list stores groups as profiles. Check username, not the status code.
The numbers are prose, not fields The likes and talking-about figures live inside about as an English sentence. There is no likes field to read. You are writing a text parser, and a phrasing change breaks it without raising anything.
talking_about scatters between identical calls Six identical calls per profile, seconds apart: NASA moved 3,251 (14.06%), Nike 14,933 (10.32%), National Geographic 12,984 (5.81%). likes moved by 0 to 3 across the same 24 calls. A raw delta on talking_about reports engagement swings that are measurement noise. likes is the one you can trend.
name is not the brand name britishmuseum returns British Museum | London and bbcnews returns BBC News | London, while nike returns Nike and NASA returns its expanded legal name. Joining datasets on name silently drops rows. Join on id.
Every field traces to one og block data_source was opengraph on 15 of 15 calls, so the whole row rests on a handful of <meta> tags. When Meta moves that block your parser returns None rather than an error. You need alerting that can tell "empty" from "broken".

Using the Chocodata Facebook Profile Scraper API

The managed option. The Chocodata Facebook Profile Scraper API turns a handle into parsed JSON instead of ~463 KB of HTML, with a ~99% success rate, a pinned egress location so two runs are comparable, and the User-Agent trap above handled for you. Across 15 calls to 5 public organisation profiles on 2026-07-20 it returned a parsed profile with id, username, name and about on 15 of 15, at a 1.8s median. Free for the first 1,000 requests.

The API earns its money when the parser stops being something you want to own: when you need the response already parsed and shaped, the null-handling below already mapped, and someone else on the hook the next time Meta moves the markup.


Facebook Profile Scraper API reference

Below is the Facebook Profile Scraper API reference to get you started: one endpoint, its parameters, a runnable call, and the response it returns.

Quickstart

curl "https://api.chocodata.com/api/v1/facebook/profile?api_key=YOUR_KEY&url=https://www.facebook.com/NASA"
import requests

r = requests.get(
    "https://api.chocodata.com/api/v1/facebook/profile",
    params={"api_key": "YOUR_KEY", "username": "NASA"},
    timeout=90,
)
r.raise_for_status()          # a 502 means no public profile document came back
p = r.json()
print(p["id"], "|", p["name"])
# 100044561550831 | NASA - National Aeronautics and Space Administration

id, username, url, name, about and both image fields come back on every call. category and is_verified do not, which the field notes cover.

After running the command, your terminal should look something like this:

Running the Facebook Profile Scraper API profile endpoint

Get a key at chocodata.com (1,000 requests, one-time, no card).

Authentication

Pass api_key as a query parameter on every request. That is the whole auth model.

Global parameters

Param Type Required Default Description
api_key string yes - Your Chocodata API key.
country string (ISO-2) no us Egress location. Must be exactly 2 characters: usa returns a 400. Facebook localises what it renders to the location it sees, so pin this for reproducible results. Verified on us, ca and jp: a parsed profile on 3 of 3 calls each.

Each request costs 5 credits (= 1 request). Responses are billed only on success (2xx).

Errors

Status error code Meaning Billed What to do
400 invalid_params A required param is missing or the wrong type. Body lists the exact issue under issues. no Fix the query string.
401 INVALID_API_KEY Key missing, unrecognised, or revoked. no Check api_key. Get one at chocodata.com.
402 INSUFFICIENT_CREDITS Balance exhausted. no Top up or upgrade.
429 RATE_LIMITED Over your plan's concurrency. no Back off and retry; see Rate limits.
502 extraction_failed No public profile document came back for this handle or URL. no Retry once after a few seconds.

Two response shapes exist: auth and billing errors nest under error.code (uppercase), while scrape-layer errors are flat with a lowercase error string. The 400 and 401 bodies are shown below and committed in errors.json.

There is no 404. A handle that does not exist returns 502 extraction_failed, and so does a non-Facebook URL: example.com as a control returned 502 rather than a 200 with name: "Example Domain" under a profile schema. The five public profiles sampled here returned 200 on 15 of 15 calls, so on this endpoint a 502 reads as "no public profile document came back for this input". It does not tell you why, so do not read a deleted profile into it.

A bad key, verbatim:

curl "https://api.chocodata.com/api/v1/facebook/profile?api_key=totally_invalid_key_123&username=NASA"
{"error": {"code": "INVALID_API_KEY", "message": "Api key not recognised."}}

A missing required param, verbatim. Note it names the exact requirement rather than a generic 400:

{
  "error": "invalid_params",
  "issues": [
    {
      "code": "custom",
      "message": "url or username is required",
      "path": []
    }
  ]
}

The scripts in this repo map the statuses in the table above onto actionable messages, so a typo'd key does not hand you a stack trace:

Facebook Profile Scraper API error handling

Rate limits and concurrency

There is no per-minute request cap. The limit is concurrency: how many requests you may have in flight at once.

Plan Concurrent requests
Free 10
Vibe 30
Pro 50
Custom 100 to 500+

Exceed it and you get 429, not a queue. The endpoint is a synchronous GET: there is no webhook, callback, or async job to poll. A request typically takes ~2s (see Measured latency), and the examples use timeout=90 for headroom.

Sizing: at Pro (50 concurrent) and a ~1.8s median call, one worker pool sustains roughly 50 / 1.8 = 28 requests/second. Fan out with a thread pool:

from concurrent.futures import ThreadPoolExecutor
import requests

def one(handle):
    r = requests.get("https://api.chocodata.com/api/v1/facebook/profile",
                     params={"api_key": KEY, "username": handle}, timeout=90)
    return handle, (r.json() if r.ok else None)

handles = ["NASA", "natgeo", "bbcnews", "nike", "britishmuseum"]
with ThreadPoolExecutor(max_workers=10) as pool:   # <= your plan's concurrency
    for handle, p in pool.map(one, handles):
        print(handle, p["id"] if p else "no public document")

1. Profile: profile names, usernames, IDs, about text and likes

A public profile's own about surface, by vanity handle or full URL. Profile-level metadata only: no posts, no friends, no followers list, no comments.

Param Type Required Default Description
username string one of username / url - Vanity handle, e.g. NASA. Case is preserved in the response but not required to match.
url string (URL) one of username / url - Full profile URL, facebook.com/<handle>.
country string (ISO-2) no us Egress location.
curl "https://api.chocodata.com/api/v1/facebook/profile?api_key=YOUR_KEY&username=NASA"

Real response. Complete object, all 11 fields verbatim; the two CDN image URLs are truncated where marked (full sample):

{
  "id": "100044561550831",
  "username": "NASA",
  "url": "https://www.facebook.com/NASA/",
  "name": "NASA - National Aeronautics and Space Administration",
  "category": null,
  "about": "NASA - National Aeronautics and Space Administration. 28,656,732 likes · 20,516 talking about this. Explore the universe and discover our home planet. \nThere's space for everybody. ✨",
  "profile_image": "https://scontent-lax3-2.xx.fbcdn.net/v/t39.30808-1/243095782_416661036495945_3843362260429099279_n.p...",
  "thumbnail": "https://scontent-lax3-2.xx.fbcdn.net/v/t39.30808-1/243095782_416661036495945_3843362260429099279_n.p...",
  "is_verified": null,
  "source": "facebook",
  "data_source": "opengraph"
}

id is the field to build on and about is the field to mine. Neither is the one people ask for first.

Field coverage across 15 calls to five public profiles

Read the field behaviour, it is load-bearing:

  • id is the join key. It is Facebook's numeric profile id (100044561550831 for NASA), it is stable, and it is the only identifier here that is. A vanity handle can be changed by its owner and name is neither unique nor stable, so key your own storage on id and treat the handle as a lookup.
  • The likes and talking-about figures are inside about, as one English sentence: "NASA - National Aeronautics and Space Administration. 28,656,732 likes · 20,516 talking about this." parse_about() in profile.py regexes them out and returns {'likes': 28656732, 'talking_about': 20516, 'were_here': None}. Unlike a lot of Facebook's rendered counts these arrive at full precision, comma-grouped rather than rounded to "28M".
  • were_here only exists on profiles with a physical location. The British Museum returns 1,448,379 were here and BBC News returns 9,569, while NASA and National Geographic do not render it at all, so parse_about() returns None rather than zero.
  • likes is steady and talking_about is not. Six identical calls per profile on 2026-07-20, seconds apart: likes moved by 0 to 3 units across four profiles (0.0000% relative), while talking_about moved 3,251 on NASA (14.06%), 14,933 on Nike (10.32%), 12,984 on National Geographic (5.81%) and 189 on the British Museum (0.56%). Trend likes; treat talking_about as an order of magnitude.
  • category and is_verified were null on 15 of 15 calls. Facebook does not render either to a logged-out request on this surface, so they come back null rather than guessed from the about text.
  • name carries whatever Facebook renders, which is often not the brand name. British Museum | London and BBC News | London both append the city; NASA returns its expanded legal name; nike returns Nike. Do not join on it.
  • profile_image and thumbnail are the same URL. It carries Facebook's own oh signature and oe expiry parameters, so it stops working: fetch the bytes if you need to keep the image.
  • A non-profile Facebook URL still answers 200. Point it at a group and username comes back as the path segment it found (sample):
{
  "id": "220885194946296",
  "username": "groups",
  "url": "https://www.facebook.com/groups/canvadesigncommunity/",
  "name": "Canva Design Community (Official) | Facebook"
}

A username of groups, events, marketplace or watch means you asked for a surface this endpoint does not describe. profile.py checks for exactly that and says so:

A group URL answering the profile endpoint

Runnable: facebook_profile_scraper_api_codes/profile.py


Build a dataset of organisation profiles from a handle list

Turning a list of handles into a table with stable ids is the main reason people scrape Facebook profiles, so that use case is in the repo end to end rather than as a snippet. profile_dataset.py resolves any number of handles, stores every observation as a local dataset in SQLite, and prints what moved since the last run:

python facebook_profile_scraper_api_codes/profile_dataset.py NASA natgeo nike britishmuseum

Facebook profile dataset run

Export it with one command:

sqlite3 -header -csv facebook_profiles.db "select * from observations" > profiles.csv

That screenshot is the second run, seconds after the first, and every line of it is the script declining to report a change. The floors come from the scatter measured above: 0.001% on likes, which is over a hundred times the worst scatter observed there, and 20% on talking_about, which is about 1.4x its worst. Both are printed alongside the raw move, so you can see the noise rather than take the threshold on trust.

That second floor is where this field runs out. A 20% floor on NASA's talking_about means roughly 4,200 people have to move before the number is allowed to count, which is most of the range the field occupies in a quiet week. likes is the field that supports a trend line; talking_about supports a chart with a wide band on it and nothing more.

One API call per handle, per run. Your one-time 1,000 free requests covers roughly a month of daily checks on 30 profiles.


Measured latency

Real end-to-end wall-clock, measured from a laptop against the live API on 2026-07-20. This includes the upstream fetch, the anti-bot handling, and the parse:

Endpoint Median Range 2xx n
/facebook/profile 1.8s 1.2 to 3.1s 15/15 15

The range is tight, which is what you want from a surface whose whole job is one document fetch and a parse.

That is one laptop on one afternoon, and n=15 is too small a sample to say anything about the ~99% platform benchmark either way. Reproduce it with the scripts in this repo.


License

MIT. See LICENSE.