# Instagram Post Scraper - $0.18/1k, No Login (`dami_studio/instagram-post-scraper`) Actor

Turn Instagram post and reel links into clean data rows at $0.18 per 1,000 posts - the cheapest on the market. Caption, likes, comments, publish time, every carousel image and video URL, owner, hashtags and mentions. No login, no browser.

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

## Pricing

Pay per event

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 Post Scraper

Paste Instagram post or reel links and get one clean row per post: the full caption, the like and comment counts, the exact publish time, every image and video URL including all the frames of a carousel, the owner, the hashtags, the mentions, the tagged users and the co-authors. No browser, and nothing to set up — runs work out of the box.

- Takes post links, reel links, IGTV links or bare shortcodes — mix them freely in one run.
- Carousels come back whole: every child image and video URL, not just the cover frame.
- Reels come back with the video file URL and its exact duration in seconds.
- Duplicate links are removed before anything is charged, so pasting a messy list is safe.
- A link that is dead, private or deleted produces a free diagnostic row, never a charge.
- Run it with empty input and you get a free labelled sample row so you can see the shape first.
- Nothing to configure. Links are read without an account, and a built-in session covers the occasional post Instagram will not serve a logged-out reader.

### Price

**$0.18 per 1,000 posts**, 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 post 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.

| Posts | Total cost |
|---|---|
| 100 | $0.0185 |
| 1,000 | $0.1805 |
| 10,000 | $1.8005 |
| 100,000 | $18.0005 |

#### What is actually charged

- **One `post-scraped` event per post 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`.
- Duplicate links in your list are collapsed before the run starts, so the same post is never charged twice.
- Links that are not Instagram post or reel links are reported free of charge and never fetched.
- 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
{
  "postUrls": [
    "https://www.instagram.com/p/DbwWBxpDm_0/",
    "https://www.instagram.com/reel/DcCH2ZygIiP/",
    "DbtErSrlB2J"
  ],
  "maxItems": 10
}
```

| Field | What it does |
|---|---|
| `postUrls` | The post, reel or IGTV links you want. Bare shortcodes such as `DbwWBxpDm_0` work too, and so do `instagram.com/username/p/<shortcode>/` links copied from a profile. Up to 5,000 per run; duplicates are removed first. |
| `maxItems` | Stop after this many posts, counting from the top of your list. Leave it empty to take every link you supplied. Keep it low while you are testing — you pay per post. |
| `sessionCookies` | Leave empty. Runs work out of the box using a built-in session for the posts that need one. That session is shared, so under heavy concurrent load you may see rows 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. 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 post. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "post",
  "inputUrl": "https://www.instagram.com/p/DbwWBxpDm_0/",
  "id": "3958760951503548404",
  "shortCode": "DbwWBxpDm_0",
  "url": "https://www.instagram.com/p/DbwWBxpDm_0/",
  "type": "Video",
  "productType": "clips",
  "isReel": true,
  "isSponsored": false,
  "caption": "Are you team LEGO Charizard 🔥, or LEGO Jigglypuff 🎀? Watch the most epic LEGO Pokémon Trainer battle between Finn Wolfhard 🏆 and Terry Crews 💪 \n\n#LEGO #Pokemon #LEGOPokemon",
  "hashtags": [
    "LEGO",
    "Pokemon",
    "LEGOPokemon"
  ],
  "mentions": [],
  "likesCount": 259719,
  "commentsCount": 1302,
  "videoViewCount": null,
  "likesAndViewCountsDisabled": false,
  "isCommentsDisabled": false,
  "timestamp": "2026-08-08T14:00:05.000Z",
  "displayUrl": "https://scontent.cdninstagram.com/v/t51.82787-15/768860404_18619203115007445_n.jpg",
  "videoUrl": "https://scontent.cdninstagram.com/o1/v/t2/f2/m86/AQMfU9jUuK82HTk2KE6vE8OoaDkCtaMPVX3MEaAQxxWerJREhWEM.mp4",
  "videoDuration": 30.037,
  "images": [
    "https://scontent.cdninstagram.com/v/t51.82787-15/768860404_18619203115007445_n.jpg"
  ],
  "videos": [
    "https://scontent.cdninstagram.com/o1/v/t2/f2/m86/AQMfU9jUuK82HTk2KE6vE8OoaDkCtaMPVX3MEaAQxxWerJREhWEM.mp4"
  ],
  "childPosts": [],
  "childPostsCount": 0,
  "dimensionsWidth": 1080,
  "dimensionsHeight": 1920,
  "alt": null,
  "ownerId": "196743444",
  "ownerUsername": "lego",
  "ownerFullName": "LEGO",
  "ownerIsVerified": true,
  "ownerIsPrivate": false,
  "ownerProfilePicUrl": "https://scontent.cdninstagram.com/v/t51.2885-19/456426104_487557390884013_n.jpg",
  "ownerProfileUrl": "https://www.instagram.com/lego/",
  "coauthorProducers": [
    "finnwolfhardofficial",
    "pokemonnewsuk",
    "pokemon",
    "terrycrews"
  ],
  "taggedUsers": [
    {
      "username": "terrycrews",
      "fullName": "Terry Crews",
      "id": "196598493"
    },
    {
      "username": "pokemon",
      "fullName": "Pokémon",
      "id": "1711337410"
    }
  ],
  "locationName": null,
  "locationId": null,
  "musicArtist": null,
  "musicTitle": "Original audio",
  "source": "graphql",
  "scrapedAt": "2026-08-15T22:26:02.669Z"
}
```

#### Field notes

- `type` — `Image`, `Video` or `Sidecar`. `Sidecar` is Instagram's name for a carousel — a post holding several images or clips.
- `productType` — the finer-grained kind: `feed` for an ordinary post, `clips` for a reel, `carousel_container` for a carousel, `igtv` for a long-form video.
- `timestamp` — the moment the post was published, normalised to ISO 8601 in UTC. It never moves, so it is the right key for sorting and for date filtering after the fact.
- `likesCount / commentsCount` — a snapshot at the moment the row was read, not at publish time. On an account that has hidden its counts these come back `null` and `likesAndViewCountsDisabled` is `true`.
- `images / videos` — flat arrays of every media URL in the post, in carousel order. For a single-image post `images` holds one URL; for a six-frame carousel it holds six.
- `childPosts` — the same carousel frames as objects, each with its own id, shortcode, type, dimensions and alt text — use this when you need to tell which frame is a video.
- `videoDuration` — length of the clip in seconds, to three decimals. `null` on a still image.
- `taggedUsers` — people tagged in the photo itself, as `{username, fullName, id}`. People merely @-mentioned in the caption are in `mentions` instead.
- `coauthorProducers` — usernames of accounts that share authorship of a collab post; empty for an ordinary post.
- `displayUrl / videoUrl` — direct links to the media on Instagram's CDN. They carry a signed query string that expires after a few days, so download what you need in the same session rather than storing the URL for later.
- `source` — which reader produced the row. `graphql` is the normal one; `post-page` means the run fell back to the slower reader mid-flight. The fields are identical either way.

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 asks Instagram directly for the post record that the public post page itself is built from, and reads the answer as structured data. No page is rendered and no browser is started.
- Every post is tried without a session first, because a post is public and normally answers a logged-out reader on the first attempt. Only when Instagram refuses twice, on two different addresses, does the run fall back to a built-in session — so the fallback is reserved for the posts that actually need it.
- Requests leave through a large pool of rotating addresses, and the run moves to a fresh one every few requests, so the per-address rate limit is answered by moving rather than by waiting.
- Posts are fetched several at a time, which is why a hundred links take seconds rather than minutes.
- A second, slower reader is kept in reserve. If the fast path ever stops answering, the run switches to it automatically, writes one free note in the dataset saying so, and carries on returning the same fields.
- Nothing is stored between runs, so the same input always produces the same rows plus whatever changed on Instagram since.

### What people use it for

- Measuring campaign performance: drop in the links your brand and your agency posted this month and get likes, comments, publish times and captions in one table you can pivot.
- Influencer vetting: pull the last twenty posts of a creator you are about to pay and check the real engagement numbers instead of the ones in the media kit.
- Archiving your own content: every image and video URL of every carousel frame, so a back-up script has something concrete to fetch.
- Feeding a content dashboard or a warehouse — the rows are flat, typed and deduplicated, so they load into a spreadsheet or a database with no cleaning step.
- Enriching a list of links you already collected elsewhere: this Actor is built to take URLs in and give full records out, which is exactly the missing half of a discovery tool.
- Tracking hashtag and mention usage over time by diffing the `hashtags` and `mentions` arrays across scheduled runs.

### Reading the output

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

- **Real rows** carry `"charged": true` and `"recordType": "post"`. 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 links, 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 a post is deleted, private or the link is wrong; `BAD_INPUT` when an entry in your list is not an Instagram post link at all; `RATE_LIMITED` or `BLOCKED` when the target pushed back on that one link; `TIME_BUDGET` when the run ran out of time before reaching a link. Each carries the `inputUrl` it belongs to, so you can retry exactly those.

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

- It reads posts, not profiles. Give it post, reel or IGTV links; it does not take a username and walk somebody's grid.
- Only public posts. A post on a private account is not visible to anyone who is not logged in and following, so it returns a free `NOT_FOUND` row rather than data.
- View and play counts are not exposed to logged-out callers, so `videoViewCount` is almost always `null`. Likes and comments are exposed and are returned.
- Comments themselves are not returned — only how many there are.
- Like and comment counts are a snapshot at read time and will differ from what you see if you open the post a minute later.
- Media URLs are signed and expire after a few days. Fetch the files during the run or shortly after; do not treat the URL as permanent.
- Carousel child dimensions are whatever Instagram reports for that frame, which for some older posts is a scaled size rather than the original.
- Age-restricted posts, and posts removed for a copyright claim, return a free `NOT_FOUND` row.
- Instagram rate-limits per address. The run handles that by moving addresses, but a very large single run will be slower per post than a small one.
- The built-in session that covers gated posts 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.
- If the first 40 links in a list all return nothing, the run stops there rather than reading the rest, writes one free note saying so, and charges nothing. That is a guard against a list of dead links, not a limit on real work — a single good post resets it.
- The ceiling is 5,000 links per run. For more than that, split the work across runs — nothing is held between runs, so that is safe.

### Questions

**Do I need an Instagram account, a session cookie or a login?**

No. Runs work out of the box with nothing filled in. Post links are read the way a logged-out visitor reads them, and for the occasional post Instagram will not serve that way there is a built-in session behind the scenes. Nothing is ever posted, liked, followed or changed.

**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 happens to a link that is dead or private?**

You get one uncharged diagnostic row for that link with `errorCode: "NOT_FOUND"`, and the run carries on through the rest of your list. You are never billed for a link that returned nothing.

**Does a carousel count as one post or as several?**

One. A carousel is one row, one charge, with every frame's URL inside it — however many frames it has.

**Can I hand it a reel link?**

Yes. Reel links, post links, IGTV links and bare shortcodes all resolve to the same record, and reels come back with the video URL and its duration.

**Will the run fail if Instagram blocks something?**

No. A blocked or empty link produces an uncharged diagnostic row explaining what happened 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.

**Do I need 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 already own.

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

Yes. Nothing is held between runs, so a schedule with the same links is safe and gives you a clean time series of engagement counts. Use `shortCode` as the key when you diff one run against the next.

**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.

# Actor input Schema

## `postUrls` (type: `array`):

One or more Instagram post, reel or IGTV links — for example https://www.instagram.com/p/DbtErSrlB2J/ or https://www.instagram.com/reel/DcCH2ZygIiP/. Bare shortcodes such as DbtErSrlB2J work too. Duplicates are removed before anything is charged. Up to 5,000 links per run.

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

Stop after this many posts, counting from the top of your list. Leave it empty to scrape every link you supplied. Keep it low while you are testing — you pay per post.

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

Leave this empty. Post links are read without an account, and a built-in session is used only for the occasional post Instagram will not serve to a logged-out reader. That built-in session is shared, so if you are running heavily and see rows come back blocked, paste your own Instagram cookie here and the 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 through 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.

## Actor input object example

```json
{
  "postUrls": [
    "https://www.instagram.com/p/DbtErSrlB2J/",
    "https://www.instagram.com/reel/DcCH2ZygIiP/"
  ],
  "maxItems": 10
}
```

# Actor output Schema

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

Every row in the default dataset: shortCode, url, type, ownerUsername, caption, likesCount, commentsCount, timestamp, displayUrl, videoUrl, childPostsCount, hashtags, mentions, locationName, id, inputUrl, productType, isReel, isSponsored, images, videos, childPosts, videoDuration, videoViewCount, dimensionsWidth, dimensionsHeight, alt, likesAndViewCountsDisabled, isCommentsDisabled, ownerId, ownerFullName, ownerIsVerified, ownerIsPrivate, ownerProfilePicUrl, ownerProfileUrl, coauthorProducers, taggedUsers, locationId, musicArtist, musicTitle, 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 = {
    "postUrls": [
        "https://www.instagram.com/p/DbtErSrlB2J/",
        "https://www.instagram.com/reel/DcCH2ZygIiP/"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/instagram-post-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 = {
    "postUrls": [
        "https://www.instagram.com/p/DbtErSrlB2J/",
        "https://www.instagram.com/reel/DcCH2ZygIiP/",
    ],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/instagram-post-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 '{
  "postUrls": [
    "https://www.instagram.com/p/DbtErSrlB2J/",
    "https://www.instagram.com/reel/DcCH2ZygIiP/"
  ],
  "maxItems": 10
}' |
apify call dami_studio/instagram-post-scraper --silent --output-dataset

```

## MCP server setup

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