# Facebook Reels Scraper - $0.35 per 1,000 Reels (`dami_studio/facebook-reels-scraper`) Actor

Scrape any public Facebook Page's Reels for $0.35 per 1,000 - up to 200 per Page. Reel link, video URL, thumbnails, caption, hashtags, duration, posting time and view, reaction, comment and share counts. No login, no cookies. The cheapest Facebook Reels scraper on the market.

- **URL**: https://apify.com/dami\_studio/facebook-reels-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, Videos, Automation
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.35 / 1,000 reels

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Facebook Reels Scraper

Paste public Facebook Page URLs and get their newest Reels back as clean, flat rows: the reel link, a direct video file URL, the thumbnail, the full caption and its hashtags, the exact time it was posted, how long it runs, its frame size, and its view, reaction, comment and share counts — plus who the Page is. No login, no cookies, no Facebook account, no browser, and nothing for you to configure.

- Up to 50 Pages in a single run, and up to 200 reels from each of them — not just the handful a Page shows on its front tab.
- A direct video file URL and two thumbnails on every row, so you can download or embed without a second tool.
- Reaction and comment counts are exact numbers, not rounded labels.
- Captions come back whole, with the hashtags already split out into their own array.
- Date filtering is included at no extra charge — it is a field on the input, not a paid add-on.
- Runs with empty input return a labelled sample row, free, so you can see the shape first.
- A Page that is blocked, private, misspelled or simply has no reels produces an uncharged diagnostic row, never a failed run.

### Price

**$0.35 per 1,000 reels**, plus a **$0.0005 start fee per run** (billed per gigabyte of run memory, so exactly that on the default 1 GB).

This is **the cheapest Facebook Page Reels scraper on the market**, and it is the same rate on every plan, free or paid. There are no volume tiers, no minimum spend, no subscription and no add-on fees. What you read here is what you pay on day one and on day four hundred.

| Reels | Total cost |
|---|---|
| 100 | $0.0355 |
| 1,000 | $0.3505 |
| 10,000 | $3.5005 |
| 100,000 | $35.0005 |

#### What is actually charged

- **One `reel-scraped` event per reel row written to the dataset.** Nothing else is metered per row.
- **Free:** the sample row an empty run returns, and every diagnostic row — a blocked target, a dead URL, a search that matched nothing. Those rows all carry `"charged": false`.
- Reels dropped by your date filter are never charged — the filter runs before billing, and using it costs nothing extra.
- Duplicate reels inside a single run are dropped before they are charged.
- A Page that returns nothing produces one free diagnostic row and no reel charges.
- A run that finds nothing costs the start fee and nothing more.
- Rows never leave the dataset without a charge, and are never charged without a row. The billed event is a named one, so there is no price quietly attached to `apify-default-dataset-item` — the trick that makes some scrapers bill you for their own error messages.

### Input

```json
{
  "startUrls": [
    "https://www.facebook.com/9gag",
    "https://www.facebook.com/natgeo"
  ],
  "resultsLimit": 60,
  "onlyPostsNewerThan": "30 days"
}
```

| Field | What it does |
|---|---|
| `startUrls` | Public Facebook Page URLs, one per line. A bare handle (`9gag`), a Reels-tab URL, a `/pg/` URL, a `/p/` URL and a `profile.php?id=` URL all work. Up to 50 Pages per run. A single `/reel/…` link is not a Page and is rejected with a free diagnostic row. |
| `resultsLimit` | How many recent reels to take from each Page, newest first. Default 50, maximum 200. The first ten cost one request; past that the Actor walks the Page eight reels at a time, so a big number costs more time but roughly the same per reel. Lower it while testing; you pay per reel. |
| `onlyPostsNewerThan` | Optional. Drop reels older than this. Accepts `2026-08-01`, a full ISO timestamp, an epoch, or a relative window such as `7 days`. Free. |
| `onlyPostsOlderThan` | Optional. Drop reels newer than this. Same formats. Free. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for, as `http://user:pass@host:port`. |

Run it with **empty input** and you get one clearly labelled sample row, free, so you can see the output shape before you spend anything.

### Output

One row per reel. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "reel",
  "inputUrl": "https://www.facebook.com/9gag",
  "pageName": "9GAG",
  "pageId": "100064681269092",
  "pageUrl": "https://www.facebook.com/9gag/",
  "pageIsVerified": true,
  "pageProfilePicture": "https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=100064681269092",
  "reelId": "27847866498168467",
  "reelUrl": "https://www.facebook.com/reel/27847866498168467",
  "postId": "1532426152256746",
  "videoUrl": "https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=27847866498168467",
  "thumbnailUrl": "https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=27847866498168467&get_thumbnail=1",
  "previewImageUrl": "https://scontent-yyz1-1.xx.fbcdn.net/v/t15.5256-10/759017053_1030907243061362_759126857526234640_n.jpg",
  "caption": "how fancy you're #9gag #foodart",
  "captionLength": 31,
  "hashtags": [
    "9gag",
    "foodart"
  ],
  "time": "2026-08-15T07:00:01.000Z",
  "timestamp": 1786777201,
  "viewCount": 586000,
  "viewCountText": "586K",
  "reactionsCount": 1833,
  "commentsCount": 15,
  "sharesCount": 33,
  "sharesCountText": "33",
  "durationMs": 24682,
  "durationSeconds": 24.682,
  "width": 1080,
  "height": 1920,
  "aspectRatio": 0.5625,
  "audioTitle": "Esperé Eckard-Lee · Paris Carousel",
  "audioIsOriginal": true,
  "isReshare": false,
  "isSponsored": false,
  "scrapedAt": "2026-08-16T05:08:11.916Z"
}
```

#### Field notes

- `inputUrl` — the Page URL you asked for, so you can group rows by Page after merging several runs.
- `reelUrl` — the canonical `facebook.com/reel/…` permalink. This is the link to share or open in a browser.
- `videoUrl` — Facebook's own public media endpoint for that reel. It is a stable address rather than a signed link that expires in a few hours, so it keeps working in a spreadsheet you open next week — but Facebook only serves the MP4 itself to a crawler identity: fetched with an ordinary browser or HTTP-client user-agent it answers with a small HTML redirect to the reel's watch page instead. Measured: `curl -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' '<videoUrl>' -o reel.mp4` returns video/mp4; the same request with a browser user-agent returns 302 bytes of HTML. The Actor never downloads the file itself — you are charged for the row, not for the video.
- `thumbnailUrl` — a stable thumbnail URL from the same public media endpoint. `previewImageUrl` is the higher-resolution first frame, on a CDN address that does expire — take a copy if you need it to last.
- `caption` — the reel text exactly as posted, including emoji and line breaks. `hashtags` is the same caption with the tags pulled out into an array, without the `#`.
- `time` — the exact posting time in ISO 8601 UTC, taken from the reel itself rather than a "2 days ago" label. `timestamp` is the same moment in epoch seconds.
- `viewCount` — the play count. Facebook only publishes this rounded — it shows "586K", never 586,214 — so `viewCount` is that rounded figure turned into a number and `viewCountText` keeps the original string so nobody mistakes it for exact.
- `reactionsCount` — total reactions of all types at read time, and this one IS exact rather than rounded. `commentsCount` is exact too. Both were present on all 1,031 rows of the last measured sweep; they can still be null on a reel Facebook has not yet published a counter for, which is usually the very newest one on a Page.
- `sharesCount` — shares at read time, rounded the same way views are; `sharesCountText` keeps the original. Null when Facebook publishes no share count for that reel.
- `durationMs` — the reel length in milliseconds, with `durationSeconds` carrying the same value with its fractional part.
- `width` — `width`, `height` and `aspectRatio` describe the video frame. Reels are usually 1080x1920, but Facebook also accepts landscape reels, so do not assume vertical.
- `audioTitle` — the soundtrack label Facebook shows on the reel — a licensed track, or "<Page> · Original audio" when the creator used their own. `audioIsOriginal` distinguishes the two.
- `isReshare` — true when the Page re-shared somebody else's reel rather than posting its own.
- `isSponsored` — true when the reel is a paid placement rather than an organic one.

Every real row carries `"charged": true`. Sample rows carry `"_sample": true` and diagnostic rows carry `"_diagnostic": true` with an `errorCode` you can filter on, and neither is ever billed.

### How it works

- It reads the public, logged-out version of the Page's Reels tab and lifts the reels out of the structured data already inside that page. No headless browser, no rendering, no scrolling, no account and no cookies.
- The first ten reels of a Page arrive in that one request. Beyond that the Actor follows the same paging cursor the Reels tab itself uses, eight reels per call, on the same connection — so going deeper costs a little more time and no extra page loads.
- The engagement counters live in a different part of that same response from the reels themselves, so the read runs on until the counters for every reel found have arrived, and then stops — the rest of the page is deferred video-player state this Actor has no use for.
- Traffic leaves through a large pool of rotating addresses. Facebook throttles per address, so a refusal is answered by moving to a fresh one and asking again, up to eight times per Page, with the request identity varied each time.
- Nothing is charged until a reel row is safely written to your dataset.

### What people use it for

- Short-form content research — pull the newest reels from thirty Pages in your category every morning and sort by `viewCount` to see what is actually working this week.
- Competitive and brand monitoring on the surface Facebook is pushing hardest, with engagement numbers as plain columns rather than screenshots.
- Creator and influencer vetting — reach, reaction rate and posting cadence for a list of Pages, in one run, before you sign anything.
- Hook and format analysis: `caption`, `hashtags`, `durationSeconds` and `aspectRatio` together tell you what the top performers have in common.
- Building a reel archive — `reelUrl`, `videoUrl` and `thumbnailUrl` on every row mean the record survives a deleted reel.
- Feeding a dashboard or a warehouse: rows are flat and typed, so they load without a transformation step.

### Reading the output

Every run writes three kinds of row, and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "reel"`. These are the rows you paid for, one billed event each.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when the input had no Page URLs, and it exists so you can look at the output shape before you spend anything.
- **Diagnostic rows** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` you can switch on: `NOT_FOUND` when there is no public Page at that URL, `BLOCKED` when Facebook served a login interstitial from every address tried, `NO_REELS` when the Page rendered but publishes no reels, `BAD_INPUT` when a URL could not be read as a Page, `TIME_BUDGET` when the run ran out of time before reaching a Page, and `NETWORK` when Facebook could not be reached. Each carries the `inputUrl` it belongs to and a plain-English explanation.

If you only want the data, filter on `charged == true`. The count of those rows always equals the number of events you were billed for, so the dataset is its own invoice.

### Limitations

- The ceiling is 200 reels per Page per run. A Page that has published fewer than you asked for returns what it has — that is not an error, and you are only charged for what arrives.
- Deep runs take longer than shallow ones: the first ten reels of a Page come back in one request, and every eight after that is another. Asking 50 Pages for 200 reels each is a long run, so give it a generous timeout or split it.
- The date filters narrow the reels a Page has published — they cannot reach further back than the Page itself goes. Asking for last year on a Page that started reels in March will correctly return nothing.
- View counts and share counts are published by Facebook only in rounded form ("586K"), so those two columns are rounded. Reaction and comment counts are exact.
- Occasionally Facebook has not yet published any engagement counter for a brand-new reel, and `reactionsCount`, `commentsCount` and `sharesCount` come back null for it; the reel itself, its caption, its view count and its media links are all still there. It is rare - across the last 1,681 measured rows it did not happen once - and it fills in within a day.
- Two of the optional fields are frequently empty because the reels themselves are: `hashtags` is empty when the caption has none, and `audioTitle` is null when the reel carries no soundtrack label. Across a 1,031-row sample they were populated on 32% and 49% of rows respectively. Everything else on this list was populated on 100% of those rows.
- Not every Page has a public Reels tab. Plenty of large Pages post short video to their timeline without it ever appearing under Reels, and those Pages correctly return an uncharged `NO_REELS` row rather than invented data.
- Public Pages only. Personal profiles with a private timeline, Groups, Events and anything behind a login are out of scope and produce an uncharged `NOT_FOUND` or `BLOCKED` row.
- Some Pages are age-restricted or country-restricted and show a logged-out visitor nothing at all. Those return an uncharged diagnostic row, and no amount of retrying changes it.
- Comments themselves are not returned, only the comment count. The same goes for the identities of people who reacted.
- View, reaction, comment and share counts are a snapshot at read time and keep moving on a live reel.
- `videoUrl` points at a public Facebook media address. It is not a licence: what you may do with somebody else's video is between you and them.
- Facebook throttles per address. Under heavy parallel load a Page can come back as `BLOCKED`; re-running a few minutes later normally clears it, and nothing is charged for the attempt.
- Captions are returned as posted. They are not translated, and emoji and line breaks are preserved exactly.

### Questions

**My Page has no @username, only a numeric id. Does it work?**

Yes. Paste whatever Facebook shows you — `facebook.com/profile.php?id=1000...`, the `facebook.com/people/Page-Name/1000.../` form, the `/p/Page-Name-1000.../` form, or just the number on its own. All four read the same Page, with the same fields and the same pagination as a Page that has a custom URL.

**Do I need a Facebook account or cookies?**

No. Nothing in this Actor logs in, holds a session, or asks you for credentials. It reads only what Facebook shows the public web, which is also why private content is out of reach.

**How many reels can I get from one Page?**

Up to 200 per Page per run, set with `resultsLimit`. The default is 50: the first request already carries 10 and the rest are paginated eight at a time, at roughly the same cost per reel. Lower it to 10 if you only want the newest few and want the cheapest possible run. Running it on a schedule and deduplicating on `reelId` builds deep history over time, one cheap run at a time.

**Why is the view count rounded but the reaction count exact?**

Because that is how Facebook publishes them. The reels surface ships plays as a display string like "586K" and ships reactions and comments as real integers. `viewCount` is that string parsed into a number for you, and `viewCountText` keeps the original so the rounding is never hidden.

**Is `videoUrl` a real download link?**

It is Facebook's own public media address for that reel, and it is stable rather than a signed URL that expires — but whether it hands you the file depends on who asks. Fetched with a crawler user-agent it answers `video/mp4`; fetched with an ordinary browser or HTTP-client user-agent it answers a ~300-byte HTML redirect to the reel's watch page instead. So it downloads fine with `curl -A "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" "<videoUrl>" -o reel.mp4`, and not with a plain `curl <videoUrl>`. The Actor never fetches the video itself — you are charged for the row, not for the file.

**What happens if a Page has no reels?**

You get one uncharged `NO_REELS` diagnostic row for that Page, the run carries on to your other Pages, and you are billed nothing for it.

**Does the date filter cost extra?**

No. It is a plain input field, it runs before anything is billed, and reels it removes are never charged. Filtering to last week is strictly cheaper than not filtering.

**Will the run fail if something goes wrong?**

No. A blocked, empty or broken target produces an uncharged diagnostic row and the run still finishes as succeeded. A failed run would still bill you the start fee, which would mean paying to be told something went wrong.

**Can I run this on a schedule?**

Yes, and it is the intended way to use it. Nothing in the run holds state between runs. Schedule the same Page list daily and deduplicate on `reelId` to see only new reels.

**How do I get exactly the rows I paid for?**

Filter the dataset on `"charged": true`. Sample and diagnostic rows are always `false`, and the number of charged rows always equals the number of billed events.

**Do I need to supply a proxy?**

No. The run brings its own egress and its cost is already inside the price you see. The `proxyUrls` field exists only for callers who specifically want traffic to leave through servers they own.

# Actor input Schema

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

Public Facebook Page URLs, one per line. A bare handle (9gag), a Reels-tab URL, a /pg/ URL, a /p/ URL and a profile.php?id= URL all work. Up to 50 Pages per run. A single /reel/ link is not a Page - give the Page it belongs to.

## `resultsLimit` (type: `integer`):

How many recent reels to take from each Page, newest first. Default 50; raise it up to 200 for a deeper crawl. The first render returns 10 reels and the rest are paginated, so a higher number costs more time but works.

## `onlyPostsNewerThan` (type: `string`):

Optional. Drop reels older than this. Accepts 2026-08-01, a full ISO timestamp, an epoch, or a relative window such as "7 days". Filtering is free - dropped reels are never charged.

## `onlyPostsOlderThan` (type: `string`):

Optional. Drop reels newer than this. Same formats as above. Also free.

## `proxyUrls` (type: `array`):

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte. Fill it in only if you specifically want the traffic to leave through proxy servers you already pay for, in the form http://user:pass@host:port.

## `sessionCookies` (type: `array`):

Leave this empty unless you need it. Runs are logged out by default and that is enough for public Facebook content. Facebook shows some things only to a signed-in account, and it limits how fast any one account may read; supplying your own cookie uses your account and your own rate limit, shared with nobody. In Chrome: open facebook.com while signed in, press F12, then Application > Cookies > https://www.facebook.com, and paste the values as "c\_user=<value>; xs=<value>". One line per account. Treat these like a password: anyone with them can act as that account, and Facebook may sign the session out or restrict the account for automated use.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.facebook.com/9gag",
    "https://www.facebook.com/natgeo"
  ],
  "resultsLimit": 50
}
```

# Actor output Schema

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

Every row in the default dataset: inputUrl, pageName, pageId, pageUrl, pageIsVerified, pageProfilePicture, reelId, reelUrl, postId, videoUrl, thumbnailUrl, previewImageUrl, caption, captionLength, hashtags, time, timestamp, viewCount, viewCountText, reactionsCount, commentsCount, sharesCount, sharesCountText, durationMs, durationSeconds, width, height, aspectRatio, audioTitle, audioIsOriginal, isReshare, isSponsored. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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 = {
    "startUrls": [
        "https://www.facebook.com/9gag",
        "https://www.facebook.com/natgeo"
    ],
    "resultsLimit": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/facebook-reels-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "startUrls": [
        "https://www.facebook.com/9gag",
        "https://www.facebook.com/natgeo",
    ],
    "resultsLimit": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/facebook-reels-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "startUrls": [
    "https://www.facebook.com/9gag",
    "https://www.facebook.com/natgeo"
  ],
  "resultsLimit": 50
}' |
apify call dami_studio/facebook-reels-scraper --silent --output-dataset

```

## MCP server setup

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

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Qf0fUl3E1N3Dzda1X/builds/yOK3eblBpafDvOoe1/openapi.json
