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

Scrape Instagram posts and reels from links. You get the caption, likes, comments and publish time. Also every carousel image and video URL. Plus the owner, hashtags, mentions and tagged users. It runs logged out, with no browser. $0.18 per 1,000 posts, flat on every plan.

- **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:** 20 total users, 13 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/actors/running/actors-in-store.md#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 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.

Nothing to set up, and no browser is started.

- 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's dead, private or deleted produces a free diagnostic row, never a charge.

### Price

**$0.18 per 1,000 posts.**

$0.18 on the free plan, $0.18 on a paid one. No volume tiers, no minimum spend, nothing charged
monthly.

#### What is actually charged

- **One `post-scraped` event per post row written to the dataset.** Nothing else is metered per row.
- **Never charged:** the sample row, and any diagnostic row. A dead URL, a deleted post and a link
  the target refused all come back at no cost, marked `"charged": false`.
- Duplicate links in your list are collapsed before the run starts, so the same post is never
  charged twice.
- Links that aren't Instagram post or reel links are reported free of charge and never fetched.
- Every row has a charge behind it and every charge has a row, so the two reconcile. The billed
  event is named `post-scraped`, not Apify's generic per-item event.

### Input

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

| Field | What it does |
|---|---|
| `postUrls` | The post, reel or IGTV links you want. Bare shortcodes such as `DbtErSrlB2J` 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. |
| `sessionCookies` | Optional. Leave it empty for public posts. See **About logins** below. |
| `proxyUrls` | Leave empty. Fill it in only if you want the traffic to leave through proxy servers you already pay for. |

Hand it **no links at all** and it writes one labelled sample row, free of charge, so you can see
the shape first.

### Output

One row per post. Real row from a real run, with the media URLs and the carousel frames trimmed:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "post",
  "inputUrl": "https://www.instagram.com/p/DbtErSrlB2J/",
  "id": "3957840214710558089",
  "shortCode": "DbtErSrlB2J",
  "url": "https://www.instagram.com/p/DbtErSrlB2J/",
  "type": "Sidecar",
  "productType": "carousel_container",
  "isReel": false,
  "isSponsored": false,
  "caption": "Where in the (Martian) world is Curiosity?\n\nOur Curiosity Mars rover has started its climb up a Martian valley called Valle Grande…\n\n#NASA #Mars #CuriosityRover",
  "hashtags": ["NASA", "Mars", "CuriosityRover"],
  "mentions": [],
  "likesCount": 273272,
  "commentsCount": 1692,
  "videoViewCount": null,
  "likesAndViewCountsDisabled": false,
  "isCommentsDisabled": false,
  "timestamp": "2026-08-06T15:49:01.000Z",
  "displayUrl": "https://scontent-fra3-1.cdninstagram.com/v/t51.82787-15/766074877_...jpg",
  "videoUrl": null,
  "videoDuration": null,
  "images": ["…jpg", "…jpg", "…jpg", "… 5 more …"],
  "videos": [],
  "childPosts": ["… one object per frame, trimmed …"],
  "childPostsCount": 8,
  "dimensionsWidth": 495,
  "dimensionsHeight": 292,
  "alt": null,
  "ownerId": "528817151",
  "ownerUsername": "nasa",
  "ownerFullName": "NASA",
  "ownerIsVerified": true,
  "ownerIsPrivate": false,
  "ownerProfilePicUrl": "https://scontent-fra3-1.cdninstagram.com/v/t51.2885-19/%E2%80%A6jpg",
  "ownerProfileUrl": "https://www.instagram.com/nasa/",
  "coauthorProducers": [],
  "taggedUsers": [],
  "locationName": null,
  "locationId": null,
  "musicArtist": null,
  "musicTitle": null,
  "source": "graphql",
  "scrapedAt": "2026-08-20T19:51:52.669Z"
}
```

A reel row fills in `videoUrl`, `videoDuration` and `videos`, and carries `type: "Video"` with
`productType: "clips"`.

#### Field notes

- `type` — `Image`, `Video` or `Sidecar`. `Sidecar` is Instagram's name for a carousel.
- `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's 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 sharing 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.

### About logins

Runs are logged out. A public post normally answers a logged-out reader on the first attempt, and
that's how the vast majority of links are read.

Instagram does sometimes refuse a logged-out reader on a specific post or during a busy stretch.
When that happens you get a free `RATE_LIMITED` or `BLOCKED` diagnostic row for that one link and
the run carries on. If you're hitting that a lot, `sessionCookies` lets you paste your own Instagram
cookie (`sessionid=…; csrftoken=…`, from Chrome's F12 → Application → Cookies → instagram.com) and
the run reads through your account's own rate limit instead. Treat those values like a password.
They're used for that run's requests and nothing else, never stored, never logged, never written to
the dataset.

Nothing is ever posted, liked, followed or changed on any account.

### How it works

It asks Instagram directly for the post record the public post page is built from, and reads the
answer as structured data. No page is rendered and no browser is started.

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

### Diagnostic rows

A link that doesn't work out comes back as a row rather than failing the run. Those rows carry
`"_diagnostic": true`, `"charged": false` and an `errorCode`: `NOT_FOUND` when a post is deleted,
private or the link is wrong; `BAD_INPUT` when an entry isn't 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.

The free sample row is tagged `"_sample": true` and only appears when the run was given no links.
Filter on `charged == true` for the data alone, and counting those rows checks the bill.

### Limitations

- It reads posts, not profiles. Give it post, reel or IGTV links. It doesn't take a username and
  walk somebody's grid.
- Public posts only. A post on a private account isn't visible to anyone who isn't logged in and
  following, so it returns a free `NOT_FOUND` row rather than data.
- View and play counts aren't exposed to logged-out callers, so `videoViewCount` is almost always
  `null`. Likes and comments are exposed and are returned.
- Comments themselves aren't 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. Don't 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.
- 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's 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.

### Questions

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

Not for public posts. Runs work with nothing filled in and read post links the way a logged-out
visitor does. The `sessionCookies` field is there for the case where Instagram starts refusing your
links and you'd rather read through your own account's rate limit.

**What happens to a link that's dead or private?**

One uncharged diagnostic row for that link with `errorCode: "NOT_FOUND"`, and the run carries on
through the rest of your list. You're 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 finishes as succeeded.

**Do I need a proxy?**

No. Egress is included in the rate above and there's nothing to set up. `proxyUrls` is there only
for people who already pay for their own servers and want the traffic to leave through those.

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

# 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 for public posts. Runs are logged out and that is enough for the vast majority of links. Instagram does sometimes refuse a logged-out reader on a specific post, and those links come back as free diagnostic rows; if that happens often, paste your own Instagram cookie here and the run reads through your account's own rate limit instead. 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/1arGFvJwZ1OBsJbzi/openapi.json
