# Instagram Reel Scraper - $0.35 per 1,000 Reels (`dami_studio/instagram-reel-scraper`) Actor

Give it Instagram handles and get each profile's reels tab newest-first: play counts, likes, comments, caption, duration, the direct video file, the cover image and the audio track. No login, no cookies, no API key. The cheapest Instagram reel scraper on the market.

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

## Pricing

from $0.35 / 1,000 reel scrapeds

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

## Instagram Reel Scraper

Give it Instagram handles and get each profile's reels tab back, newest first: play count, likes, comments, caption, hashtags, duration, the direct video file, the cover image and the audio track behind the reel. No API key and no browser, and nothing for you to set up — it reads the same public feed the reels tab itself reads.

- Many handles in one run; every reel is one row, deduplicated inside the run.
- Newest first, exactly as Instagram orders the reels tab, with pinned reels flagged rather than silently reordered.
- Stop at a date with `onlyPostsNewerThan`, or cap each profile with `resultsLimit` — you only pay for rows you receive.
- Collab reels are included, with every co-author handle listed.
- Run it with empty input and you get a labelled sample row, free, so you can see the shape before spending anything.
- Nothing to configure. Handles are read without an account, and a built-in session covers the handles Instagram refuses to show a logged-out visitor at all.

### 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 Instagram reel 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`.
- A profile that turns out to be private, deleted or reel-less costs nothing beyond the start fee — it returns a free diagnostic row instead.
- Reels already returned earlier in the same run are dropped before they are charged.
- Reels filtered out by `onlyPostsNewerThan` or `skipPinnedPosts` are never charged, because they never become rows.
- 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
{
  "usernames": [
    "nasa",
    "natgeo",
    "https://www.instagram.com/nike/"
  ],
  "resultsLimit": 30,
  "onlyPostsNewerThan": "3 months",
  "skipPinnedPosts": false
}
```

| Field | What it does |
|---|---|
| `usernames` | One or more Instagram handles. `nasa`, `@nasa`, `https://www.instagram.com/nasa/` and a numeric profile id all work. Up to 100 per run. |
| `resultsLimit` | How many reels to take from each profile, newest first. Default 30, maximum 1,000. This is per profile, so three handles at 30 is up to 90 rows. |
| `maxItems` | Optional overall ceiling for the whole run, across every handle. Useful as a spend guard when you pass a long list of handles. |
| `onlyPostsNewerThan` | Stop once the reels get older than this. Takes a date (`2026-05-01`) or a plain phrase (`3 months`, `2 weeks`, `10 days`). Leave it empty to ignore dates. |
| `skipPinnedPosts` | Set to true to drop the reels the account has pinned to the top of its tab, so you only get the chronological feed. |
| `sessionCookies` | Leave empty. Runs work out of the box using a built-in session for the handles that need one. That session is shared, so on a long list under heavy load some handles can come back blocked; putting your own Instagram cookie here gives the run a rate limit nobody else shares. Paste it as `sessionid=...; csrftoken=...` — Chrome: F12 → Application → Cookies → instagram.com. It is used for this run's requests and nothing else. |
| `proxyUrls` | Leave empty. The run brings its own addresses and the cost of them is already inside the price. Fill this in only if you want 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",
  "inputUsername": "nasa",
  "shortCode": "DcBx_I1iHZW",
  "url": "https://www.instagram.com/reel/DcBx_I1iHZW/",
  "caption": "Check out the eclipse from 50,000 feet in the air!\n\nThese views, including footage from GoPro cameras, were captured by NASA's WB-57 aircraft during the 2026 total solar eclipse. Taking off from Iceland, our pilots studied the Sun's corona as they raced along the path of totality.\n\nCredit: NASA\n\n#NASA #Eclipse2026 #Aircraft",
  "videoPlayCount": 6843356,
  "likesCount": 266714,
  "commentsCount": 1329,
  "videoDuration": 92.9489974975586,
  "videoUrl": "https://scontent-yyz1-1.cdninstagram.com/o1/v/t2/f2/m86/AQPIqo5-Uqofr5Hb3KFa-LDO1CW2ZC_FITrcfxz-9Cvg.mp4?...",
  "displayUrl": "https://scontent-yyz1-1.cdninstagram.com/v/t51.82787-15/774149093_18637093324049152_5106811212992624343_n.jpg?...",
  "audioTitle": "Original audio",
  "ownerUsername": "nasa",
  "isPinned": false,
  "id": "3963668990156895830",
  "type": "Video",
  "productType": "clips",
  "hashtags": [
    "NASA",
    "Eclipse2026",
    "Aircraft"
  ],
  "mentions": [],
  "videoViewCount": 6843356,
  "dimensionsWidth": 1080,
  "dimensionsHeight": 1920,
  "takenAtTimestamp": 1786726237,
  "timestamp": "2026-08-14T16:50:37.000Z",
  "ownerFullName": "NASA",
  "ownerId": "528817151",
  "ownerIsVerified": true,
  "coauthorUsernames": [
    "gopro"
  ],
  "isPaidPartnership": false,
  "hasAudio": true,
  "audioId": "27860248933613853",
  "audioArtist": "nasa",
  "audioIsOriginal": true,
  "audioDurationMs": 92933,
  "inputUrl": "https://www.instagram.com/nasa/reels/",
  "scrapedAt": "2026-08-15T22:22:06.980Z"
}
```

#### Field notes

- `videoPlayCount` — plays as Instagram reports them at read time. `videoViewCount` carries the same number, kept so an existing pipeline written against another reels feed keeps parsing.
- `videoUrl` — a direct, playable MP4 link. It is signed and expires after a few hours, so download it in the same session rather than storing the link — shortened above for legibility, the real row carries the full URL.
- `displayUrl` — the cover frame as a JPEG, also a signed link — shortened above for the same reason.
- `isPinned` — true when the account has pinned this reel to the top of its tab. Pinned reels are hoisted out of date order, which is why they are flagged rather than silently moved.
- `coauthorUsernames` — the other accounts on a collab reel. A collab reel appears on every co-author's tab, so you may see it under more than one handle if you scrape both.
- `ownerUsername` — who actually posted the reel. On a collab this can differ from the handle you asked for, which is why `inputUsername` is a separate field.
- `audioTitle` — "Original audio" for a sound the account made itself, or the track title for a licensed song. `audioIsOriginal` tells you which.
- `videoDuration` — length in seconds, as a decimal.
- `timestamp` — when the reel was posted, ISO 8601 in UTC. `takenAtTimestamp` is the same moment as a Unix number.
- `id` — the numeric media id. Stable, and safe as a primary key across re-runs; `shortCode` is equally stable and is what appears in the public URL.

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 calls the same public feed the reels tab on a profile page calls, and reads the JSON that comes back — no page rendering and no headless browser.
- Every handle is tried logged-out first, because that is what the reels tab of a public profile answers. Only the handles Instagram will not show that way — measured as a property of the account, not of the connection — fall back to a built-in session, so the fallback is reserved for the handles that actually need it.
- Each handle is turned into its numeric profile id once, and the id is only accepted if Instagram states it belongs to the handle you asked for — a collab post on the profile is owned by the other account, and taking that id would quietly return the wrong profile's reels.
- Several profiles are read at the same time, each on its own address, so a long list of handles finishes in a fraction of the time and costs a fraction of the compute.
- The reels feed is then paged with a cursor until your limit or your date cut-off is reached.
- Traffic leaves through a large pool of rotating addresses. Instagram meters these feeds per address, so a refusal is answered by moving to a new address rather than by waiting, and nothing is billed per gigabyte.
- Reel ids already seen in the run are skipped, so a collab reel that appears under two handles is only charged once.

### What people use it for

- Tracking a set of creators or brands week by week: schedule the same handles daily and diff on `id` to see what is new and how the play counts moved.
- Finding out what actually works on an account before you brief a campaign — sort the rows by `videoPlayCount` and read the top ten captions.
- Building a swipe file of reference reels with the cover image and a playable video link already attached.
- Audio research: `audioId`, `audioTitle` and `audioIsOriginal` show which sounds an account leans on and which of those are its own.
- Competitive benchmarking across a whole category at once — pass thirty handles, cap each at ten reels, and you have a category snapshot in one run.
- Feeding a dashboard or a warehouse with a flat, typed table instead of scraping a rendered page that changes shape every month.

### Reading the output

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

- **Real rows** carry `"charged": true` and `"recordType": "reel"`. One billed event each, and nothing else is metered per row.
- **The sample row** carries `"_sample": true` and `"charged": false`. There is exactly one, it only appears when the input had no handles in it, 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` for a handle Instagram says does not exist, `NOT_AVAILABLE` when Instagram returns an empty answer for the handle from every address tried and from a second endpoint — either a typo or an account that is closed to logged-out visitors, `NO_RESULTS` for a public account with no reels, `BAD_INPUT` for an entry that is not a handle, `BLOCKED` when Instagram stopped answering for that profile, `TIME_BUDGET` when the run ran out of time before reaching a handle, and `DEAD_LIST` once when six handles in a row have returned nothing and the run stops early rather than spend your time on the rest. Each one names the `inputUsername` it belongs to and explains itself in plain English.

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

### Handling the video and cover links

The `videoUrl` and `displayUrl` fields are direct, signed links to Instagram's media servers. They play and download without any header or cookie, but the signature expires within a few hours, so treat them as something to fetch now rather than to store. If you need the files kept, download them in the same job that reads the dataset. The reel's own page URL (`url`) never expires, so keep that as the permanent reference.

### Limitations

- Public accounts only. A private account returns a free diagnostic row saying so — there is no way to read a private profile without an account, and this Actor never creates or uses one.
- A small number of otherwise public accounts are closed to logged-out visitors entirely: Instagram answers for them with an empty result no matter where the request comes from, verified from a clean home connection as well as from the run's own addresses. Those handles are retried with the built-in session, and if that does not open them either they return a free NOT\_AVAILABLE row; no proxy or re-run changes it.
- The built-in session is shared, and Instagram rate-limits per account rather than per caller, so it is not unlimited throughput. Under heavy concurrent load supply your own cookie in `sessionCookies` for a rate limit that is yours alone. If the built-in session runs out mid-run, the run finishes with what it collected and says so on its status line rather than failing.
- The reels tab only. Photo posts, carousels, stories, highlights and IGTV-only uploads are out of scope; use a posts or stories Actor for those.
- Play counts, likes and comment counts are a snapshot at read time. They keep moving, so two runs an hour apart will legitimately disagree.
- Instagram hoists pinned reels above the chronological feed. The date filter therefore ignores pinned reels when deciding where to stop, and `isPinned` tells you which rows were hoisted.
- A collab reel is on every co-author's tab, so scraping two co-authors returns it under both handles — once per handle, deduplicated only within a single run.
- Video and cover links are signed and expire after a few hours. The reel page URL does not.
- Some accounts hide their reels tab or have never posted a reel; those return a free `NO_RESULTS` row rather than an error.
- Instagram meters these feeds per address. A very deep pull on many handles at once can hit a refusal, and the run then reports an honest `BLOCKED` row for that handle with the reels it did get already delivered and charged.
- The ceiling is 100 handles per run and 1,000 reels per handle. Split larger jobs across runs.
- Comment text, liker lists, view-through rates, follower counts and anything else behind a login are not available and are not returned.

### Questions

**Do I need an Instagram account, a cookie or an API key?**

No. Runs work out of the box with nothing filled in. Handles are read the way an anonymous visitor reads a reels tab, and the handles Instagram will not show that way fall back to a built-in session. Private accounts are still out of reach either way.

**Then what is `sessionCookies` for?**

A private rate limit. The built-in session is shared between runs, and Instagram counts requests per account rather than per caller, so a busy hour is a slower hour for everyone using it. Paste your own Instagram cookie into `sessionCookies` and this run uses only that — its own quota, nobody else's. It is used for the run's requests and nothing else: never stored, never logged, never written into the dataset. Optional, and most runs never need it.

**What does a run actually cost me?**

The start fee once, plus one charge per reel row you receive. A run over three handles at 30 reels each is 90 charged rows. Sample rows and diagnostic rows are free, and a handle that returns nothing costs nothing beyond the start fee.

**Will the run fail if a handle is wrong or an account is private?**

No. It writes an uncharged diagnostic row for that handle, carries on with the rest, and finishes as succeeded. A failed run would still bill the start fee, which would mean paying to be told about a typo.

**How do I get only the newest reels?**

Set `onlyPostsNewerThan` to a date or a phrase like `2 weeks`. The run stops walking a profile as soon as it passes the cut-off, so a daily schedule reads only what is new and charges only for that.

**Can I pass profile URLs or numeric ids instead of handles?**

Yes. `nasa`, `@nasa`, `https://www.instagram.com/nasa/` and the numeric profile id are all accepted in the same list. A single reel URL is not a profile, so it comes back as a free `BAD_INPUT` row telling you so.

**Why did I get a reel from a different account?**

Collab reels. When two accounts publish a reel together it sits on both tabs, and the row keeps `ownerUsername` as whoever posted it while `inputUsername` stays the handle you asked for.

**Is it safe to run on a schedule?**

Yes. Nothing is held between runs, so the same input can run hourly or daily. Use `id` or `shortCode` to work out which rows are new since last time.

# Actor input Schema

## `usernames` (type: `array`):

The profiles to read the reels tab of. A handle (nasa), an @handle, a profile URL (https://www.instagram.com/nasa/) or a numeric profile id all work. Up to 100 per run. Public accounts only - a private account returns a free row saying so.

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

How many reels to take from each profile, newest first. This is per profile, so three handles at 30 is up to 90 rows. Keep it low while testing - you pay per reel row.

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

Stop walking a profile once its reels get older than this. Takes a date (2026-05-01) or a plain phrase (3 months, 2 weeks, 10 days). Leave empty to ignore dates. Pinned reels are hoisted out of date order by Instagram, so they are not used to decide where to stop.

## `skipPinnedPosts` (type: `boolean`):

Drop the reels the account has pinned to the top of its tab, so you only get the chronological feed. Off by default; pinned reels are flagged with isPinned either way.

## `maxItems` (type: `integer`):

Optional overall ceiling across every handle, useful as a spend guard when you pass a long list. Leave empty for no ceiling beyond the per-profile limit.

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

Leave this empty. Reels tabs are read without an account, and a built-in session is only used for the handles Instagram refuses to show a logged-out visitor at all. That built-in session is shared between runs, so if you are pulling a long list and handles come back empty or blocked, paste your own Instagram cookie here and this run gets a rate limit nobody else shares. In Chrome: F12 → Application → Cookies → instagram.com. The `sessionid` cookie is the one that matters; `csrftoken` alongside it is better. Paste it as `sessionid=...; csrftoken=...`, one entry per account. Cookies are used for this run's requests and nothing else — never stored, never logged, never written to the dataset.

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

Leave this empty. By default the run rotates a large pool of addresses that cost you nothing per gigabyte, and that cost is already inside the price. 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.

## Actor input object example

```json
{
  "usernames": [
    "nasa",
    "natgeo"
  ],
  "resultsLimit": 30
}
```

# Actor output Schema

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

Every row in the default dataset: inputUsername, shortCode, url, caption, videoPlayCount, likesCount, commentsCount, videoDuration, timestamp, videoUrl, displayUrl, audioTitle, ownerUsername, isPinned, id, type, productType, hashtags, mentions, videoViewCount, dimensionsWidth, dimensionsHeight, takenAtTimestamp, ownerFullName, ownerId, ownerIsVerified, coauthorUsernames, isPaidPartnership, hasAudio, audioId, audioArtist, audioIsOriginal, audioDurationMs, inputUrl, scrapedAt. 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 = {
    "usernames": [
        "nasa",
        "natgeo"
    ],
    "resultsLimit": 30,
    "onlyPostsNewerThan": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/instagram-reel-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 = {
    "usernames": [
        "nasa",
        "natgeo",
    ],
    "resultsLimit": 30,
    "onlyPostsNewerThan": "",
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/instagram-reel-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 '{
  "usernames": [
    "nasa",
    "natgeo"
  ],
  "resultsLimit": 30,
  "onlyPostsNewerThan": ""
}' |
apify call dami_studio/instagram-reel-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/instagram-reel-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/ubAco4VCk6men9eah/builds/OKINh5p8QKP7oFi1m/openapi.json
