# Instagram Post Scraper (Sponsored Post Finder) (`scrapier/instagram-post-scraper`) Actor

Instagram Post Scraper extracts post details and identifies sponsored content, including captions, hashtags, engagement metrics, creator details, timestamps, media URLs, and sponsorship signals. Ideal for influencer research, brand monitoring, competitor analysis, and ad intelligence.

- **URL**: https://apify.com/scrapier/instagram-post-scraper.md
- **Developed by:** [Scrapier](https://apify.com/scrapier) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.99 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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 — Sponsored Posts, Brand Tags & Ad Cues

Instagram Post Scraper (Sponsored Post Finder) scrapes a public creator's posts, reels and carousels and attaches a sponsorship verdict to every row: Instagram's own official paid-partnership flag, kept strictly separate from a deterministic caption ad-cue check, resolved to a named brand with a confidence label, and rolled up into a same-scan repeat-deal count. Every response is structured JSON, ready to pass directly to an LLM, index into a vector store, or feed a brand-safety monitoring pipeline. Point it at any public handle, profile URL, or single post link and see exactly who is paying whom, and how they disclosed it.

***

### What is Instagram Post Scraper (Sponsored Post Finder)?

It is an Instagram post auditor built on top of a full post scraper: give it one or more public creator handles, profile URLs, or single post/reel/tv URLs, and it returns every post it finds — caption, hashtags, mentions, likes, comments, media, carousel children — plus a sponsorship layer most Instagram scrapers throw away. The brand's identity (handle, display name, verified badge, private/unpublished state, profile picture, numeric ID and follower count) already rides inside the same feed payload every post scraper downloads; this Actor reads it instead of discarding it, at no extra request cost.

Its defining behavior is that it never merges two different kinds of evidence into one boolean. `isPaidPartnership` is Instagram's own official label — a fact the platform publishes. `hasCaptionAdSignal` is a deterministic keyword/phrase match against the caption (`#ad`, `#sponsored`, "paid partnership", "use code", and more) — an inference from wording. A `sponsorshipEvidence` field records which of the four states applies (`officialAndCaption`, `officialOnly`, `captionOnly`, `none`), so a brand that gets tagged but never writes `#ad` — or a creator who writes `#ad` on a post Instagram never flagged — shows up as exactly that, instead of being averaged into one number.

> ⚠️ Sponsored-post rate is creator-driven, not constant. Across the accounts used to build this Actor, it ranged from **0% on 5 of 11 accounts to 38.3% on one** (per this Actor's own published input schema). A small scan budget combined with a sponsorship filter can legitimately return zero rows on a creator who simply hasn't posted an ad recently — raise `maxPostsToScan` before assuming the filter is broken.

No Instagram account, login, or session cookie is required — every request runs against Instagram's public, logged-out feed and post-page surface, using a CSRF token and app ID harvested from the target's own profile HTML.

- 🏷️ **Official-vs-caption sponsorship evidence** — `isPaidPartnership` and `hasCaptionAdSignal` reported as two independent fields plus a combined `sponsorshipEvidence` label
- 🎯 **Resolved brand identity** — handle, display name, verified flag, private/unpublished state, profile picture, numeric ID, profile URL and exact follower count for every Instagram-confirmed sponsor
- 🧭 **Deterministic brand inference** — when Instagram tags a paid post but won't name the brand, a tagged-user/caption-mention/coauthor ranker proposes a candidate, clearly separated from confirmed sponsorship
- 🔁 **In-run repeat-deal rollup** — how many times a resolved brand appears in the same scan, and the first/last timestamp of that brand's posts, computed with zero extra requests
- ⚖️ **Sponsored-vs-organic engagement index** — median likes on sponsored posts ÷ median likes on organic posts, computed from the scan window itself
- 🔎 **Filters that don't cost a full re-scan** — sponsorship-signal filter, brand allow-list, and minimum sponsor follower count, all applied before any row is charged

***

### What data can you get with Instagram Post Scraper (Sponsored Post Finder)?

A single run returns one flat JSON row per post, mixing the standard post record with the sponsorship-intelligence layer described above.

| Result Type | Extracted Fields | Primary Use Case |
| --- | --- | --- |
| Sponsorship evidence & brand identity | `isPaidPartnership`, `hasCaptionAdSignal`, `captionAdSignals`, `sponsorshipEvidence`, `sponsorshipDataAvailable`, `sponsorUsername`, `sponsorFullName`, `sponsorId`, `sponsorIsVerified`, `sponsorIsPrivate`, `sponsorIsUnpublished`, `sponsorProfilePicUrl`, `sponsorFollowerCount`, `sponsorProfileUrl`, `sponsorCount`, `sponsorTagPermission`, `sponsorTagIsPending`, `inferredSponsorUsername`, `inferredSponsorSource`, `inferredSponsorCandidates`, `resolvedBrandUsername`, `resolvedBrandConfidence` | FTC disclosure audits, brand-deal tracking, ad-intelligence monitoring |
| Repeat-deal rollup & engagement | `sponsorDealCountInRun`, `isRepeatSponsor`, `sponsorFirstPostTimestamp`, `sponsorLastPostTimestamp`, `sponsoredEngagementIndex` | Spotting a brand's ongoing relationship with a creator inside one scan |
| Full post record | `inputUrl`, `id`, `type`, `shortCode`, `caption`, `hashtags`, `mentions`, `url`, `commentsCount`, `firstComment`, `latestComments`, `dimensionsHeight`, `dimensionsWidth`, `displayUrl`, `images`, `likesCount`, `timestamp`, `childPosts`, `ownerFullName`, `ownerUsername`, `ownerId`, `isCommentsDisabled`, `alt`, `productType`, `taggedUsers`, `coauthorProducers`, `isPinned`, `scanIndexInProfile` | Content research, engagement analysis, UGC discovery |
| Video-specific fields | `videoUrl`, `videoViewCount`, `videoPlayCount`, `videoDuration`, `musicInfo` | Reel/video performance analysis |

#### 🎯 Official flag vs. caption cue, resolved to one brand

This is the layer competing Instagram post scrapers don't expose. Instead of a single `isSponsored` boolean, every row carries Instagram's own paid-partnership tag and a caption-derived ad signal side by side:

```json
{
  "isPaidPartnership": true,
  "sponsorUsername": "ridge",
  "sponsorFollowerCount": 1450000,
  "hasCaptionAdSignal": true,
  "captionAdSignals": ["#ad"],
  "sponsorshipEvidence": "officialAndCaption",
  "resolvedBrandUsername": "ridge",
  "resolvedBrandConfidence": "confirmed",
  "sponsorDealCountInRun": 2,
  "isRepeatSponsor": true
}
```

`resolvedBrandUsername` prefers the Instagram-confirmed `sponsorUsername`, falling back to `inferredSponsorUsername` only when Instagram tagged the post as paid but didn't name a brand — `resolvedBrandConfidence` tells you which one you got (`"confirmed"` or `"inferred"`). A caption-only ad cue with no official tag (`sponsorshipEvidence: "captionOnly"`) is flagged via `hasCaptionAdSignal` but **never** resolves to a brand — inference only runs on officially-tagged posts, because a caption `@mention` alone is not proof of a paid relationship. That distinction is deliberate: it keeps "Instagram confirmed this is paid" and "this caption reads like an ad" from ever being silently merged.

#### 📸 Full post record

Every field a plain post scraper returns ships unchanged: caption with `hashtags`/`mentions` already parsed out, engagement (`likesCount`, `commentsCount`), media (`displayUrl`, `images`, carousel `childPosts`, video fields for Reels/videos), and owner identity (`ownerUsername`, `ownerFullName`, `ownerId`). With `dataDetailLevel` set to `detailedData` (the default), each returned row also carries `latestComments`, `firstComment`, `alt` text and `musicInfo` — fetched with one extra request per **returned** row, not per post scanned.

***

### Why not build this yourself?

Instagram has no public, official API for auditing another creator's sponsorship activity. Meta's Instagram Graph API and Basic Display API only expose data for accounts you own and connect through a Business/Creator OAuth flow — neither can tell you whether `@some-other-creator`'s last post was a paid partnership, or who paid for it. There is no "list every post where account X was tagged as a sponsor" endpoint at all.

Building this yourself means solving several non-obvious problems this Actor already handles:

- Instagram's private feed endpoint (`/api/v1/feed/user/{username}/username/?count=12`) requires an `x-csrftoken` and `x-ig-app-id` header pulled from the target's own profile HTML — there is no token-issuing endpoint to call instead.
- The feed is not perfectly chronological: 7 out-of-order posts were measured in 200 items, so a naive "stop at the first post older than my cutoff" truncates real results. This Actor uses a tolerance window instead of a hard stop.
- Sponsor identity is not where it looks like it should be. `sponsor_tags[0].sponsor_id` and `.username` are `null` on every live tag observed — the brand's actual handle, name, verification and follower count only exist inside the nested `sponsor_tags[0].sponsor` object.
- Instagram names the sponsoring brand on only about half of officially-tagged paid posts. Surfacing a candidate for the rest requires a deterministic, multi-source ranker (tagged user, then caption `@mention`, then coauthor) built and tuned from scratch.
- A single post/reel/tv URL's page never carries the official paid-partnership flag at all — a scraper that doesn't know this will silently report `false` instead of "unknown."

***

### What is the difference between a caption ad cue and an official paid-partnership tag?

A caption ad cue is text a creator chose to write — a hashtag like `#ad` or a phrase like "in partnership with." An official paid-partnership tag is a flag Instagram's own systems attach to the post because the creator used Instagram's branded-content tool to disclose the deal. They are not the same signal, and they disagree more often than most people assume: per this Actor's own published measurements, across 636 posts checked, 28 officially-tagged posts carried no caption cue at all, 18 caption-flagged posts had no official tag, and only 14 posts carried both.

The distinction matters for anyone doing disclosure or compliance work, because a single merged boolean hides exactly the cases worth finding: a brand relationship Instagram confirms but the creator never disclosed in writing, or a creator using ad language on a post Instagram never tagged as paid. Instagram Post Scraper (Sponsored Post Finder) returns both as separate JSON fields — `isPaidPartnership` for the official tag, `hasCaptionAdSignal`/`captionAdSignals` for the caption cue — plus the combined `sponsorshipEvidence` label (`officialAndCaption`, `officialOnly`, `captionOnly`, `none`) so you can query for either signal, both, or their disagreement, without recomputing it from raw text yourself.

***

### How to scrape Instagram with Instagram Post Scraper (Sponsored Post Finder)?

1. Open **Instagram Post Scraper (Sponsored Post Finder)** on its Apify Store listing and click **Try for free** (or **Start**, if it's already in your account).
2. Add one or more creators to **🎯 Creators to audit** (`username`) — a plain handle, a profile URL, or a single post/reel/tv URL.
3. Choose your sponsorship filter and budgets: **🏷️ Which posts should come back** (`sponsorshipFilter`), **📦 Rows returned per creator** (`resultsLimit`), and **🔭 Posts to scan through per creator** (`maxPostsToScan`).
4. Click **Start** and let the run finish — the log reports how many posts were officially flagged, caption-flagged, and brand-named in each creator's scan.
5. Download results as JSON, CSV, Excel or XML from the **Storage** tab, or pull them programmatically through the Apify API or `apify_client`.

```json
{
  "username": ["mkbhd", "https://www.instagram.com/natgeo/"],
  "sponsorshipFilter": "anySponsorshipSignal",
  "resultsLimit": 20,
  "maxPostsToScan": 500
}
```

#### How to run multiple creators in one job

`username` accepts an array, so one run can audit any mix of handles, profile URLs, and single post/reel/tv URLs — each is deduplicated (case-insensitive for profiles, by shortcode for posts) and processed one after another, with its own scan depth, row limit, and filters applied independently. A creator that fails (private profile, empty feed, or an unreachable page) does not stop the run — it's logged as a failure and the remaining creators are still scanned. There is no documented per-creator concurrency setting; targets run sequentially within a single job.

***

### ⬇️ Input

Twelve input fields; only `username` is required.

| Parameter | Required | Type | Description | Example Value |
| --- | --- | --- | --- | --- |
| `username` | Yes | array | One entry per creator: a handle (`mkbhd`), a profile URL (`https://www.instagram.com/mkbhd/`), or a single post/reel/tv URL (`https://www.instagram.com/p/SHORTCODE/`). Post URLs return exactly one row each and are exempt from the scan budget — Instagram's post page carries no official paid-partnership flag, so those rows always come back with `sponsorshipDataAvailable: false`. | `["mkbhd", "https://www.instagram.com/natgeo/"]` |
| `sponsorshipFilter` | No | string (enum) | Which posts come back. Default `"allPosts"` (no filter — identical to a plain post scraper). Options: `"allPosts"`, `"officiallySponsored"` (Instagram's own tag only), `"captionSignalled"` (caption cue only), `"anySponsorshipSignal"` (either signal). | `"anySponsorshipSignal"` |
| `sponsorUsernames` | No | array | Optional brand allow-list, matched against the resolved brand handle (confirmed sponsor first, inferred candidate second). Default `[]` (every brand kept). | `["ridge", "bestbuy"]` |
| `minSponsorFollowerCount` | No | integer (min `0`) | Drops posts whose **confirmed** sponsor has fewer followers than this. Only Instagram-named sponsors carry a follower count, so any value above `0` also excludes inferred-only rows. Default `0` (off). | `1000000` |
| `includeInferredSponsors` | No | boolean | When Instagram tags a post as paid but doesn't name the brand, a deterministic ranker (tagged user > caption `@mention` > coauthor) proposes a candidate into `inferredSponsorUsername`/`inferredSponsorSource`/`inferredSponsorCandidates`. Never writes to `sponsorUsername` or `isPaidPartnership`. Default `true`. | `false` |
| `resultsLimit` | No | integer (min `1`, max `100000`) | Rows returned **and billed** per creator. Example: `25` returns at most 25 rows even if 300 posts were scanned to find them. Default `10`. | `25` |
| `maxPostsToScan` | No | integer (min `1`, max `5000`) | How deep to scan per creator while filtering. Only consulted when `sponsorshipFilter`, `sponsorUsernames`, or `minSponsorFollowerCount` is active — with the default "All posts" filter, the scan stops as soon as `resultsLimit` rows are collected. Default `300` (≈ 25 feed requests, at 12 posts per page). | `1000` |
| `onlyPostsNewerThan` | No | string (date picker, nullable) | UTC cutoff. Absolute (`YYYY-MM-DD` or a full ISO timestamp) or relative (`"3 days"`, `"2 weeks"`, `"6 months"`, `"1 year"`). An unparseable value fails the run instead of silently disabling the filter. Default `null` (no cutoff). | `"6 months"` |
| `skipPinnedPosts` | No | boolean | Skip posts pinned to the creator's own grid. Pin status is still emitted as `isPinned` either way. Default `false`. | `true` |
| `dataDetailLevel` | No | string (enum) | `"detailedData"` (default) adds alt text, latest comments, music info and video play count via one extra request per **returned** row. `"basicData"` skips that request. Every sponsorship column is available on both levels at no extra cost. | `"basicData"` |
| `computeSponsoredEngagementIndex` | No | boolean | Adds `sponsoredEngagementIndex` = median likes on sponsored posts ÷ median likes on organic posts, computed only from this run's scan window. Stays `null` unless the scan contains at least 5 organic and 1 sponsored post. Default `true`. | `false` |
| `proxyConfiguration` | No | object | Apify Proxy configuration. Residential is the default and most reliable at volume; datacenter is a valid, cheaper choice since Instagram's post feed answers on both in testing. Optional — the Actor also runs without a proxy. Default `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}`. | `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}` |

#### Example JSON input

```json
{
  "username": [
    "mkbhd",
    "https://www.instagram.com/natgeo/",
    "https://www.instagram.com/p/C9xY2AbCdEf/"
  ],
  "sponsorshipFilter": "anySponsorshipSignal",
  "sponsorUsernames": [],
  "minSponsorFollowerCount": 0,
  "includeInferredSponsors": true,
  "resultsLimit": 20,
  "maxPostsToScan": 500,
  "onlyPostsNewerThan": "6 months",
  "skipPinnedPosts": false,
  "dataDetailLevel": "detailedData",
  "computeSponsoredEngagementIndex": true,
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

**Common pitfall:** raising `maxPostsToScan` alone does nothing if `sponsorshipFilter` is left at its default `"allPosts"` and no `sponsorUsernames`/`minSponsorFollowerCount` is set — with no active filter, the Actor stops scanning as soon as `resultsLimit` rows are collected, regardless of how high `maxPostsToScan` is. The scan budget only kicks in once a sponsorship filter, brand allow-list, or follower threshold is actually turned on.

***

### ⬆️ Output

Every post is a single flat JSON object pushed to the dataset, charged under the `row_result` event. Export as JSON, CSV, Excel or XML from the Apify Console's **Storage** tab, or read it programmatically via the Apify API or `apify_client`. The underlying row can carry up to 60 possible keys depending on media type and detail level; three published dataset views curate the most relevant subsets — **🏷️ Sponsorship overview** (default, 23 columns), **📸 Full post data** (31 columns), and **🤝 Brand deal ledger** (15 columns, sponsorship rows only) — but every field documented above exists on the row regardless of which view you're looking at.

#### Scraped results

```json
[
  {
    "inputUrl": "https://www.instagram.com/mkbhd/",
    "id": "3701234567890123",
    "type": "Image",
    "shortCode": "Cabc123XYZ",
    "caption": "This desk setup is unreal, thanks to @ridge for the wallet #ad",
    "hashtags": ["ad"],
    "mentions": ["ridge"],
    "url": "https://www.instagram.com/p/Cabc123XYZ/",
    "commentsCount": 812,
    "dimensionsHeight": 1350,
    "dimensionsWidth": 1080,
    "displayUrl": "https://scontent.cdninstagram.com/v/t51.../cover1.jpg",
    "images": ["https://scontent.cdninstagram.com/v/t51.../cover1.jpg"],
    "likesCount": 128340,
    "timestamp": "2026-06-02T14:05:11.000Z",
    "childPosts": [],
    "ownerFullName": "Marques Brownlee",
    "ownerUsername": "mkbhd",
    "ownerId": "163571619",
    "isCommentsDisabled": false,
    "firstComment": "This thing looks incredible, need one!",
    "latestComments": [
      {
        "id": "18070377008012681",
        "text": "This thing looks incredible, need one!",
        "ownerUsername": "some_fan",
        "ownerProfilePicUrl": "https://scontent.cdninstagram.com/v/t51.../fan.jpg",
        "timestamp": "2026-06-02T15:10:02.000Z",
        "repliesCount": 0,
        "replies": [],
        "likesCount": 4,
        "owner": { "id": "52290062017", "is_verified": false, "profile_pic_url": "https://scontent.cdninstagram.com/v/t51.../fan.jpg", "username": "some_fan" }
      }
    ],
    "alt": "Photo of a desk setup with a wallet on it",
    "isPaidPartnership": true,
    "sponsorUsername": "ridge",
    "sponsorFullName": "Ridge Wallet",
    "sponsorId": "179664998",
    "sponsorIsVerified": true,
    "sponsorIsPrivate": false,
    "sponsorIsUnpublished": false,
    "sponsorProfilePicUrl": "https://scontent.cdninstagram.com/v/t51.../ridge.jpg",
    "sponsorFollowerCount": 1450000,
    "sponsorProfileUrl": "https://www.instagram.com/ridge/",
    "sponsorCount": 1,
    "sponsorTagPermission": true,
    "sponsorTagIsPending": false,
    "captionAdSignals": ["#ad"],
    "hasCaptionAdSignal": true,
    "sponsorshipEvidence": "officialAndCaption",
    "sponsorshipDataAvailable": true,
    "inferredSponsorUsername": null,
    "inferredSponsorSource": null,
    "inferredSponsorCandidates": [],
    "resolvedBrandUsername": "ridge",
    "resolvedBrandConfidence": "confirmed",
    "sponsorDealCountInRun": 2,
    "isRepeatSponsor": true,
    "sponsorFirstPostTimestamp": "2026-04-11T10:00:00.000Z",
    "sponsorLastPostTimestamp": "2026-06-02T14:05:11.000Z",
    "sponsoredEngagementIndex": 1.42,
    "isPinned": false,
    "scanIndexInProfile": 4
  },
  {
    "inputUrl": "https://www.instagram.com/mkbhd/",
    "id": "3701234567890456",
    "type": "Video",
    "shortCode": "Cdef456UVW",
    "caption": "In partnership with @insta360 for this shoot",
    "hashtags": [],
    "mentions": ["insta360"],
    "url": "https://www.instagram.com/p/Cdef456UVW/",
    "commentsCount": 341,
    "dimensionsHeight": 1920,
    "dimensionsWidth": 1080,
    "displayUrl": "https://scontent.cdninstagram.com/v/t51.../cover2.jpg",
    "images": ["https://scontent.cdninstagram.com/v/t51.../cover2.jpg"],
    "likesCount": 54210,
    "timestamp": "2026-05-19T09:47:02.000Z",
    "childPosts": [],
    "ownerFullName": "Marques Brownlee",
    "ownerUsername": "mkbhd",
    "ownerId": "163571619",
    "isCommentsDisabled": false,
    "firstComment": "Camera quality on this is unreal",
    "latestComments": [],
    "alt": null,
    "videoUrl": "https://scontent.cdninstagram.com/v/t50.../reel2.mp4",
    "videoViewCount": 302100,
    "videoPlayCount": 302100,
    "videoDuration": 27.4,
    "productType": "clips",
    "isPaidPartnership": false,
    "sponsorUsername": null,
    "sponsorFullName": null,
    "sponsorId": null,
    "sponsorIsVerified": null,
    "sponsorIsPrivate": null,
    "sponsorIsUnpublished": null,
    "sponsorProfilePicUrl": null,
    "sponsorFollowerCount": null,
    "sponsorProfileUrl": null,
    "sponsorCount": 0,
    "sponsorTagPermission": null,
    "sponsorTagIsPending": null,
    "captionAdSignals": ["in partnership with"],
    "hasCaptionAdSignal": true,
    "sponsorshipEvidence": "captionOnly",
    "sponsorshipDataAvailable": true,
    "inferredSponsorUsername": null,
    "inferredSponsorSource": null,
    "inferredSponsorCandidates": [],
    "resolvedBrandUsername": null,
    "resolvedBrandConfidence": null,
    "sponsorDealCountInRun": null,
    "isRepeatSponsor": null,
    "sponsorFirstPostTimestamp": null,
    "sponsorLastPostTimestamp": null,
    "sponsoredEngagementIndex": 1.42,
    "isPinned": false,
    "scanIndexInProfile": 9
  },
  {
    "inputUrl": "https://www.instagram.com/p/C9xY2AbCdEf/",
    "id": "3660778310592222546",
    "type": "Image",
    "shortCode": "C9xY2AbCdEf",
    "caption": "Somewhere quiet, no caption cues here",
    "hashtags": [],
    "mentions": [],
    "url": "https://www.instagram.com/p/C9xY2AbCdEf/",
    "commentsCount": 230,
    "dimensionsHeight": 1350,
    "dimensionsWidth": 1080,
    "displayUrl": "https://scontent.cdninstagram.com/v/t51.../cover3.jpg",
    "images": ["https://scontent.cdninstagram.com/v/t51.../cover3.jpg"],
    "likesCount": 73473,
    "timestamp": "2026-06-22T19:00:10.000Z",
    "childPosts": [],
    "ownerFullName": "National Geographic",
    "ownerUsername": "natgeo",
    "ownerId": "787132",
    "isCommentsDisabled": false,
    "firstComment": "Amen.",
    "latestComments": [
      {
        "id": "18070377008012682",
        "text": "Amen.",
        "ownerUsername": "acostadorego",
        "ownerProfilePicUrl": "https://scontent.cdninstagram.com/v/t51.../fan2.jpg",
        "timestamp": "2026-06-22T20:15:00.000Z",
        "repliesCount": 0,
        "replies": [],
        "likesCount": 0,
        "owner": { "id": "52290062018", "is_verified": false, "profile_pic_url": "https://scontent.cdninstagram.com/v/t51.../fan2.jpg", "username": "acostadorego" }
      }
    ],
    "alt": "Photo by National Geographic",
    "isPaidPartnership": null,
    "sponsorUsername": null,
    "sponsorFullName": null,
    "sponsorId": null,
    "sponsorIsVerified": null,
    "sponsorIsPrivate": null,
    "sponsorIsUnpublished": null,
    "sponsorProfilePicUrl": null,
    "sponsorFollowerCount": null,
    "sponsorProfileUrl": null,
    "sponsorCount": null,
    "sponsorTagPermission": null,
    "sponsorTagIsPending": null,
    "captionAdSignals": [],
    "hasCaptionAdSignal": false,
    "sponsorshipEvidence": null,
    "sponsorshipDataAvailable": false,
    "inferredSponsorUsername": null,
    "inferredSponsorSource": null,
    "inferredSponsorCandidates": [],
    "resolvedBrandUsername": null,
    "resolvedBrandConfidence": null,
    "sponsorDealCountInRun": null,
    "isRepeatSponsor": null,
    "sponsorFirstPostTimestamp": null,
    "sponsorLastPostTimestamp": null,
    "sponsoredEngagementIndex": null,
    "isPinned": null,
    "scanIndexInProfile": null
  }
]
```

Note the third row: single post/reel/tv URLs always carry `isPaidPartnership: null` and `sponsorshipEvidence: null`, not `false` — Instagram's post page simply doesn't expose the official flag, and this Actor reports that as "unknown" rather than fabricating a negative. `sponsorshipDataAvailable: false` is the field to check before treating a `null` as a real answer.

***

### How can I use the data extracted with Instagram Post Scraper (Sponsored Post Finder)?

- 🛡️ **Brand safety and compliance teams:** filter `sponsorUsernames` down to your own brand's handle to see every post where you were tagged as a sponsor across any creator, then cross-check rows where `hasCaptionAdSignal` is `false` but `isPaidPartnership` is `true` for under-disclosed deals worth a compliance note.
- 🎤 **Influencer marketing and talent managers:** before signing a creator, scan their recent posts with no filter to see their true sponsorship rate, `resolvedBrandUsername` history, and `sponsorDealCountInRun` for existing brand relationships.
- 🤖 **AI engineers and LLM developers:** a grounded agent architecture — an agent issues a run against a creator list, receives structured `sponsorshipEvidence` and brand fields back, and passes them straight into a compliance-checking or brand-monitoring prompt.
- 📊 **Market and competitive researchers:** track which brands are buying placements across a roster of creators, and benchmark `sponsoredEngagementIndex` to see whether sponsored content underperforms organic posts for a given account.

***

### How do you monitor brand deals over time?

Sponsorship activity is not static — a creator with zero sponsored posts last month can pick up several new deals this month, and a brand can rotate its spend from one creator to another. Monitoring means re-running the same creator list on a schedule and comparing each run's output, rather than treating a single scan as the whole picture.

The Actor itself only rolls up deals **within** a single scan — `sponsorDealCountInRun`, `isRepeatSponsor`, `sponsorFirstPostTimestamp` and `sponsorLastPostTimestamp` describe what happened inside that run's scanned window, not across separate runs, since the Actor keeps no state between runs. The comparison across time therefore happens in your own pipeline: keep each run's `resolvedBrandUsername`, `sponsorshipEvidence` and `sponsoredEngagementIndex` values, then diff them against the previous run — a brand handle that appears in `resolvedBrandUsername` for the first time is a new deal, and a rising `sponsoredEngagementIndex` shows sponsored content starting to underperform (or outperform) a creator's organic baseline.

A practical loop: schedule a weekly run across your watch-list of creators with Apify's **Schedules** feature, store each run's rows in your own database or a second Apify dataset, and alert when a new `resolvedBrandUsername` shows up, when `sponsorDealCountInRun` for a known brand increases, or when a post's `isPaidPartnership` and `hasCaptionAdSignal` disagree on a creator you're actively monitoring for disclosure compliance.

***

### Integrate Instagram Post Scraper (Sponsored Post Finder) and automate your workflow

Instagram Post Scraper (Sponsored Post Finder) works with any language or tool that can send an HTTP request — it's an Apify Actor, reachable through the Apify API and every official Apify client library.

#### REST API with Python

```python
import requests

TOKEN = "<YOUR_APIFY_TOKEN>"
url = f"https://api.apify.com/v2/acts/instagram-post-scraper-sponsored-post-finder/run-sync-get-dataset-items?token={TOKEN}"

payload = {
    "username": ["mkbhd", "https://www.instagram.com/natgeo/"],
    "sponsorshipFilter": "anySponsorshipSignal",
    "resultsLimit": 20,
}

response = requests.post(url, json=payload)
rows = response.json()  # primary result array: one post row per item

for row in rows:
    print(row["ownerUsername"], row["resolvedBrandUsername"], row["sponsorshipEvidence"])
```

#### Scheduled monitoring and delivery

There is no dedicated webhook payload built into this Actor, but Apify's platform-level **Schedules** run it on a cron expression against a fixed creator list, and platform **webhooks** (Console → Integrations → Webhooks) can notify your endpoint whenever a scheduled run finishes so a downstream job can pull the fresh dataset. For a simpler setup, poll the Apify API for the latest run's dataset on your own interval instead.

***

### Is it legal to scrape Instagram posts and sponsorship data?

Yes — scraping publicly accessible data is generally lawful when it targets content any logged-out visitor can already see, which is what this Actor reads: no login, no session, no private profiles. In the United States, courts have held that accessing data available to any visitor without bypassing an access control does not violate the Computer Fraud and Abuse Act (*hiQ Labs, Inc. v. LinkedIn Corp.*, 9th Cir. 2019). Because the output includes personal and business data — creator usernames, sponsor brand handles, follower counts, captions — data-protection regimes such as GDPR (EU) and CCPA (California) can govern how you store, process and retain it, separately from whether the initial scrape was lawful, and Instagram's own Terms of Use separately restrict automated access as a contractual matter. Scraping for internal brand-safety monitoring generally carries a different risk profile than scraping to train a model on individuals' content. Consult your legal team for commercial use cases involving bulk storage of creator or sponsor personal data.

***

### Frequently asked questions

#### Which languages does the caption ad-cue vocabulary cover?

The built-in phrase and hashtag list is mostly English (`#ad`, `#sponsored`, `#paidpartnership`, "paid partnership", "sponsored by", "use code", "promo code", and more), plus a handful of non-English markers: German (`werbung`, `anzeige`), Spanish (`publicidad`). There's no `extraAdKeywords`-style input to extend the vocabulary yourself in this Actor — the list is fixed in the Actor's code.

#### What's the difference between `resultsLimit` and `maxPostsToScan`?

`resultsLimit` is how many rows you get back **and pay for** per creator (default 10). `maxPostsToScan` is how deep the Actor looks while filtering (default 300), and it only matters once a sponsorship filter, brand allow-list, or follower threshold is active — with no filter on, the Actor stops as soon as `resultsLimit` rows are collected, no matter how high `maxPostsToScan` is set.

#### How does Instagram Post Scraper (Sponsored Post Finder) handle Instagram's anti-bot measures?

Every feed and post-page request retries up to 3 times with an increasing delay, rotating to a fresh Apify Proxy session on each retry (except the last). Requests use browser-realistic headers, and the feed connection is capped at 10 concurrent connections (5 per host) to avoid overwhelming a single proxy exit. A CSRF token and numeric app ID are harvested from the target profile's own HTML before the first feed request, since Instagram's feed endpoint rejects requests without them.

#### Does Instagram Post Scraper (Sponsored Post Finder) extract sponsored posts and named brand deals?

Yes — every post row carries `isPaidPartnership` (Instagram's official label), the full sponsor object (`sponsorUsername`, `sponsorFullName`, `sponsorFollowerCount`, and more) when Instagram names a brand, `hasCaptionAdSignal`/`captionAdSignals` for caption-based cues, and `resolvedBrandUsername`/`resolvedBrandConfidence` for the brand the Actor is confident about. `resolvedBrandUsername` is `null` whenever a post is only caption-flagged with no official tag — caption cues alone are never enough to name a brand.

#### How many results does it return per creator?

Up to `resultsLimit` rows per creator (default 10, schema maximum 100000), drawn from up to `maxPostsToScan` posts inspected (default 300, schema maximum 5000). A single post/reel/tv URL always returns exactly one row and doesn't count against either budget.

#### Am I charged for posts that get filtered out or fail?

No. Per this Actor's own published pricing configuration, the `row_result` charge ($0.002) fires only for a row that is actually saved to the dataset — posts scanned but excluded by `sponsorshipFilter`, `sponsorUsernames`, or `minSponsorFollowerCount`, and posts that fail to load, are never pushed to the dataset and never charged.

#### How do I monitor a creator's or brand's sponsorship activity over time?

Schedule a recurring run across the same creator list with Apify's Schedules feature, and diff `resolvedBrandUsername`, `sponsorDealCountInRun` and `sponsoredEngagementIndex` against your own stored copy of the previous run's rows — see "How do you monitor brand deals over time?" above for the full loop. The Actor itself keeps no history between runs.

#### Does it work with Claude, ChatGPT, and AI agent frameworks?

It's callable as an HTTP endpoint through the Apify API by any agent framework that can issue a request and read JSON back — start a run, poll or await its dataset, and pass `sponsorshipEvidence` and the resolved-brand fields to your model as grounded, current context. No dedicated MCP server is documented for this Actor.

#### How does Instagram Post Scraper (Sponsored Post Finder) compare to other Instagram scrapers?

As observed on their own Apify Store listings on 2026-07-30: scrapemint's Instagram Influencer Analyzer & Sponsored Post Tracker documents a single `isSponsored` boolean plus a `sponsorSignals` array that flags `#ad`/`#sponsored`/partnership labels together, without separating Instagram's official tag from a caption cue, and without a brand-follower-count filter or a repeat-deal rollup. Apify's own Instagram Post Scraper lists "Is it a sponsored post?" and "Is it a paid partnership?" among its extractable fields, but its published sample output and field table don't show a sponsor-identity object, caption ad-cue detection, or brand inference. Apify's Export Instagram Comments and Posts Tool exposes only a plain `isSponsored` boolean in its sample output, with no sponsor identity or caption-signal detection at all. This Actor is the only one of the three, as observed on that date, that reports the official flag and caption cue as separate fields and resolves them to a confidence-labelled brand handle.

#### Can I use it without managing proxies or an Instagram login?

Yes. No Instagram username, password, or session cookie is ever requested — the Actor authenticates each request with a CSRF token and app ID it harvests itself from the target's public profile page. A residential Apify Proxy group is pre-filled by default and recommended for reliability at volume; you can swap in a different proxy group or turn proxying off entirely to run direct.

#### What happens if a profile is private, or a target returns no posts?

A private profile, an empty feed, and a rejected URL form (Instagram tab paths like `/stories/`, `/explore/`, or `/reels/` without a shortcode aren't treated as usernames) are each logged with their specific reason and that target is skipped — the run continues with the remaining creators. The whole run only fails if every target produces zero scanned posts; if some posts were scanned but none matched your sponsorship filter, the run finishes normally with a status message suggesting you raise `maxPostsToScan`.

***

### 💬 Your feedback

Found a bug, or need a sponsorship field this Actor doesn't expose yet? Open an issue on the Actor's **Issues** tab in Apify Console, or leave a review on the Store listing — it goes straight to the people maintaining it.

# Actor input Schema

## `username` (type: `array`):

One entry per creator: a handle (mkbhd), a profile URL (https://www.instagram.com/mkbhd/), or a single post / reel / tv URL (https://www.instagram.com/p/SHORTCODE/). Post URLs return one row each and are exempt from the scan budget - note that Instagram's post page does not carry the official paid-partnership flag, so those rows come back with sponsorshipDataAvailable=false.

## `sponsorshipFilter` (type: `string`):

Instagram's official flag and caption cues disagree far more than people expect: over 636 measured posts, 28 officially-paid posts carried no caption cue and 18 caption-flagged posts were not officially paid (only 14 agreed). A single boolean cannot express that, so pick the population you want. Default 'All posts' keeps this actor's behaviour identical to a plain post scraper.

## `sponsorUsernames` (type: `array`):

Optional brand allow-list, e.g. ridge, djiglobal, gmc. Matched against the resolved brand handle (Instagram-confirmed first, inferred candidate second). Leave empty to keep every brand. Example: \['ridge','bestbuy'].

## `minSponsorFollowerCount` (type: `integer`):

Drop posts whose CONFIRMED sponsor has fewer followers than this. Only rows where Instagram itself names the brand carry a follower count, so any value above 0 also drops inferred-brand rows. Example: 1000000 keeps only big-brand deals. Default 0 = off.

## `includeInferredSponsors` (type: `boolean`):

On officially-paid posts Instagram names the brand only about half the time. When it does not, a deterministic ranker (tagged user > caption @mention > coauthor - no AI, no LLM) proposes a candidate into inferredSponsorUsername / inferredSponsorSource / inferredSponsorCandidates. It NEVER writes to sponsorUsername and never changes isPaidPartnership. Default on.

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

How many post rows land in the dataset per creator - this is the BILLED number. Example: 25 returns at most 25 rows even if 300 posts were scanned to find them. Default 10.

## `maxPostsToScan` (type: `integer`):

How deep into the grid to look while filtering. Only used when a sponsorship filter, brand allow-list or follower threshold is active - with 'All posts' the actor stops as soon as the row budget is full. Example: rows 20 + scan 300 checks 300 posts and returns the first 20 sponsored ones. Default 300 (= 25 feed requests).

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

Only keep posts newer than this (UTC). Absolute tab takes YYYY-MM-DD or a full ISO timestamp; Relative tab takes '3 days', '2 weeks', '6 months', '1 year' - minutes, hours and weeks all parse. An unparseable value fails the run loudly instead of silently disabling the filter. Leave empty for no cutoff.

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

Skip the posts a creator has pinned to the top of their profile. Pin status is also emitted as the isPinned column either way. Default off.

## `dataDetailLevel` (type: `string`):

Every sponsorship column is available on BOTH levels - brand identity rides in the feed payload at zero extra cost. Detailed adds alt text, the latest comments, music info and video play count by fetching one extra page per returned row (measured: 2.8s vs 6.2s per 12 posts). Billing is one row event per row on both levels.

## `computeSponsoredEngagementIndex` (type: `boolean`):

Adds sponsoredEngagementIndex = median likes on sponsored posts / median likes on organic posts, COMPUTED FROM THIS RUN's scan window only - it is not a value scraped from Instagram. Stays null unless the scan contains at least 5 organic and 1 sponsored post. Default on.

## `proxyConfiguration` (type: `object`):

Instagram's post feed answers on both residential and datacenter exits in testing, so datacenter is a valid cheaper choice here. Residential is the default because it is the most reliable at volume.

## Actor input object example

```json
{
  "username": [
    "https://www.instagram.com/mkbhd/"
  ],
  "sponsorshipFilter": "allPosts",
  "sponsorUsernames": [],
  "minSponsorFollowerCount": 0,
  "includeInferredSponsors": true,
  "resultsLimit": 10,
  "maxPostsToScan": 300,
  "onlyPostsNewerThan": null,
  "skipPinnedPosts": false,
  "dataDetailLevel": "detailedData",
  "computeSponsoredEngagementIndex": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

One row per post. Instagram's official paid-partnership flag and named brand are kept strictly apart from caption-derived and inferred signals. Columns here are present on every row. This is the default dataset view.

## `fullPostData` (type: `string`):

The complete post record — caption, hashtags, mentions, media/dimensions, tagged users, coauthors, and (on Detailed payload depth) alt text, comments, music info and video stats.

## `brandDealLedger` (type: `string`):

Sponsorship-only slice — resolved brand, confidence, follower count, tag permission and repeat-deal counters — for pasting into a brand-deal tracker.

# 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 = {
    "username": [
        "https://www.instagram.com/mkbhd/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapier/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 = { "username": ["https://www.instagram.com/mkbhd/"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapier/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 '{
  "username": [
    "https://www.instagram.com/mkbhd/"
  ]
}' |
apify call scrapier/instagram-post-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapier/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/3MvS4cVND1c7ItxNL/builds/hsSTu3unZgdgi9R5j/openapi.json
