Facebook Videos Scraper
Pricing
$14.99/month + usage
Facebook Videos Scraper
Facebook Videos Scraper helps you collect video data from Facebook pages, groups, or profiles. Extract titles, descriptions, view counts, reactions, comments, and video URLs. Great for content analysis, marketing insights, or competitor tracking. Fast, accurate, and easy-to-use scraping solution.
Pricing
$14.99/month + usage
Rating
5.0
(6)
Developer
Scraper Engine
Maintained by CommunityActor stats
1
Bookmarked
126
Total users
5
Monthly active users
20 days ago
Last modified
Categories
Share
Facebook Videos Scraper — Reel Captions, Stream URLs and Metadata
Facebook Videos Scraper turns a list of public Facebook Reel links into structured JSON rows: caption text, publish timestamp, duration, owner id, thumbnail, and the complete set of playable stream URLs — every DASH video and audio representation plus the direct progressive HD and SD MP4 links. Paste one URL per line and typed rows land in the dataset as each reel is fetched. No browser, no ffmpeg, no yt-dlp, no Facebook account.
⚠️ What this Actor does not return. There is no view count, play count, reaction total, comment total or share total anywhere in the output, and no creator display name — uploader is null on every reel row. Facebook does prefix the page's og:title with an approximate blurb like 5.2K views · 72 reactions | …, and this Actor strips that prefix out of title rather than emitting it as a numeric field, because it is a rounded display string and not a count. What you get is the reel's own metadata and its media.
⚠️ Only /reel/<id> URLs return video data
This is the single thing to know before your first run, and it is Facebook's constraint rather than the Actor's.
Logged out, the only Facebook URL shape whose HTML carries data about the video you asked for is the reel permalink, https://www.facebook.com/reel/<id>. For a page-video link (/username/videos/<id>/), a /watch/?v=<id> link, or a reel id that no longer exists, Facebook answers HTTP 200 with a roughly 450–500 KB recommendation feed — a page full of other creators' videos and nothing at all about the id you requested. A status-code check cannot tell those two documents apart.
Rather than reject those URLs, the Actor accepts them and classifies the answer honestly:
| Input URL shape | Result |
|---|---|
https://www.facebook.com/reel/<id> | status: "Success" with the full row |
https://www.facebook.com/<page>/videos/<id>/ | status: "Unavailable", unavailableReason: "feed_fallback_no_video_data" — not charged |
https://www.facebook.com/watch/?v=<id> | status: "Unavailable", unavailableReason: "feed_fallback_no_video_data" — not charged |
| A reel id that is deleted, private or otherwise gone | status: "Unavailable", unavailableReason: "video_unavailable_or_not_a_reel" — not charged |
A mixed input list therefore still runs cleanly end to end. You get one honest row per URL, and you are billed only for the ones that carried a real playable stream.
What is Facebook Videos Scraper Pay Per Events?
Facebook Videos Scraper Pay Per Events is an Apify Actor that fetches public Facebook reel pages over plain HTTP and extracts the video document embedded in the returned HTML — metadata, thumbnail, and the DASH manifest and progressive MP4 URLs Facebook ships to the page. One input URL produces exactly one dataset row.
It runs logged out end to end. No Facebook account, cookie, token or session is used, requested or accepted — there is no field to put one in. The reason the logged-out HTML carries data at all is the request's user agent: the Actor identifies as facebookexternalhit/1.1, Facebook's own link-preview crawler.
It is built for developers assembling Facebook video datasets, archival and media pipelines that need a real stream URL, and AI engineers indexing public reel captions.
What Facebook Reel data is publicly available to scrape?
A public Facebook reel page served to a logged-out crawler carries the reel's caption, its title string, the numeric video and owner ids, the publish timestamp, a preferred thumbnail, and the full media manifest — DASH representations plus browser-native SD and HD MP4 URLs. What it does not carry is the creator's display name or any numeric engagement figure.
| Data Category | Public on the logged-out reel page | Absent from this surface, or behind login |
|---|---|---|
| Reel caption text, title string | ✅ Public | — |
| Numeric video id and owner id | ✅ Public | — |
| Publish timestamp and duration | ✅ Public | — |
| Preferred thumbnail image URL | ✅ Public | — |
| DASH manifest representations, codecs, bitrates, resolution | ✅ Public | — |
| Direct progressive SD and HD MP4 URLs | ✅ Public | — |
| Creator display name | ❌ | The owning_profile block is not present on the reel surface |
| View, play, reaction, comment and share counts | ❌ | Only a rounded display blurb on og:title, which is stripped |
| Subtitle and closed-caption tracks | ❌ | Empty on the reels sampled, so no field is emitted |
| Comments, reactor identities, page or profile discovery | ❌ | Out of scope — one URL in, one row out |
| Page-video and Watch pages, logged out | ❌ | Facebook serves a recommendation feed instead |
| Private, friends-only and group-restricted reels | ❌ | Login |
Facebook Videos Scraper only returns publicly visible data — what any visitor or link-preview crawler sees. Nothing behind a login wall.
What data can I extract with Facebook Videos Scraper?
A Success row carries the reel's identity and links, its timing fields, its thumbnail, the full stream list, and a flat summary of the best video-plus-audio pair. Forty-four keys on a full success row; forty-five distinct keys across all three row types.
| Field Name | Description |
|---|---|
status | Success, Unavailable or Failed. Present on every row. |
unavailableReason | null on Success. Otherwise video_unavailable_or_not_a_reel, feed_fallback_no_video_data, no_playable_formats or fetch_failed. |
charged | true only on Success. false on Unavailable and Failed. |
error | Exception type and message. Written on Failed rows only — the key is absent from every other row. |
id | Numeric Facebook video id, read from the URL you supplied, falling back to video_id in the page data |
display_id | Same value as id |
webpage_url_basename | Same value as id |
title | The page's og:title with Facebook's views/reactions blurb stripped off the front. An empty string when the page carries no og:title |
fulltitle | Duplicate of title, kept for compatibility with info_dict consumers |
description | The reel's caption text, bound to the requested video id. null when the page carries no message block for that id |
uploader | Creator display name. null on every reel row — the block that carries it is absent from this surface |
uploader_id | Numeric owner id of the account that posted the reel |
webpage_url | Canonical link built as https://www.facebook.com/<uploader_id>/videos/<id>/. null if either part is missing |
original_url | The URL you supplied, verbatim |
webpage_url_domain | Constant facebook.com |
extractor | Constant facebook |
extractor_key | Constant Facebook |
duration | Runtime in seconds, float, from playable_duration_in_ms |
duration_string | Same runtime formatted M:SS, or H:MM:SS past an hour |
timestamp | Unix publish time, from creation_time or publish_time |
upload_date | YYYYMMDD in UTC, derived from timestamp |
epoch | Unix time the row itself was built |
thumbnail | Preferred thumbnail image URL, falling back to og:image |
thumbnails | Array of one object — url and id (always "0"). Empty array when no thumbnail was found |
formats | Every playable stream found on the page. Shape documented below |
requested_formats | Two-element array: the highest-bitrate DASH video plus the DASH audio track |
format | Human-readable label of the selected pair, e.g. …v - 720x1280 (DASH video)+…a - audio only (DASH audio) |
format_id | The two selected format ids joined with + |
ext | Container of the selected pair — mp4 |
protocol | https+https |
format_note | DASH video+DASH audio |
filesize_approx | Estimated total bytes: selected video plus selected audio |
tbr | Total bitrate in kbps: video plus audio |
width | Pixel width of the selected video representation |
height | Pixel height of the selected video representation |
resolution | <width>x<height> of the selected video representation |
fps | Frame rate declared on the DASH video adaptation set |
dynamic_range | SDR |
vcodec | Video codec string of the selected representation, e.g. avc1.64001f |
vbr | Video bitrate in kbps |
aspect_ratio | Width divided by height, rounded to two places |
acodec | Audio codec string, e.g. mp4a.40.5 |
abr | Audio bitrate in kbps |
asr | Audio sampling rate in Hz |
audio_channels | Channel count from the DASH audio channel configuration |
Identity, caption and link fields
id, display_id and webpage_url_basename are the same numeric video id three times over — the duplication exists so info_dict-shaped tooling finds the key it expects. The id is taken from the URL you supplied first, and only falls back to the page's own video_id if the URL carries none.
description is the field worth understanding, because a reel page does not carry one caption — it carries roughly six, one for the requested reel and the rest for the sibling reels in the up-next tray, and Facebook randomises their order server-side on every fetch. A naive first-match parser returns a neighbouring creator's caption. Here each caption block is bound to its nearest preceding story token, and only the block belonging to your requested video id is returned. uploader_id is resolved the same way: the base64 story token that decodes to your video id carries the owner id directly, and the fallback owner read is used only when the whole document agrees on a single owner. Otherwise uploader_id is null rather than a guess.
One practical warning about webpage_url: it is constructed as a page-video link from uploader_id and id, not copied from the page. It is a fine canonical reference, but feeding it back into urls returns an Unavailable row, because page-video URLs are exactly the shape Facebook will not serve logged out. Round-trip original_url instead.
Stream fields — formats[]
formats is where the media lives, and it holds three different entry shapes. Entries are ordered DASH audio first, then the progressive sd and hd entries, then the DASH video representations sorted ascending by bandwidth.
| Entry shape | format_id | Keys | Notes |
|---|---|---|---|
| DASH audio | The representation id, suffixed a | 29 | container: "m4a_dash", ext: "m4a", vcodec: "none", resolution: "audio only", carries asr and audio_channels |
| DASH video | The representation id, suffixed v | 28 | container: "mp4_dash", acodec: "none", carries width, height, resolution, aspect_ratio, fps |
| Progressive | sd or hd | 17 | A single complete MP4 file. Carries quality (-3 for sd, -2 for hd) and no manifest_url, width, height or container |
Every entry carries url — a direct fbcdn media URL — plus http_headers and downloader_options, so a downloader can replay the exact request the Actor made, with the same facebookexternalhit/1.1 user agent. Two naming details to code around: DASH entries report their estimated size as filesize while progressive entries use filesize_approx, and progressive entries leave resolution as null with a format label of sd - unknown or hd - unknown, because Facebook does not declare the geometry of those URLs. All size figures are estimates derived from bitrate multiplied by duration — none is a Content-Length.
🤖 Add-on: Need additional Facebook data?
Companion Scraper-Engine Actors cover the parts this one deliberately leaves out. Facebook Video Search Scraper finds videos by keyword when you do not already have permalinks. Facebook Pages Recent Posts Scraper and Facebook Posts Scraper cover the text-post side of the same accounts, and Facebook Hashtag Search Scraper surfaces reels and posts behind a hashtag.
Why not build this yourself?
Meta's Graph API exposes no general third-party access to arbitrary public videos — there is no public endpoint that takes someone else's reel id and returns its metadata and media. Check Meta's current published Graph API documentation for which video and page endpoints exist and under what permissions and app-review process, because those terms change and are not reproduced here. That gap is why a scraper exists, and it is also why building one means owning four problems that are not obvious until you hit them.
The user agent decides whether you get data at all. Measured in an Apify container on 20 July 2026 and recorded in this Actor's source: a desktop Chrome user agent returns HTTP 400 with a body of roughly 1.5 KB and zero media keys — a 100% failure rate — while facebookexternalhit/1.1 returns HTTP 200 with roughly 847 KB of HTML carrying the media. Sending browser-style client hints alongside a crawler user agent is contradictory, so the header set here is deliberately minimal and consistent with the agent actually sent.
HTTP 200 does not mean success. Facebook returns a 450–500 KB recommendation shell for dead ids and for non-reel URL shapes. It is a plausible-looking page full of other creators' videos. The only reliable separator found across probing is the presence of a playable media key — dash_manifest_xml_string, browser_native_sd_url or browser_native_hd_url. Without that check you write rows that look successful and are almost entirely null, and on a pay-per-event Actor you bill for them.
Every field has to be bound to the video you asked for. A reel page embeds several sibling reels in server-randomised order. First-match parsing on "owner" or "message" returns a different creator's id and caption from one run to the next. Binding requires decoding the base64 story tokens (S:_I<ownerId>:VK:<videoId>), ordering them by document offset, and attaching each caption to its nearest preceding anchor.
Then there is the media layer and the egress. The DASH manifest arrives as MPD XML that has to be parsed into per-representation entries with codecs, bandwidths, sampling rates and channel configuration, with sizes estimated from bitrate and duration. On top of that sits proxy egress, block detection and retry — infrastructure to build, run and pay for before a single row is parsed.
How to use Facebook Videos Scraper
Facebook Videos Scraper runs on Apify. Start it from the Apify Console, or call it through the Apify API.
- Open Facebook Videos Scraper Pay Per Events on Apify and click Try for free
- Paste your reel links into Facebook Reel URLs (
urls), one per line — this is the only required input - Optionally set Proxy Configuration (
proxyConfiguration). Leave it off for a direct connection - Click Start. Each URL is fetched in turn and its row appears in the dataset immediately
- Download the dataset as JSON, CSV, Excel or XML, or read it through the Apify API
Use reel permalinks. A page-video or Watch URL will run, but it returns an uncharged Unavailable row rather than data.
How to scale to bulk reel extraction
urls is a list with no length cap in the schema and none in the Actor, so one run can cover as many reels as you want to paste. URLs are processed strictly in order, one at a time — there is no concurrency setting and no parallel fetching, so runtime scales roughly linearly with list length.
Two behaviours to plan around. There is no de-duplication: the same URL listed twice is fetched twice and produces two charged rows, so de-duplicate your list before the run. And the run is not transactional — rows are pushed live as each URL finishes, so a run you abort early keeps everything it had already written.
What can you do with Facebook Reel data?
- 🎬 A media archivist building a reel archive reads
formats[]for thehdentry'surland pairs it withhttp_headers, then storesid,timestampanddescriptionas the catalogue record for each file. - 📊 A content analyst tracking a creator's output collects reels weekly and groups on
uploader_id, usingduration,upload_dateandresolutionto see how format and cadence shift over time. - 🔧 A pipeline engineer feeding a transcoder filters
formats[]onformat_note: "DASH video", picks the representation whoseheightmatches the target profile, and usesvcodecandtbrto decide whether a re-encode is needed at all. - 🏷️ A social researcher studying hashtag use parses
descriptionfor tags across a reel set and joins ontimestampto date each occurrence, since the caption is the only free-text field the surface carries. - 🤖 An AI engineer building a video-understanding pipeline indexes
descriptionandtitleinto a vector store keyed onidandoriginal_url, then hands the agent the matchingformats[]URL when a query needs the media itself rather than the metadata.
Every one of these is callable from an agent framework over the Apify API, since the Actor is a standard HTTP-triggered run.
How does Facebook Videos Scraper handle rate limits and blocking?
No CAPTCHA is solved and no browser is launched. This Actor makes one plain HTTP GET per URL and reads Facebook's own HTML. What it does handle is transport failure and IP-level refusal.
Each URL gets up to three attempts, with a 30-second total timeout per request and a progressive back-off between attempts — 1.5 seconds after the first failure, 3 seconds after the second. If all three fail, the URL is written as a Failed row with the exception on error, the run continues with your remaining URLs, and you are not charged for it.
Proxy handling is deliberate rather than aggressive. If you enable Apify Proxy, one exit is resolved at the start of the run and reused for every URL — there is no per-request rotation. If you supply custom proxyUrls, only the first entry is used, and only if it parses as an http, https or socks5 URL with a hostname. Environment proxy variables are ignored outright.
There is one automatic escalation, and it is narrow. When the run is on a direct connection with no proxy and a request fails with an error message containing one of 403, 429, access denied, checkpoint, temporarily blocked, login required or not available in your region, the Actor switches to an Apify Residential exit for the remaining attempts. If you already configured a proxy, that fallback does not fire — your configuration is honoured as given.
⬇️ Input
Two parameters. One is required, the other is optional and behaves more narrowly than the field name suggests.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
urls | Yes | array | Facebook Reel links, one per line — https://www.facebook.com/reel/<id>. Page-video and Watch links are accepted but return an uncharged Unavailable row. Editor is a string list; no minimum, maximum or item-count limit is set. Prefilled with one reel URL. | ["https://www.facebook.com/reel/2429400350831482"] |
proxyConfiguration | No | object | Apify Proxy settings, or your own proxy URLs. Prefilled with {"useApifyProxy": false}, which means a direct connection. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
On urls: this is the required parameter, and a run that omits it or supplies an empty list does not fail — it logs No URLs provided., sets that as the run status message, and exits with zero rows and zero charges. Entries are trimmed of surrounding whitespace, and blank entries are dropped silently.
There is one input shape the schema does not advertise: as well as plain strings, urls accepts { "url": "https://www.facebook.com/reel/<id>" } objects, so output from another tool that emits request-list-style records can usually be fed in unchanged. Anything that is neither a non-empty string nor an object with a usable url key is discarded without a warning. Entries must be full URLs — a bare numeric video id is parsed by the id resolver but cannot be fetched, and will end up as a Failed row.
On proxyConfiguration: it does less than the editor implies. useApifyProxy: true resolves exactly one exit for the whole run. Custom proxyUrls uses only the first entry in the list; the rest are ignored. Neither path rotates between URLs. Leaving it at the prefilled {"useApifyProxy": false} runs everything on the container's direct connection, which is the one case where the automatic residential fallback described above can trigger.
Example input
{"urls": ["https://www.facebook.com/reel/2429400350831482","https://www.facebook.com/reel/1775334020324572","https://www.facebook.com/reel/8143902276618054"],"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}}
⬆️ Output
One dataset row per input URL, pushed live as soon as that URL finishes, in the order you supplied them. Typed JSON with stable key names across runs. Export as JSON, CSV, Excel or XML, or read the dataset through the Apify API. A SUMMARY.json record is also written to the run's key-value store, carrying total_requested, total_processed, success_count, unavailable_count, failed_count and a per-URL stats map.
Rows are not rectangular, and that is worth knowing before you write a consumer. status, unavailableReason and charged are on every row. Everything else depends on the outcome: Unavailable rows carry only id and original_url beyond those three, Failed rows carry original_url and error and no id at all, and on a Success row the twenty-key selected-format block — requested_formats, format, format_id, ext, protocol, format_note, filesize_approx, tbr, width, height, resolution, fps, dynamic_range, vcodec, vbr, aspect_ratio, acodec, abr, asr, audio_channels — is written only when the page carried a DASH manifest. Those keys are omitted, not nulled. Use .get() rather than indexing.
Each Success row is billed as one row_result event. Unavailable and Failed rows are pushed deliberately uncharged — a page that carried no data about your video must not bill. Filter on status:
videos = [row for row in items if row["status"] == "Success"]
row["charged"] is True selects the same set and is equally safe, since both keys are written on every push path.
One naming inconsistency to expect: the media and metadata keys follow the yt-dlp info_dict convention and are snake_case (uploader_id, upload_date, webpage_url, filesize_approx), while the three run-control keys added by this Actor are camelCase (status, unavailableReason, charged). That is intentional — the snake_case block is a compatibility surface, and renaming it would break downloader tooling that consumes these rows directly.
Example output
A Success row. formats is trimmed to four entries here; a real row carries every representation Facebook publishes for that reel.
{"id": "2429400350831482","status": "Success","unavailableReason": null,"charged": true,"title": "Leo Messi Vs Real Madrid Defenders #football #footballreels","description": "Leo Messi Vs Real Madrid Defenders #football #footballreels #goviral #footballfans","uploader": null,"uploader_id": "61559962654628","duration": 92.066,"duration_string": "1:32","timestamp": 1772964155,"upload_date": "20260308","thumbnail": "https://scontent-dub4-1.xx.fbcdn.net/v/t15.5256-10/518402771_1187433096220841_5580127714003912664_n.jpg?stp=dst-jpg_s960x960&_nc_cat=104&ccb=1-7&_nc_sid=be8305&_nc_zt=23","thumbnails": [{"url": "https://scontent-dub4-1.xx.fbcdn.net/v/t15.5256-10/518402771_1187433096220841_5580127714003912664_n.jpg?stp=dst-jpg_s960x960&_nc_cat=104&ccb=1-7&_nc_sid=be8305&_nc_zt=23","id": "0"}],"webpage_url": "https://www.facebook.com/61559962654628/videos/2429400350831482/","original_url": "https://www.facebook.com/reel/2429400350831482","webpage_url_basename": "2429400350831482","webpage_url_domain": "facebook.com","display_id": "2429400350831482","fulltitle": "Leo Messi Vs Real Madrid Defenders #football #footballreels","extractor": "facebook","extractor_key": "Facebook","epoch": 1784970840,"format": "1487302119645331v - 720x1280 (DASH video)+902471336154829a - audio only (DASH audio)","format_id": "1487302119645331v+902471336154829a","format_note": "DASH video+DASH audio","ext": "mp4","protocol": "https+https","resolution": "720x1280","width": 720,"height": 1280,"aspect_ratio": 0.56,"fps": 30,"dynamic_range": "SDR","vcodec": "avc1.64001f","vbr": 1936.842,"acodec": "mp4a.40.5","abr": 66.058,"asr": 48000,"audio_channels": 2,"tbr": 2002.9,"filesize_approx": 23049874,"formats": [{"format_id": "902471336154829a","manifest_url": "https://www.facebook.com/dash_mpd_debug.mpd?v=2429400350831482&dummy=.mpd","ext": "m4a","width": null,"height": null,"tbr": 66.058,"asr": 48000,"fps": null,"language": null,"format_note": "DASH audio","filesize": 760212,"container": "m4a_dash","vcodec": "none","acodec": "mp4a.40.5","dynamic_range": null,"url": "https://video-dub4-1.xx.fbcdn.net/o1/v/t2/f2/m412/AQMzR4kPqLd8vXnT2fWbYgHc0uJeS5aKpN9mDx3QrTvB.mp4?_nc_cat=110&_nc_sid=9ca052&_nc_zt=28&oe=68F1A24C","manifest_stream_number": 0,"is_dash_periods": true,"http_headers": {"User-Agent": "facebookexternalhit/1.1","Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language": "en-us,en;q=0.5","Sec-Fetch-Mode": "navigate"},"downloader_options": {"http_chunk_size": 262144000},"protocol": "https","audio_ext": "m4a","video_ext": "none","audio_channels": 2,"vbr": 0,"abr": 66.058,"resolution": "audio only","aspect_ratio": null,"format": "902471336154829a - audio only (DASH audio)"},{"format_id": "sd","quality": -3,"url": "https://video-dub4-1.xx.fbcdn.net/o1/v/t2/f2/m412/AQNv7HsKcRw2TpZfXqLmY8bJd3UgE6nWtQxV5rDaSoBc.mp4?_nc_cat=106&_nc_sid=8bf8fe&_nc_zt=28&oe=68F1B031&bitrate=412973&tag=sve_sd","http_headers": {"User-Agent": "facebookexternalhit/1.1","Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language": "en-us,en;q=0.5","Sec-Fetch-Mode": "navigate"},"downloader_options": {"http_chunk_size": 262144000},"protocol": "https","ext": "mp4","video_ext": "mp4","audio_ext": "none","vbr": null,"abr": null,"tbr": 412.973,"resolution": null,"dynamic_range": "SDR","aspect_ratio": null,"filesize_approx": 4752596,"format": "sd - unknown"},{"format_id": "hd","quality": -2,"url": "https://video-dub4-2.xx.fbcdn.net/o1/v/t2/f2/m412/AQPk9WdBnTx4LcRvZmYqJ7HsE2gUaX6VtNpQ3rFoDwSb.mp4?_nc_cat=101&_nc_sid=5e9851&_nc_zt=28&oe=68F1C7A9&bitrate=1621504&tag=dash_h264-basic-gen2_1080p","http_headers": {"User-Agent": "facebookexternalhit/1.1","Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language": "en-us,en;q=0.5","Sec-Fetch-Mode": "navigate"},"downloader_options": {"http_chunk_size": 262144000},"protocol": "https","ext": "mp4","video_ext": "mp4","audio_ext": "none","vbr": null,"abr": null,"tbr": 1621.504,"resolution": null,"dynamic_range": "SDR","aspect_ratio": null,"filesize_approx": 18660673,"format": "hd - unknown"},{"format_id": "1487302119645331v","manifest_url": "https://www.facebook.com/dash_mpd_debug.mpd?v=2429400350831482&dummy=.mpd","ext": "mp4","width": 720,"height": 1280,"tbr": 1936.842,"asr": null,"fps": 30,"language": null,"format_note": "DASH video","filesize": 22289662,"container": "mp4_dash","vcodec": "avc1.64001f","acodec": "none","dynamic_range": "SDR","url": "https://video-dub4-2.xx.fbcdn.net/o1/v/t2/f2/m412/AQOtY5xVnKq8RcJmWzLbP2fHd7UgTa3EoNsQ9rXvBiDw.mp4?_nc_cat=108&_nc_sid=9ca052&_nc_zt=28&oe=68F1D562","manifest_stream_number": 0,"is_dash_periods": true,"http_headers": {"User-Agent": "facebookexternalhit/1.1","Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language": "en-us,en;q=0.5","Sec-Fetch-Mode": "navigate"},"downloader_options": {"http_chunk_size": 262144000},"protocol": "https","video_ext": "mp4","audio_ext": "none","abr": 0,"vbr": 1936.842,"resolution": "720x1280","aspect_ratio": 0.56,"format": "1487302119645331v - 720x1280 (DASH video)"}],"requested_formats": ["… the 1487302119645331v and 902471336154829a entries, repeated …"]}
An uncharged Unavailable row, produced here by a page-video URL that Facebook answered with a recommendation feed:
{"id": "1395487772147320","original_url": "https://www.facebook.com/mrbeast/videos/1395487772147320","status": "Unavailable","unavailableReason": "feed_fallback_no_video_data","charged": false}
An uncharged Failed row, produced when all three fetch attempts errored:
{"original_url": "https://www.facebook.com/reel/9999999999999999","status": "Failed","unavailableReason": "fetch_failed","charged": false,"error": "RuntimeError: Failed to fetch https://www.facebook.com/reel/9999999999999999 after 3 attempts: ClientResponseError: 403, message='Forbidden'"}
The media URLs in formats[] are signed, time-limited fbcdn links. The Actor returns them exactly as Facebook served them and does not re-sign, refresh or track their validity, so fetch them promptly after the run rather than storing them as long-lived references.
How does it work?
Facebook Videos Scraper sends a single HTTP GET to each reel URL you supply, identifying as facebookexternalhit/1.1 — the crawler user agent Facebook serves a data-bearing HTML document to. Requests go out through your proxy configuration if you set one, or the container's direct connection if you did not, with three attempts and a progressive back-off per URL.
The returned HTML is then read directly rather than rendered. The Actor pulls the video and owner ids, the caption, the publish time and the thumbnail out of the embedded JSON, decodes the base64 story tokens so every field is bound to the id you actually asked for, and parses the DASH manifest XML into per-representation stream entries alongside the progressive SD and HD URLs. Before anything is written, the document is classified: no playable media key means no data about your video, and that row is marked Unavailable and left uncharged.
Only publicly visible data is collected. No Facebook account, cookie, token or session is used at any point. Because the row shape is defined by this Actor rather than by Facebook's markup, a front-end redesign does not change your field names — a moved key nulls one field instead of breaking the row.
Integrations
Facebook Videos Scraper is an Apify Actor, so it works with anything that can call the Apify API or consume a dataset — schedules, webhooks, no-code platforms and agent frameworks included.
Calling Facebook Videos Scraper from Python
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/facebook-videos-scraper-pay-per-events").call(run_input={"urls": ["https://www.facebook.com/reel/2429400350831482","https://www.facebook.com/reel/1775334020324572",],"proxyConfiguration": {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]},})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if row["status"] != "Success":print("skipped", row["original_url"], row["unavailableReason"])continuehd = next((f for f in row["formats"] if f["format_id"] == "hd"), None)print(row["id"], row["duration_string"], row.get("resolution"), hd and hd["url"])
Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Branch on status and you are left with charged results only.
No-code tools (n8n, Make, Zapier)
In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; a Filter node on status drops the uncharged rows before anything downstream sees them. In Make, the Apify module supports run-and-wait, so a batch of reel links can feed a Google Sheets or Airtable step directly. Apify's own schedules and webhooks cover the recurring case with no external tool at all: schedule a link list, and have a webhook push each finished dataset into your storage.
Is it legal to scrape Facebook Reels?
Scraping publicly accessible video pages is broadly treated as permissible where no authentication is bypassed, and Facebook Videos Scraper reads only what Facebook serves to a logged-out crawler — no account, no cookie, no session.
This output does identify people, so treat it accordingly. uploader_id is the numeric Facebook account id of whoever posted the reel, and on a reel that is frequently an individual rather than a brand page; webpage_url embeds that same id, uploader is declared as the creator's display name even though this surface leaves it null, and description and title routinely contain names, handles and other personal detail written into the caption. That makes these rows personal data under GDPR and comparable regimes: if you store them you need a lawful basis — a documented and assessed legitimate interest, or consent — plus a retention limit and a route to honour access and deletion requests. CCPA/CPRA treats the same fields as personal information about California residents.
Separately, the URLs in formats[] point at copyrighted video. A stream URL is a link to the file, not a licence to redistribute it, and the reel's creator retains their rights in the content regardless of how the URL was obtained.
Consult legal counsel if your use case involves bulk storage of personal data, profiling individual creators, or republishing downloaded media. Facebook's own terms of service apply independently of both questions.
❓ Frequently asked questions
What Facebook Reel fields does Facebook Videos Scraper return?
The five most used are description, formats, uploader_id, duration and timestamp. A full Success row carries 44 keys, and 45 distinct keys exist across the three row types — see the data fields table above for all of them, plus the three formats[] entry shapes.
Does Facebook Videos Scraper require a Facebook account, login or cookie?
No. The Actor runs logged out end to end. There is no cookie, token or credential field in the input schema, and none is used at runtime — the request identifies as facebookexternalhit/1.1 and reads the public HTML Facebook returns to that agent. The only credential you need is your Apify token.
Does it return a direct video download URL?
Yes. Every entry in formats[] carries a url pointing at a real fbcdn media file: the sd and hd entries are complete progressive MP4s, and the DASH entries are the individual video and audio streams referenced by the manifest. Each entry also ships the http_headers needed to fetch it. This is the main practical difference from the search-driven Facebook Video Search Scraper, which returns a watch-page permalink and a thumbnail but no media URL, because the search payload it reads does not contain one. Note that the Actor returns the links and stops there — it does not download, mux or transcode anything, and the links are signed and time-limited.
What happens if a Reel is deleted, private, region-restricted, or I paste a non-reel URL?
You get one row, status: "Unavailable", and you are not charged. All four cases share a mechanism: Facebook answers HTTP 200 with a recommendation feed containing no data about your video, and the Actor detects that by the absence of any playable media key rather than by the status code. unavailableReason tells you which case it was — video_unavailable_or_not_a_reel when the URL was a reel link but the page carried nothing, feed_fallback_no_video_data when the URL was a page-video or Watch link, and no_playable_formats when media keys were present but no stream could be parsed. A transport failure is different again: three failed attempts produce status: "Failed" with unavailableReason: "fetch_failed" and the exception on error, also uncharged. In every case the run continues with your remaining URLs.
How many Reels can I extract in one run, and what am I charged for?
There is no cap in the schema and none in the Actor — paste as many reel URLs as you like into urls and each is fetched in turn. Processing is strictly sequential, one URL at a time, so runtime scales roughly linearly with list length. Charging is per row and per outcome: each Success row bills one row_result event, while Unavailable and Failed rows are pushed uncharged. There is no de-duplication, so the same URL listed twice is fetched twice and charged twice — de-duplicate before you run.
Does Facebook Videos Scraper work with Claude, ChatGPT and other AI agent tools?
Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON with no parsing step. Give the agent a list of reel URLs and have it branch on status.
How does Facebook Videos Scraper compare to other Facebook video scrapers?
Checked on the Apify Store on 25 July 2026. lexis-solutions/facebook-videos-watch-scraper takes a video URL or a search query, documents a scrollsAmount input for infinite scroll, and publishes an output of title, url, thumbnail, date, views, comments, reactions, channelName and channelUrl — so it does document engagement figures and a channel name, as rounded display strings such as "497K", which this Actor does not return at all. It does not document a direct media or stream URL. easyapi/facebook-videos-scraper is search-driven, taking query and maxItems, and returns videoId, title, description, relative_time_string, thumbnail_image, video_duration, video_url and a video_owner_profile object with name and is_verified; its feature list names "Direct video URL", while the sample output on the same listing shows video_url as a facebook.com/watch/?… page link. The flat-price scraper-engine/facebook-videos-scraper takes the same urls and proxyConfiguration inputs as this Actor but nests a yt-dlp info_dict under a raw key and documents uploader and view_count populated, from a page-video URL. What this pay-per-events Actor documents that none of those listings does is per-row charge classification with uncharged Unavailable and Failed rows, the reel-only URL constraint stated up front, and per-representation DASH parsing with direct fbcdn URLs on every entry. Silence on a listing is not the same as a missing feature.
Does Facebook Videos Scraper return data in a format LLMs can use directly?
Yes. Rows are typed, normalized JSON with the same key names on every run — no HTML parsing, no selectors. The one thing to handle is that rows are not rectangular: read fields with .get() rather than indexing, since Unavailable and Failed rows carry only the run-control keys and the selected-format block is omitted on success rows with no DASH manifest.
What happens when Facebook changes its layout or anti-bot system?
The scraper is maintained, and because it reads the JSON embedded in Facebook's HTML rather than rendered markup, a visual redesign generally does not affect it. Facebook's internal keys are unversioned and can move without notice; when one does, the affected field returns null rather than a stale or fabricated value, and the row still arrives with your field names intact. The classification step is deliberately keyed on the presence of a playable media stream rather than on a status code or a body size, which is the check most likely to survive an upstream change.
Can I use Facebook Videos Scraper without managing proxies or browser infrastructure?
Yes. There is no browser to run — the Actor makes plain HTTP requests — and you never create a proxy account or rotate an IP yourself; tick Apify Proxy and one exit is resolved for the run. Be aware of what it does not do: it does not solve CAPTCHAs, it does not rotate between URLs, and it uses only the first entry of a custom proxyUrls list. The one automatic escalation is a switch to Apify Residential, and it only fires when the run started with no proxy at all and an error message matched a known block signature.
Which Facebook Reel fields work best for AI training data and RAG indexing?
For RAG indexing: description is the only substantial free-text field on this surface and chunks cleanly, with title as a short label and original_url as the citation anchor. For training data: duration, timestamp, upload_date, width, height, resolution and vcodec are the most structurally consistent fields across records, and uploader_id is a stable partition key. All values return as typed primitives, arrays or plain nested objects, so no normalization pass is needed — just remember that absent keys are omitted rather than nulled on non-success rows.
🔗 Related scrapers
| Scraper Name | What it extracts |
|---|---|
| Facebook Video Search Scraper | Public Facebook Watch videos found by keyword, when you do not already have reel permalinks |
| Facebook Hashtag Search Scraper | Public posts and videos behind a Facebook hashtag |
| Facebook Pages Recent Posts Scraper | The newest posts from a list of Facebook pages |
| Facebook Posts Scraper | Text posts, captions and post metadata from Facebook pages |
| Instagram Reel Creator Insights Scraper | Instagram reels with creator-level detail |
| Youtube Shorts Scraper Creator Lead Enrichment | Short-form video metadata with creator contact enrichment on YouTube |
Choosing between the two Facebook video Actors is straightforward: use Facebook Video Search Scraper when you have a topic and need to find videos, and this Actor when you already have reel permalinks and need the caption, the metadata and a real stream URL for each one.
💬 Your feedback
Found a bug, or need a field that is present in Facebook's reel page data but not in the output? Open an issue on the Actor's Issues tab. Reports that include the exact reel URL and the input JSON you ran are the fastest to reproduce and fix.