# Instagram Posts Scraper & Instagram Reels Scraper, No Login (`hyperbach/instagram-posts-scraper`) Actor

Scrape Instagram profile posts and reels, no login: an Instagram post scraper and reels scraper for usernames, profile links or post URLs. Caption, hashtags, exact time, likes, comments, reel plays, owner, coauthors, the real pinned flag, paid flag, location, alt text, carousel images, video URLs.

- **URL**: https://apify.com/hyperbach/instagram-posts-scraper.md
- **Developed by:** [Hyperbach](https://apify.com/hyperbach) (community)
- **Categories:** Social media, Videos, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.35 / 1,000 post or reels

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Instagram Posts Scraper & Instagram Reels Scraper, No Login

**Instagram posts and reels as data, logged out: usernames, profile links or profile ids in — or post and reel links — and one row per post out.** Each post has its whole caption with its hashtags and mentions, the **exact publish time**, the likes (**null when the owner hides them, never a fake 0**), the comment count, a reel's **plays**, duration and music, the owner and their followers, the coauthors of a collab post, the **paid-partnership flag**, the **owner's own pinned flag** (a coauthor's pin is not counted), the tagged accounts, the location, Instagram's alt text and topic tags, every carousel item, and the image, video and audio URLs with the time they expire. Pasted links carry the post's latest comments; profile rows can too. Every profile gets a free summary row with what was read and a verdict — COMPLETE (the profile's list ended), CAPPED, DATE_LIMIT or PARTIAL — and a private, missing or age-gated profile, a deleted post or a value that names nothing is a free row that says which. No login, no Instagram account, no cookie, proxies handled for you.

### Why this Instagram posts scraper

- **The pinned flag, right.** A pinned post is the owner's own pin, read from the order of the owner's list and Instagram's grid; a coauthor's pin is not the owner's. On National Geographic's three pinned posts the three biggest Instagram scrapers all say `isPinned: false`; this Actor flags the three, and leaves out the post a coauthor pinned.
- **The newest posts, not the oldest pins.** A pinned post counts at its own date: three old pins never take the places of three new posts in your `maxPostsPerProfile`, and they never stop a date cut-off early. Every row has its `position` in the profile, newest first.
- **Deep, with a proof.** The walk reads the profile's posts in date order to Instagram's end flag, 50 posts a call (our source study walked a 1,066-post profile to its last post that way), with the owner's own list beside it for the pins, the type, the tags and the paid flag; a post the owner's list skips at one of its page boundaries is still delivered. Each profile's summary row says how many posts were looked at and delivered and whether the list ended (COMPLETE) or your cap came first (CAPPED): a short dataset is never silent.
- **Counts that do not lie.** Likes are exact; a post whose owner hides the likes gets `likes` null and `likes_hidden` true, never Instagram's placeholder 0. A reel's plays come from the reels list, the counter the app shows, not a smaller web counter; a row without plays says why (`plays_status`).
- **Collab posts as they are.** A post another account owns but lists in this profile's grid keeps its real owner, with the profile you gave in `profile` and in the coauthors. The coauthors come with their usernames.
- **One row per post, free rows for the rest.** A post given twice, or both as a link and in a profile's posts, is one row and one charge. A post left out by a filter, an old post `onlyNewPosts` skips, a private or missing profile: free.
- **Plain HTTP, no browser.** Instagram's own logged-out answers: no login, no cookie, no account of yours, nothing to configure. On our test a profile's 200 newest posts and reels, every reel with its plays and the three pins flagged, came out in 45 to 70 seconds (21 requests; the time depends on the connection the run gets); the first row arrives with the profile's newest reel's plays.

### Who it's for

- **Marketers and brand teams** — a competitor's or a creator's newest posts and reels with likes, comments, plays and the paid-partnership flag, pinned posts told apart, on a schedule with only the new posts.
- **Influencer and social analysts** — engagement per post against the owner's followers, collab posts and their coauthors, the music of reels, a date window per profile across many profiles in one run.
- **Researchers** — a profile's whole list of posts to its end with captions, hashtags, mentions, exact times, locations and Instagram's own topic tags, with a verdict per profile.
- **Developers and data teams** — typed rows keyed by the media id, one free summary row per profile, a run summary to check a pipeline against; post links in, the same columns out.

### Quick start

**A profile's 12 newest posts and reels**

```json
{
  "profiles": [
    "natgeo"
  ],
  "maxPostsPerProfile": 12
}
```

**Several profiles' reels with their plays, the last 30 days**

```json
{
  "profiles": [
    "natgeo",
    "nasa"
  ],
  "contentType": "reels",
  "maxPostsPerProfile": 50,
  "newerThan": "30 days"
}
```

**Post and reel links, with their latest comments**

```json
{
  "postUrls": [
    "https://www.instagram.com/p/Des_X6JFjbE/",
    "https://www.instagram.com/reel/Dewd1c3iRmI/"
  ]
}
```

**Every post of a profile, to the end of its list**

```json
{
  "profiles": [
    "https://www.instagram.com/jefferson_fisher/"
  ],
  "maxPostsPerProfile": 0
}
```

**Only the new posts since the last run (for a schedule)**

```json
{
  "profiles": [
    "natgeo"
  ],
  "maxPostsPerProfile": 20,
  "onlyNewPosts": true,
  "stateStoreName": "natgeo-daily"
}
```

### Output

One row per post or reel (and one free summary row per profile, one free row per input that gave no post):

| field | meaning |
|---|---|
| `record_type` | `post` (a post or a reel: charged when `status` is `ok`) or `profile` (a free summary row per profile: what was read, the verdict, or why the profile gave no post). A post link that gave no post has a free `post` row with its status. |
| `status` | A post: `ok`. A profile's summary row: `ok` (see `verdict`), `not_found` (no profile with that username: two exits agree), `restricted` (Instagram serves no record logged out: an age-gated brand or an otherwise restricted profile; two exits agree), `private`, `no_posts`, `duplicate` (the same profile as another input of the run, e.g. a username and its id: walked once), `failed`, `not_read` (maxItems, the charge limit or the time limit came first). A post link with no post: `not_found`, `restricted`, `duplicate` (delivered under another input of the run: the note says which), `filtered` (left out by a min filter), `failed`, `not_read`. A value that names nothing: `invalid_input`. |
| `input` | The value you gave that led to this row. |
| `input_type` | `profile`, `post` or `invalid`. |
| `profile` | The username of the profile input the row came from; on a post link's row, the post's owner. |
| `position` | 1-based place in the profile's list of posts, newest first by date, a pinned post at its own date; every post the walk looked at keeps its place (one left out by a filter, a date limit or `onlyNewPosts` too), so the delivered rows can skip numbers. Null on a post link's row. |
| `id` | Instagram's media id (the number behind the shortcode): one row and one charge per post a run, whatever inputs overlap. |
| `shortcode` | The code in the post's link (`DeuOR-CjKE9`). |
| `url` | The post on Instagram: `/reel/<code>/` for a reel, `/p/<code>/` for the rest. |
| `type` | `photo`, `carousel` (several photos or videos), `reel`, or `video` (a long video that is not a reel), from Instagram's own product type, never from the media type alone (a reel and a long video share it). |
| `product_type` | Instagram's own name for the kind of post: `feed`, `carousel_container`, `clips` (a reel) or `longform`. |
| `timestamp` | The exact publish time, UTC (ISO 8601). |
| `taken_at` | The same time as a Unix timestamp (seconds). |
| `caption` | The whole caption, emoji and line breaks kept. |
| `hashtags` | Every #hashtag of the caption, without the #, in order, once each. |
| `mentions` | Every @username the caption mentions, without the @, once each. |
| `caption_is_edited` | True when Instagram marks the caption as edited; read with the post itself (a post link's row, or `includeComments` on a profile's rows), else null. |
| `alt` | Instagram's accessibility caption of the post ("Photo by National Geographic on October 20, 2026. May be an image of …"); not on reels-only rows (the reels list carries none). |
| `likes` | The exact like count; null when the owner hides the likes (Instagram then serves 0, which is not the count: `likes_hidden` is true). |
| `likes_hidden` | True when the owner hid the post's like count. |
| `comments_count` | The comment count Instagram shows. |
| `comments_disabled` | True when the owner turned the comments off; null when the route that read the post does not say. |
| `plays` | A reel's plays, from the profile's reels list (the counter the app shows); null with the reason in `plays_status` elsewhere. |
| `plays_status` | `served`; `not_a_reel` (a photo, a carousel, a long video); `outside_walk` (an old pasted reel past the 5 pages of its owner's reels list the Actor reads for it); `not_listed` (the reels list passed its date without it); `refused` (the reels list refused two exits); `not_asked`. |
| `video_duration` | A video's or reel's length in seconds (the reels list's figure, else the video manifest's). |
| `has_audio` | False on a video Instagram marks as silent; null on a photo. |
| `engagement_rate` | (likes + comments) ÷ the owner's followers, four significant digits; null when the likes are hidden or the owner is not the profile read (a collab post another account owns). |
| `owner_username` | The post's owner. On a collab post another account owns, that account (the profile you gave is then in `coauthors` and in `profile`). |
| `owner_id` | The owner's numeric profile id. |
| `owner_full_name` | The owner's display name. |
| `owner_is_verified` | True when the owner has the verified badge. |
| `owner_profile_pic_url` | The owner's profile picture URL (signed; see `media_urls_expire_at`). |
| `owner_followers` | The owner's follower count, read once per profile at the start of the run; null on a collab post owned by another account. |
| `coauthors` | The collab post's other accounts: {username, is_verified}; the owner left out. |
| `coauthor_ids` | The coauthors' numeric profile ids, as the owner's list serves them. |
| `is_paid_partnership` | True on a post marked "Paid partnership"; read from the profile's own list (on a pasted photo link whose owner is not walked in the run: null; a pasted reel gets it from its owner's reels list). |
| `is_pinned` | True when the profile's owner pinned the post to the top of the profile (a coauthor's pin is not the owner's); on a reels-only row, pinned on the reels tab. Null on a pasted post whose owner is not walked in the run. |
| `tagged_users` | The accounts tagged in the post or any carousel item: {username, full_name, id}. |
| `location_name` | The place tagged on the post. |
| `location_id` | Instagram's id of the place. |
| `location_lat` | The place's latitude: on a post link's row, or with `includeComments` (the post itself carries the coordinates). |
| `location_lng` | The place's longitude, as `location_lat`. |
| `display_url` | The post's image at its largest size (a video's cover; a carousel's first item). |
| `images` | Every image of the post at its largest size: one for a photo, one per item for a carousel. |
| `carousel` | A carousel's items in order: {type (photo or video), shortcode, display_url, video_url, width, height, alt}; empty for a single photo or video. |
| `carousel_count` | How many items the carousel holds. |
| `video_url` | The video file at its largest size (a reel, a long video, a carousel's first video). |
| `audio_url` | The video's audio-only file from its manifest. |
| `media_urls_expire_at` | When the signed media URLs of the row stop working (Instagram's own expiry in each URL: 1 to 4 days after the read). Download what you need before. |
| `width` | The post's width in pixels. |
| `height` | The post's height in pixels. |
| `music` | A reel's sound: {title, artist, audio_id, uses_original_audio} (a licensed track, or the creator's original audio). |
| `shared_to_facebook` | True when the reel was also posted to Facebook. |
| `ai_label` | Instagram's own AI label of the post ("Likely made with AI"); null when it shows none. |
| `topics` | Instagram's own topic tags of the post (the related-topic pills of its page). |
| `latest_comments` | The post's latest comments: {id, text, timestamp, likes, owner_username, owner_id, owner_is_verified, gif_url}. On a post link's row up to 15 (with `includeComments` up to `maxComments`); on a profile's rows only with `includeComments`. |
| `verdict` | On a profile's row: `COMPLETE` (the profile's list ended: Instagram's own end flag), `CAPPED` (maxPostsPerProfile), `DATE_LIMIT` (the walk reached posts older than newerThan), `NO_NEW_POSTS` (onlyNewPosts: the walk reached the newest post an earlier run of the memory delivered), `RUN_CAP` (maxItems or the charge limit), `PARTIAL` (an error or the time limit stopped it; the note says which). |
| `posts_listed` | On a profile's row: the posts of its list the walk looked at. |
| `posts_delivered` | On a profile's row: the posts delivered from it. |
| `pinned_posts` | On a profile's row: the pinned posts among them. |
| `reels_with_plays` | On a profile's row: the delivered reels that carry their plays. |
| `note` | Why, in words: the reason of a free row, what a walk did not read and why. |
| `scraped_at` | When the row was read (UTC). |

Example record:

```json
{
  "record_type": "post",
  "status": "ok",
  "input": "natgeo",
  "input_type": "profile",
  "profile": "natgeo",
  "position": 24,
  "id": "4008597565992995700",
  "shortcode": "DehZj9wggt0",
  "url": "https://www.instagram.com/reel/DehZj9wggt0/",
  "type": "reel",
  "product_type": "clips",
  "timestamp": "2026-10-15T19:00:02Z",
  "taken_at": 1792090802,
  "caption": "Keeping up with a pack of hunting wolves is just another day at the office for @bertiegregory and his team.\n\n#WolvesUpClose with Bertie Gregory premieres Friday, November 6 at 8/7c on National Geographic. Streaming on @DisneyPlus and @hulu.",
  "hashtags": [
    "WolvesUpClose"
  ],
  "mentions": [
    "bertiegregory",
    "DisneyPlus",
    "hulu"
  ],
  "caption_is_edited": false,
  "alt": "Video by National Geographic on October 15, 2026. May be an image of wolf, screen, poster and text.",
  "likes": 52551,
  "likes_hidden": false,
  "comments_count": 394,
  "comments_disabled": false,
  "plays": 1365388,
  "plays_status": "served",
  "video_duration": 30.037,
  "has_audio": true,
  "engagement_rate": 0.0001973,
  "owner_username": "natgeo",
  "owner_id": "787132",
  "owner_full_name": "National Geographic",
  "owner_is_verified": true,
  "owner_profile_pic_url": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-19/683576066_18653628823019133_9051036240972105113_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby40MDAuYzIifQ&_nc_ht=scontent-lax3-2.cdninstagram.com&_nc_cat=1&_nc_oc=Q6cZ2gGIu2AAIfvqb_qru1YNeVSltqzzlfg3q7mRYg0BpTh-Hford4St2YUa751WTgvOuec&_nc_ohc=3hWKJm3WbW0Q7kNvwHlU5hP&_nc_gid=D2Uom52ccYS92qkQv1Uuvg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AQOVgoqmr4j4loX7a4unOepSshwHhvMpoY9141_eUbpEuA&oe=6ADE8DAB&_nc_sid=10d13b",
  "owner_followers": 268325315,
  "coauthors": [
    {
      "username": "bertiegregory",
      "is_verified": true
    },
    {
      "username": "natgeoanimals",
      "is_verified": true
    }
  ],
  "coauthor_ids": [
    "1284886006",
    "1029649300"
  ],
  "is_paid_partnership": false,
  "is_pinned": true,
  "tagged_users": [
    {
      "username": "natgeoanimals",
      "full_name": "National Geographic Animals",
      "id": "1029649300"
    },
    {
      "username": "bertiegregory",
      "full_name": "Bertie Gregory",
      "id": "1284886006"
    }
  ],
  "location_name": null,
  "location_id": null,
  "location_lat": null,
  "location_lng": null,
  "display_url": "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/849098148_18708313780019133_730867734770692515_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=NDAwODU5NzU2NTk5Mjk5NTcwMDE4NzA4MzEzNzc3MDE5MTMz.3-ccb7-5&ccb=7-5&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6IkNMSVBTLnhwaWRzLjEwODAuc2RyLnZpZGVvX2RlZmF1bHRfY292ZXJfZnJhbWUuQzMifQ%3D%3D&_nc_ohc=rG4S4sXViaUQ7kNvwE5V2uj&_nc_oc=AdrKEiKYpn0XcPIN4gYWhlTeHevXStfRhQns_6itwLUAAwTWqFNVB6b5aHNJe1vtTCo&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&_nc_gid=D2Uom52ccYS92qkQv1Uuvg&_nc_ss=7a20f&oh=00_AQNalScRgJgRgiZU6Ce2XepdSpcRdBJmYnRI2ABAU4JbDQ&oe=6ADEB64C",
  "images": [
    "https://scontent-lax3-2.cdninstagram.com/v/t51.82787-15/849098148_18708313780019133_730867734770692515_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=NDAwODU5NzU2NTk5Mjk5NTcwMDE4NzA4MzEzNzc3MDE5MTMz.3-ccb7-5&ccb=7-5&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6IkNMSVBTLnhwaWRzLjEwODAuc2RyLnZpZGVvX2RlZmF1bHRfY292ZXJfZnJhbWUuQzMifQ%3D%3D&_nc_ohc=rG4S4sXViaUQ7kNvwE5V2uj&_nc_oc=AdrKEiKYpn0XcPIN4gYWhlTeHevXStfRhQns_6itwLUAAwTWqFNVB6b5aHNJe1vtTCo&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent-lax3-2.cdninstagram.com&_nc_gid=D2Uom52ccYS92qkQv1Uuvg&_nc_ss=7a20f&oh=00_AQNalScRgJgRgiZU6Ce2XepdSpcRdBJmYnRI2ABAU4JbDQ&oe=6ADEB64C"
  ],
  "carousel": [],
  "carousel_count": null,
  "video_url": "https://scontent-lax7-1.cdninstagram.com/o1/v/t2/f2/m86/AQPmzWFyFbdQEUSTj3bdWcEdgJrx_FjybCJC5cI7WvE3uOZ2rh-VGb63KeYgGbUc5ovqd29n4IW7ZzvvAKPIhr9epNJVMFjcrAsy4GI.mp4?_nc_cat=101&_nc_sid=5e9851&_nc_ht=scontent-lax7-1.cdninstagram.com&_nc_ohc=QpkVpzHAn_oQ7kNvwEJowvT&efg=eyJ2ZW5jb2RlX3RhZyI6Inhwdl9wcm9ncmVzc2l2ZS5JTlNUQUdSQU0uQ0xJUFMuQzMuNzIwLmRhc2hfYmFzZWxpbmVfMV92MSIsInhwdl9hc3NldF9pZCI6MTg3MDgyNzIzMzIwMTkxMzMsImFzc2V0X2FnZV9kYXlzIjo1LCJ2aV91c2VjYXNlX2lkIjoxMDA5OSwiZHVyYXRpb25fcyI6MzAsInVybGdlbl9zb3VyY2UiOiJ3d3cifQ%3D%3D&ccb=17-1&vs=a8143723d0c4400&_nc_vs=HBksFQIYUmlnX3hwdl9yZWVsc19wZXJtYW5lbnRfc3JfcHJvZC83MzFCMTY4MzlGODE0MTEyOTMzQjkwMDQ2ODYwQUFEOV92aWRlb19kYXNoaW5pdC5tcDQVAALIARIAFQIYUWlnX3hwdl9wbGFjZW1lbnRfcGVybWFuZW50X3YyLzAzNDgzMzFGNTA0NTRFNkQ5RUM2NjREMTM4REUwOEYzX2F1ZGlvX2Rhc2hpbml0Lm1wNBUCAsgBEgAoABgAGwKIB3VzZV9vaWwBMRJwcm9ncmVzc2l2ZV9yZWNpcGUBMRUAACb61rDr4sS7QhUCKAJDMywXQD4HrhR64UgYEmRhc2hfYmFzZWxpbmVfMV92MREAdf4HZeadAQA&_nc_gid=D2Uom52ccYS92qkQv1Uuvg&_nc_zt=28&_nc_ss=7a20f&oh=00_AQMc0GQeLtuzAXYuR--d3DhkC81H2nuoWdcOq0u4IEQkMg&oe=6ADAA7E2",
  "audio_url": "https://scontent-lax3-1.cdninstagram.com/o1/v/t2/f2/m78/AQMigN4zZ5qE3ZMxWEAeFmpCJnjzPNAFgzv_oD9D8ewf9o51S1kqsqG2ArP2PChzrVzhbrs5BgnUl1fAlc7v3YLnlcMx92At3yGJPGk.mp4?_nc_cat=110&_nc_sid=9ca052&_nc_ht=scontent-lax3-1.cdninstagram.com&_nc_ohc=bnEHLAqnLKYQ7kNvwGa1xxS&efg=eyJ2ZW5jb2RlX3RhZyI6ImlnLXhwdmRzLmNsaXBzLmMyLUMzLmRhc2hfbG5faGVhYWNfdmJyM19hdWRpbyIsInZpZGVvX2lkIjpudWxsLCJvaWxfdXJsZ2VuX2FwcF9pZCI6OTM2NjE5NzQzMzkyNDU5LCJjbGllbnRfbmFtZSI6ImlnIiwieHB2X2Fzc2V0X2lkIjoxODcwODI3MjMzMjAxOTEzMywiYXNzZXRfYWdlX2RheXMiOjUsInZpX3VzZWNhc2VfaWQiOjEwMDk5LCJkdXJhdGlvbl9zIjozMCwiYml0cmF0ZSI6NzU5NDgsInVybGdlbl9zb3VyY2UiOiJ3d3cifQ%3D%3D&ccb=17-1&_nc_gid=D2Uom52ccYS92qkQv1Uuvg&_nc_ss=7a20f&_nc_zt=28&oh=00_AQNKCQKFwt4GBMwA67slQL0GTq92Fhx5a4Iu5N7xRU0tbA&oe=6ADA9D0F",
  "media_urls_expire_at": "2026-10-23T00:18:42Z",
  "width": 1080,
  "height": 1920,
  "music": {
    "title": "Original audio",
    "artist": "natgeo",
    "audio_id": "28762088093432459",
    "uses_original_audio": true
  },
  "shared_to_facebook": false,
  "ai_label": null,
  "topics": [
    "wolves up close",
    "bertie gregory",
    "national geographic",
    "hudson bay",
    "wolf hunting moose",
    "arctic nature"
  ],
  "latest_comments": null,
  "verdict": null,
  "posts_listed": null,
  "posts_delivered": null,
  "pinned_posts": null,
  "reels_with_plays": null,
  "note": null,
  "scraped_at": "2026-10-21T14:59:56Z"
}
```

### Pricing

**Pay for the posts and reels a run delivers**, plus one run start; no platform usage is billed to you. The post price falls with your Apify plan.

| event | unit | Free | Starter | Scale | Business |
|---|---|---|---|---|---|
| Post or reel (`post`) | per 1,000 | $2.70 | $2.295 | $1.89 | $1.35 |
| Run start (`start`) | per run | $0.0013 | $0.0013 | $0.0013 | $0.0013 |

A post or a reel is **$2.70 per 1,000** on the Free plan ($0.0027 a row) and $2.295 on Starter, whatever input gave it (a profile or a link) and whatever it carries: the latest comments of a link's row, and the comments `includeComments` adds (up to `maxComments`), are inside the row's price. Each run that delivers a post also pays the **run start, $0.0013 once**; a run that delivers none pays nothing at all. So a profile's 12 newest posts cost $0.0337 on the Free plan and $0.02884 on Starter; 25 reels $0.0688 and $0.058675; 1,000 posts $2.7013 and $2.2963; several profiles and links in one run share one start. Free: every profile's summary row, a private, missing or age-gated profile, a profile with no post, a deleted post, a value that names nothing, a post given twice (one row, one charge), a post a filter or `onlyNewPosts` leaves out, and a profile left unread at a cap or the time limit. A run that stops early (a timeout, an abort) is charged for the rows it delivered. `maxItems` counts the charged rows. Before the first request the log names the price of a row at your plan, the run start and, when a cap bounds the run (`maxPostsPerProfile`, the post links, `maxItems`), the most it can cost. With `maxPostsPerProfile` 0 (every post) and no `maxItems` a run has no bound but the profiles' own lists, and Instagram shows no post count logged out: a profile holds from none to many thousands (on our runs: a profile with no post, one with 4, a restaurant's whole list of 2,045 in 99 seconds). Set `maxPostsPerProfile`, `maxItems` or a charge limit for a lower maximum. Enterprise plans have their own rates — the Actor's Pricing tab shows the price for your plan.

### Usage patterns

- **What to give** — `profiles`: usernames (`natgeo`, `@natgeo`), profile links (`instagram.com/natgeo/`, parameters such as `?igsh=` dropped), a reels tab (`instagram.com/natgeo/reels/`: reels only) or numeric profile ids. `postUrls`: post and reel links in any form (`/p/`, `/reel/`, `/reels/`, `/tv/`, with the owner's username in the path, `?img_index=`) or bare shortcodes. A value repeated is read once; a value that names nothing (a story, a tagged tab, a hashtag or location page, another site) is a free `invalid_input` row that says why. A share link (`instagram.com/share/…`) is not read in this version: open it and paste the link it lands on.
- **How many posts** — `maxPostsPerProfile` (12 by default; 0 = all) takes each profile's newest posts by date; a pinned post counts at its own date, and a post a filter leaves out does not count. `contentType` picks posts and reels, posts only, or reels only. `newerThan` and `olderThan` (a date, a date and time, or `7 days`) set a window; the walk stops at the first unpinned post older than `newerThan`. `maxItems` caps the charged rows of the whole run.
- **Filters** — `minLikes`, `minComments` and `minPlays` keep a post only above a count (a hidden like count and a post without plays do not pass a minimum above 0); `skipPinned` leaves the pinned posts out. A post left out is free and frees its place in `maxPostsPerProfile`.
- **Comments** — A post link's row carries the post's latest comments (up to 15) at no extra call. Switch on `includeComments` to add them to a profile's rows too, up to `maxComments` a post (200 at most; past 15 they are read 14 a call), with the location's coordinates and the edited flag: it reads each post once more, so the run takes longer. Replies to comments are not shown to logged-out viewers.
- **Scheduled runs: only the new posts** — Save a task with `onlyNewPosts` (and a `stateStoreName` to name the memory) and schedule it: the walk stops at the newest post an earlier run of that memory delivered (the profile's verdict `NO_NEW_POSTS`), so each run delivers the posts that appeared since the last one and nothing older; a pinned post the memory knows is skipped free and does not stop the walk. The memory keeps the newest 20,000 post ids.

### Input configuration

| field | type | default | what it does |
|---|---|---|---|
| `profiles` | `array` | `[]` | Instagram profiles, one per line: a username (`natgeo`, `@natgeo`), a profile link (`https://www.instagram.com/natgeo/`, any `?igsh=` or other parameter is dropped), a profile's reels tab (`instagram.com/natgeo/reels/`: that profile's reels only) or a numeric profile id (`787132`). Each gives its newest `maxPostsPerProfile` posts and reels. A post link pasted here is read as a post. A private, missing or age-gated profile, or a value that names nothing, is a free row that says why. |
| `postUrls` | `array` | `[]` | Posts and reels, one per line, in any form: `https://www.instagram.com/p/DeuOR-CjKE9/`, a `/reel/`, `/reels/` or `/tv/` link, a link with the owner's username in it, a link with `?img_index=`, or a bare shortcode (`DeuOR-CjKE9`). Each gives one row with the post's latest comments. The same post given twice, or also in a profile's posts, is one row. A share link (`instagram.com/share/…`) is not read in this version: open it and paste the link it lands on. |
| `maxPostsPerProfile` | `integer` | `12` | How many of each profile's newest posts and reels, by date; 0 = every post to the end of the profile's list. A pinned post counts at its own date: an old pin never takes the place of a newer post. A post left out by a filter does not count. The profile's summary row says whether the list ended (COMPLETE) or this cap came first (CAPPED). |
| `contentType` | `all` / `posts` / `reels` | `"all"` | All posts and reels as the profile's grid lists them, posts only (photos, carousels and long videos, no reels), or reels only (the profile's reels tab: every reel with its plays, the tab's own pinned reels at their dates). A profile link that ends in `/reels/` reads reels only whatever this says. |
| `newerThan` | `string` | `""` | Only posts published on or after this date: `2026-09-01`, a date and time (`2026-09-01T12:00:00Z`), or a span back from now in lower case (`7 days`, `12 hours`, `2 weeks`, `3 months`). The walk stops at the first unpinned post older than this; a pinned post older than it is skipped. |
| `olderThan` | `string` | `""` | Only posts published on or before this date (a date includes its whole day), in the same forms as `newerThan`. Newer posts are skipped and do not count toward `maxPostsPerProfile`. |
| `skipPinned` | `boolean` | `false` | Leave out the posts the owner pinned to the top of the profile. Off = a pinned post is delivered at its own date, flagged `is_pinned`. |
| `minLikes` | `integer` | `0` | Keep a post only with at least this many likes (a post whose owner hides the likes is left out when this is above 0). A post left out is free and does not count toward `maxPostsPerProfile`. 0 = every post. |
| `minComments` | `integer` | `0` | Keep a post only with at least this many comments. Free when it leaves a post out. 0 = every post. |
| `minPlays` | `integer` | `0` | Keep a reel only with at least this many plays; a photo, a carousel or a reel whose plays are not read is left out when this is above 0. Free when it leaves a post out. 0 = every post. |
| `includeComments` | `boolean` | `false` | Add each post's latest comments (`latest_comments`: text, time, likes, author) to the rows of a profile, up to `maxComments` a post, with the location's coordinates and the edited flag. It reads each post once more, so a run is slower. A post link's row always carries the post's latest comments (up to 15); with this on, up to `maxComments`. |
| `maxComments` | `integer` | `15` | The latest comments read a post when `includeComments` is on: the first 15 come with the post, the rest 14 a call. Replies to comments are not shown to logged-out viewers. |
| `onlyNewPosts` | `boolean` | `false` | Deliver only the posts that appeared since an earlier run with the same memory: the walk stops at the newest post that run delivered (the profile's verdict NO_NEW_POSTS; a pinned post it knows is skipped free). For a scheduled task. |
| `stateStoreName` | `string` | `""` | The name of the memory `onlyNewPosts` keeps (letters, digits, '.', '\_', '-'); empty = a name derived from the inputs. |
| `maxItems` | `integer` | `0` | The most charged rows (posts and reels) of the whole run, over every profile and link; 0 = no cap beyond `maxPostsPerProfile` and your charge limit. |
| `proxyUrl` | `string` |  | Your own proxy (scheme://user:pass@host:port) for every call instead of ours. Leave empty: the Actor brings its own connections at no cost to you. |

### Errors

**A run does not fail because of your input.** When an input cannot be used — a date that is not a date, an id in the wrong form, two settings that contradict each other — the run ends **Succeeded**, charges nothing, and says what to change:

- the status message starts with `INPUT REJECTED`;
- the dataset holds one row, `{"error": true, "code": "…", "message": "…"}`, and no results;
- the key-value store record `ERROR` holds the same object.

From code, check `error` on the first row before reading results.

A private, missing or age-gated profile, a deleted post or a link that names nothing is not an error: it gets one free row with its `status`, and the run goes on with the others. A run that ends **Failed** is a fault on our side, never your input; it charges nothing, and we are alerted.

### FAQ

**Does it get every post of a profile?**

Every post the profile lists to logged-out viewers, newest first, to Instagram's own end of the list (`maxPostsPerProfile` 0), 50 posts a call: our source study walked a 1,066-post profile to its last post that way. The profile's summary row says how many posts were looked at and delivered and whether the list ended (COMPLETE) or a limit came first. A few posts a profile hides from its grid may not be listed (2 of 1,068 on that profile).

**Why are some likes null?**

The owner hid the like count on that post. Instagram then serves 0, which is not the count, so the row gives `likes` null and `likes_hidden` true. The comment count is still there.

**Where do the plays come from?**

From the profile's reels list, the play counter the app shows on each reel. A profile's reels get them as the walk reaches their date. A pasted reel link gets them from its owner's reels list, read up to 5 pages back (the newest 60 reels); an older pasted reel gets `plays` null with `plays_status` `outside_walk`. Instagram's web page shows a second, smaller view counter: it is not used as plays.

**What does pinned mean here?**

`is_pinned` is true on a post the profile's owner pinned to the top of the profile. Instagram's grid also marks a post that a coauthor pinned on their own profile; that is not counted as the owner's pin. A pinned post is delivered at its own date and counts toward `maxPostsPerProfile` only there. On a reels-only row, `is_pinned` is the reels tab's own pin.

**What are the free rows?**

One summary row per profile (`record_type` profile): `ok` with the verdict and the counts, or why it gave no post — `not_found` (two reads agree), `restricted` (age-gated or otherwise restricted logged out), `private`, `no_posts`, `duplicate` (the same profile as another input: walked once), `failed`, `not_read`. One row per post link that gave no post (`not_found`, `restricted`, `duplicate` when another input of the run delivered it, `filtered`) and per value that names nothing (`invalid_input`). Only posts are charged. The free rows share the dataset with the posts: for posts alone keep the rows whose `record_type` is `post` and `status` is `ok`.

**Does it need an Instagram account, cookies or a login?**

No. Everything is read from Instagram's public logged-out data with a plain HTTP client; nothing of yours is sent to Instagram. A private account's posts are not served logged out and come back as one free `private` row; a public collab post of another account that the private account's list may serve is not delivered (read it from that account's profile).

**Does it give shares, saves or reposts?**

No. Instagram does not serve the share, save and repost counts to logged-out viewers: the scrapers that sell them read them with a logged-in account. Each row has the likes, the comment count and, on a reel, the plays.

**How long do the image and video URLs work?**

Instagram signs every media URL with an expiry, 1 to 4 days after the read; each row gives that time in `media_urls_expire_at`. Download what you need before it.

**I move from another Instagram scraper: which column is which?**

The keys most Instagram post scrapers use map one to one: `shortCode` → `shortcode`, `type` → `type`, `productType` → `product_type`, `caption`, `hashtags`, `mentions` → the same, `timestamp` → `timestamp`, `likesCount` → `likes` (+ `likes_hidden`), `commentsCount` → `comments_count`, `videoPlayCount` → `plays`, `videoDuration` → `video_duration`, `ownerUsername` / `ownerId` / `ownerFullName` → `owner_username` / `owner_id` / `owner_full_name`, `coauthorProducers` → `coauthors`, `paidPartnership` → `is_paid_partnership`, `isPinned` → `is_pinned`, `taggedUsers` → `tagged_users`, `displayUrl` → `display_url`, `childPosts` / `images` → `carousel` / `images`, `videoUrl` → `video_url`, `audioUrl` → `audio_url`, `musicInfo` → `music`, `alt` → `alt`, `isCommentsDisabled` → `comments_disabled`, `latestComments` → `latest_comments`, `inputUrl` → `input`.

**Can it read hashtags, locations, stories or tagged posts?**

Not in this version. Stories and a profile's tagged posts are not served logged out; hashtag, keyword and location pages are not read yet. A link to one of them is a free `invalid_input` row that says so.

### Integration

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('hyperbach/instagram-posts-scraper').call({"profiles": ["natgeo"], "maxPostsPerProfile": 12});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

#### Python

```python
from apify_client import ApifyClient
client = ApifyClient('YOUR_TOKEN')
run = client.actor('hyperbach/instagram-posts-scraper').call(run_input={'profiles': ['natgeo'], 'maxPostsPerProfile': 12})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

#### CLI

```bash
apify call hyperbach/instagram-posts-scraper --input '{"profiles": ["natgeo"], "maxPostsPerProfile": 12}'
```

#### REST

```bash
curl -X POST "https://api.apify.com/v2/acts/hyperbach~instagram-posts-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H 'Content-Type: application/json' -d '{"profiles": ["natgeo"], "maxPostsPerProfile": 12}'
```

### Support

apify@hyperbach.com

*This page is generated from the Actor's schemas and a live sample — it cannot describe a field the Actor does not have.*

# Actor input Schema

## `profiles` (type: `array`):

Instagram profiles, one per line: a username (`natgeo`, `@natgeo`), a profile link (`https://www.instagram.com/natgeo/`, any `?igsh=` or other parameter is dropped), a profile's reels tab (`instagram.com/natgeo/reels/`: that profile's reels only) or a numeric profile id (`787132`). Each gives its newest `maxPostsPerProfile` posts and reels. A post link pasted here is read as a post. A private, missing or age-gated profile, or a value that names nothing, is a free row that says why.

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

Posts and reels, one per line, in any form: `https://www.instagram.com/p/DeuOR-CjKE9/`, a `/reel/`, `/reels/` or `/tv/` link, a link with the owner's username in it, a link with `?img_index=`, or a bare shortcode (`DeuOR-CjKE9`). Each gives one row with the post's latest comments. The same post given twice, or also in a profile's posts, is one row. A share link (`instagram.com/share/…`) is not read in this version: open it and paste the link it lands on.

## `maxPostsPerProfile` (type: `integer`):

How many of each profile's newest posts and reels, by date; 0 = every post to the end of the profile's list. A pinned post counts at its own date: an old pin never takes the place of a newer post. A post left out by a filter does not count. The profile's summary row says whether the list ended (COMPLETE) or this cap came first (CAPPED).

## `contentType` (type: `string`):

All posts and reels as the profile's grid lists them, posts only (photos, carousels and long videos, no reels), or reels only (the profile's reels tab: every reel with its plays, the tab's own pinned reels at their dates). A profile link that ends in `/reels/` reads reels only whatever this says.

## `newerThan` (type: `string`):

Only posts published on or after this date: `2026-09-01`, a date and time (`2026-09-01T12:00:00Z`), or a span back from now in lower case (`7 days`, `12 hours`, `2 weeks`, `3 months`). The walk stops at the first unpinned post older than this; a pinned post older than it is skipped.

## `olderThan` (type: `string`):

Only posts published on or before this date (a date includes its whole day), in the same forms as `newerThan`. Newer posts are skipped and do not count toward `maxPostsPerProfile`.

## `skipPinned` (type: `boolean`):

Leave out the posts the owner pinned to the top of the profile. Off = a pinned post is delivered at its own date, flagged `is_pinned`.

## `minLikes` (type: `integer`):

Keep a post only with at least this many likes (a post whose owner hides the likes is left out when this is above 0). A post left out is free and does not count toward `maxPostsPerProfile`. 0 = every post.

## `minComments` (type: `integer`):

Keep a post only with at least this many comments. Free when it leaves a post out. 0 = every post.

## `minPlays` (type: `integer`):

Keep a reel only with at least this many plays; a photo, a carousel or a reel whose plays are not read is left out when this is above 0. Free when it leaves a post out. 0 = every post.

## `includeComments` (type: `boolean`):

Add each post's latest comments (`latest_comments`: text, time, likes, author) to the rows of a profile, up to `maxComments` a post, with the location's coordinates and the edited flag. It reads each post once more, so a run is slower. A post link's row always carries the post's latest comments (up to 15); with this on, up to `maxComments`.

## `maxComments` (type: `integer`):

The latest comments read a post when `includeComments` is on: the first 15 come with the post, the rest 14 a call. Replies to comments are not shown to logged-out viewers.

## `onlyNewPosts` (type: `boolean`):

Deliver only the posts that appeared since an earlier run with the same memory: the walk stops at the newest post that run delivered (the profile's verdict NO_NEW_POSTS; a pinned post it knows is skipped free). For a scheduled task.

## `stateStoreName` (type: `string`):

The name of the memory `onlyNewPosts` keeps (letters, digits, '.', '\_', '-'); empty = a name derived from the inputs.

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

The most charged rows (posts and reels) of the whole run, over every profile and link; 0 = no cap beyond `maxPostsPerProfile` and your charge limit.

## `proxyUrl` (type: `string`):

Your own proxy (scheme://user:pass@host:port) for every call instead of ours. Leave empty: the Actor brings its own connections at no cost to you.

## Actor input object example

```json
{
  "profiles": [
    "natgeo"
  ],
  "postUrls": [],
  "maxPostsPerProfile": 12,
  "contentType": "all",
  "newerThan": "",
  "olderThan": "",
  "skipPinned": false,
  "minLikes": 0,
  "minComments": 0,
  "minPlays": 0,
  "includeComments": false,
  "maxComments": 15,
  "onlyNewPosts": false,
  "stateStoreName": "",
  "maxItems": 0
}
```

# Actor output Schema

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

All scraped records in the default dataset. One row per post or reel (and one free summary row per profile, one free row per input that gave no post):

## `runSummary` (type: `string`):

How the run went: posts charged, free rows by status, each profile's walk (posts looked at and delivered, pins, reels with plays, the verdict), the source counters (requests by kind and their seconds, exits, rotations, slow answers, bytes), the proxy chains and the memory watch.

# 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 = {
    "profiles": [
        "natgeo"
    ],
    "maxPostsPerProfile": 12
};

// Run the Actor and wait for it to finish
const run = await client.actor("hyperbach/instagram-posts-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 = {
    "profiles": ["natgeo"],
    "maxPostsPerProfile": 12,
}

# Run the Actor and wait for it to finish
run = client.actor("hyperbach/instagram-posts-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 '{
  "profiles": [
    "natgeo"
  ],
  "maxPostsPerProfile": 12
}' |
apify call hyperbach/instagram-posts-scraper --silent --output-dataset

```

## MCP server setup

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