Skip to content

ChocoData-com/facebook-event-scraper

Repository files navigation

Facebook Event Scraper

Facebook Event Scraper

Facebook Event Scraper for extracting event names, descriptions, dates, locations, hosts and attendee counts from Facebook.com. This repo has a free Facebook event web scraping script you can run right now, and a Facebook event data API that turns an event URL 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 events hosted by organisations, and collects no personal data. Every capture, screenshot and example here targets an event run by a company, venue or organisation (New York Comic Con, Live Nation Polska and TAURON Arena Kraków, East Coast Card Shows, Go-Van). Nothing in this repo reads, stores or renders a guest list, an attendee's name or photo, or the event's discussion posts, and the endpoint has no field that would return them. Facebook Pages, posts and groups 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, and every field shown is verbatim. Full uncut samples are committed in facebook_event_scraper_api_data/, so you can diff this page against them. Every code example calls the actual API and is runnable from facebook_event_scraper_api_codes/.

pip install requests
export CHOCODATA_API_KEY="your_key"     # free: 1,000 requests, one-time, no card
python facebook_event_scraper_api_codes/event.py 1740113193315403

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

{
  "id": "1740113193315403",
  "name": "New York Comic Con 2026",
  "description": "Event in New York, NY by New York Comic Con on Thursday, October 8 2026 with 13K people interested and 1.2K people going. 37 posts in the discussion.",
  "start_time": null,
  "location": null,
  "data_source": "opengraph"
}

...that is 6 of the 15 fields on an event; the full 15 are below. Those two nulls are the interesting part of this surface and not an accident: the date and the place are inside description, and the scripts here parse them back out. Point it at a set of events and you get a row each:

Retrieved Facebook event data

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


Contents


Free Facebook Event Scraper

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

python free_scraper/facebook_event_free_scraper.py 1740113193315403

Source: free_scraper/facebook_event_free_scraper.py. It reads og:title, og:description, og:image and og:url, emits id, url, name, description, cover_image, and runs parse_description() over the description to recover the date, the place, the host and both counts. It parses first and only reports "no data" when the OpenGraph block is genuinely absent, so an event that renders is never miscounted as a block.

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

Free Facebook event scraper

That works, and it kept working: 20 plain HTTP requests across the 4 public events sampled here, 5 per event, 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 (556,127 to 562,245 bytes on one event), so those were 20 real fetches rather than one cached page served back 20 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 events

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 ~556 KB document with all 6 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 an event A Facebook Page URL sent to the event endpoint answers 200, with the Page's name in name and id: null. It is not a 404, a 400 or a redirect. A crawler fed a mixed URL list stores Pages as events. Gate on id, not on the status code.
The fields you want are prose start_time, end_time, location, host, attending_count and interested_count were null on 12 of 12 calls across 4 events. The date, place, host and both counts are inside description as an English sentence. You are writing a text parser, not reading a field, and a phrasing change breaks it without raising anything.
Both counts are rounded display text Facebook renders "13K people interested" and "1.2K people going", so those carry two significant figures. "705 people going" on a smaller event is exact. A raw delta on a 13K event "detects" up to 1,000 people who never arrived. Gate on the rounding step.
The place text depends on where you fetch from Same event, same day: from the default us egress, "Event in New York, NY" on 16 of 16 calls. From a home connection in Lithuania, "Event in New York, NY, United States" on 3 of 3, and the same string on one call each from ca and jp egress. Two runs from different locations diff as a change that did not happen. Pin country and keep it pinned.
Every field traces to one og block data_source was opengraph on 12 of 12 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 Event Scraper API

The managed option. The Chocodata Facebook Event Scraper API turns an event URL into parsed JSON instead of ~556 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 12 calls to 4 public events on 2026-07-20 it returned a parsed event with id, name, description and cover_image on 12 of 12, at a 2.1s 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 Event Scraper API reference

Below is the Facebook Event 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/event?api_key=YOUR_KEY&url=https://www.facebook.com/events/1740113193315403/"
import requests

r = requests.get(
    "https://api.chocodata.com/api/v1/facebook/event",
    params={"api_key": "YOUR_KEY", "id": "1740113193315403"},
    timeout=90,
)
r.raise_for_status()          # a 502 means no public event document came back
e = r.json()
print(e["name"], "|", e["description"][:60])
# New York Comic Con 2026 | Event in New York, NY by New York Comic Con on Thu

id, url, name, description and cover_image come back on every call. The seven date, location, host and count fields do not, which the field notes cover and event.py handles with parse_description().

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

Running the Facebook Event Scraper API event 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 writes the location it sees into the event's own description text, so pin this for reproducible results. Verified on us, ca and jp: a parsed event 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 event document came back for this id 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. An event id 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 title: "Example Domain" under an event schema. The four public events sampled here returned 200 on 12 of 12 calls, so on this endpoint a 502 reads as "no public event document came back for this input". It does not tell you why, so do not read a cancelled event into it.

A bad key, verbatim:

curl "https://api.chocodata.com/api/v1/facebook/event?api_key=totally_invalid_key_123&id=1740113193315403"
{"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 id 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 Event 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 ~2.1s median call, one worker pool sustains roughly 50 / 2.1 = 24 requests/second. Fan out with a thread pool:

from concurrent.futures import ThreadPoolExecutor
import requests

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

events = ["1740113193315403", "790870894064610", "1684287302731732", "27173894108875236"]
with ThreadPoolExecutor(max_workers=10) as pool:   # <= your plan's concurrency
    for event_id, e in pool.map(one, events):
        print(event_id, e["name"] if e else "no public document")

1. Event: event names, dates, locations, hosts and attendance counts

A public event page, by numeric id or full URL. Event-level metadata only: no guest list, no attendee names, no discussion posts.

Param Type Required Default Description
id string one of id / url - Numeric event id, e.g. 1740113193315403.
url string (URL) one of id / url - Full event URL, facebook.com/events/<id>/. Any canonical form works; the numeric id is parsed out of it.
country string (ISO-2) no us Egress location. Changes the place text Facebook writes into description.
curl "https://api.chocodata.com/api/v1/facebook/event?api_key=YOUR_KEY&id=1740113193315403"

Real response. Complete object, all 15 fields verbatim, nothing cut (full sample):

{
  "id": "1740113193315403",
  "url": "https://www.facebook.com/events/655-w-34th-st-new-york-ny-united-states-new-york-10001/new-york-comic-con-2026/1740113193315403/",
  "name": "New York Comic Con 2026",
  "description": "Event in New York, NY by New York Comic Con on Thursday, October 8 2026 with 13K people interested and 1.2K people going. 37 posts in the discussion.",
  "start_time": null,
  "end_time": null,
  "location": null,
  "host": null,
  "attending_count": null,
  "interested_count": null,
  "cover_image": "https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=1740113193315403",
  "thumbnail": "https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=1740113193315403",
  "is_online": null,
  "source": "facebook",
  "data_source": "opengraph"
}

description is the field most people come for, even though nobody arrives wanting it. It is carrying five other fields.

Field coverage across 12 calls to four public events

Read the field behaviour, it is load-bearing:

  • start_time, end_time, location, host, attending_count, interested_count and is_online were null on 12 of 12 calls, across all four events. That is the shape of this surface rather than a gap in these four samples, and it is why the repo ships a parser instead of a field mapping.
  • The date, the place, the host and both counts are inside description, as one English sentence: "Event in New York, NY by New York Comic Con on Thursday, October 8 2026 with 13K people interested and 1.2K people going." parse_description() in event.py regexes them back out and returns {'place_text': 'New York, NY', 'host_text': 'New York Comic Con', 'date_text': 'Thursday, October 8 2026', 'interested': 13000, 'going': 1200}. It is a regex over prose: it returns {} rather than a guess when the phrasing does not match, and it will not match a non-English rendering.
  • The prefix is not always "Event in". El Campo Vanfest comes back as "Party event in Quebec, QC by Go-Van...", which is why the pattern anchors on vent in and on a weekday name rather than on the start of the string.
  • interested and going are rounded once they pass a thousand. "13K" moves in steps of 1,000 and "1.2K" in steps of 100, so those are the smallest changes Facebook can express on that event. Anime City returned 705 people going, exact, because it is under the rounding threshold. Trend the small ones at full precision and the large ones only above the step.
  • host being null does not mean the event has no host. Every event sampled here names its host inside description, and all four are organisations: New York Comic Con, Live Nation Polska and TAURON Arena Kraków, East Coast Card Shows, Go-Van. An event with two hosts renders both, joined with "and", so host_text can be a compound string.
  • url is Facebook's canonical form, not your input. Ask for id 1740113193315403 and the response carries .../events/655-w-34th-st-new-york-ny-united-states-new-york-10001/new-york-comic-con-2026/1740113193315403/, with the venue and the event slug in it. Key your own storage on the numeric id.
  • cover_image and thumbnail are the same URL, a lookaside.fbsbx.com crawler endpoint keyed on the event id rather than a signed CDN link. Fetch the bytes if you need to keep the image.
  • A non-event Facebook URL still answers 200. Point it at a Page and you get the Page's name in name and id: null (sample). id is the discriminator:

A Page URL answering the event endpoint

Runnable: facebook_event_scraper_api_codes/event.py


Track interest and attendance on public events

Watching an event's interest build in the weeks before it runs is the main reason people scrape Facebook events, so that use case is in the repo end to end rather than as a snippet. event_watch.py polls any number of public events, stores every observation as a local dataset in SQLite, and prints what moved since the last run:

python facebook_event_scraper_api_codes/event_watch.py \
    1740113193315403 790870894064610 1684287302731732

Facebook event interest and attendance monitoring

Export it with one command:

sqlite3 -header -csv facebook_events.db "select * from observations" > events.csv

The interesting part of that script is not the polling, it is rounding_step(), and it is why its numbers are worth trusting. Because both counts are display text, the precision you get depends on the event's size: "13K" ticks to "14K", so the smallest move Facebook can show you on that event is 1,000 people, while "705" moves one person at a time. The script derives the step from the source text it captured, per event and per field, and only reports a change that clears it. That is why a 705-going event is trended at full precision in the same run as a 13K-interested one.

The consequence is worth stating plainly: on a large event this is a weekly-trend instrument, and on a small one it is a daily one. Two calls a day against a 13K event will tell you almost nothing, and anyone selling you hour-by-hour Facebook event "momentum" off that number is selling you rounding.

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


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/event 2.1s 1.1 to 2.7s 12/12 12

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=12 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.