Guide · Python

Get Instagram post and reel stats by URL with Python

Put post and reel links in a CSV. The script returns likes, comments, reel views when Instagram reports them, who published each post and with whom, and whether it carries the paid partnership label.

ScrapingBot 10 min read
Endpoints
/media/by_shortcode, /reels/by_user_id
Cost
5 credits per call
Language
Python 3.9+
Needs
requests
On this page

A campaign report starts as a list of links: the reel a creator posted for you, the carousel a partner published, a competitor's launch post. What you need next to each link is short: likes, comments, views, who posted it and when. Instagram shows all of it on the post's page, one page at a time.

This guide reads a list of post links and fills in those numbers with Python. Views take a second call, hidden like counts need care, and the account that published a collab is not always the one you expect; each gets its own section. Every response and output below is real, collected through ScrapingBot on November 10, 2026, from coffee posts by James Hoffmann, Lance Hedrick, Blue Bottle Coffee and others.

Before you start

  • A ScrapingBot API key. Create a free account for 100 credits: 20 post lookups. No card needed.
  • Python 3.9 or newer and pip install requests.
  • The links of the posts and reels to report on, one per row in a CSV with a url column.

Meta's API has no lookup by post link

Meta's Business Discovery reads another account's posts with like_count, comments_count and, for reels, view_count. It takes a username, not a link, and its media reference says it "can not be used to get data for media owned by personal Instagram accounts". The oEmbed endpoint does take a post link, and returns embed HTML with no counts. Insights are for your own posts (all checked November 2026).

When every creator in a campaign has a business or creator account and you know their usernames, Business Discovery is free and official: list each creator's recent media and match your links on permalink. For a personal account, or a pile of links whose owners you haven't looked up, the public page is what there is. /media/by_shortcode returns what that page shows a visitor, as JSON.

One post

Every Instagram call is a POST to https://scrapingbot.io/api/v1/instagram. The shortcode is the part of the link after /p/ or /reel/:

curl -X POST https://scrapingbot.io/api/v1/instagram \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endpoint": "/media/by_shortcode", "params": {"shortcode": "DdZOfSWiKJb"}}'

James Hoffmann's September 17 reel from a Lima coffee bar, trimmed to the fields a report needs:

{
  "success": true,
  "data": {
    "id": "3988282667673625179",
    "code": "DdZOfSWiKJb",
    "media_type": 2,
    "product_type": "clips",
    "taken_at": 1789660448,
    "like_count": 10457,
    "comment_count": 255,
    "view_count": null,
    "like_and_view_counts_disabled": false,
    "is_paid_partnership": false,
    "sponsor_tags": null,
    "coauthor_producers": [],
    "caption": { "text": "I’m in Peru chasing a truly original specialty coffee experience. So naturally, I’ve ended…", … },
    "owner": { "id": "971700", "username": "jimseven", "full_name": "James Hoffmann", "is_verified": true, "is_private": false, … },
    "video_versions": [ { "height": 1280, "type": 101, "url": "https://scontent-iad3-1.cdninstagram.com/…" }, … ],
    …
  },
  "duration": "0.82",
  "statusCode": 200,
  "creditsUsed": 5
}
FieldWhat it is
code, idThe shortcode from the link, and the numeric media ID. Key your records on the shortcode.
like_count, comment_countLikes and comments, exact. comment_count includes replies.
product_type, media_typeclips is a reel. Otherwise media_type 1 is a photo and 8 a carousel (carousel_container, with carousel_media_count slides).
taken_atWhen it was posted, as a Unix timestamp (UTC).
ownerThe account that published it: id, username, full_name, is_verified.
coauthor_producersThe other accounts on a collab post. Empty for most posts.
is_paid_partnership, sponsor_tagsInstagram's paid partnership label, and the brand when one is tagged.
like_and_view_counts_disabledtrue when the owner hides like counts. Then like_count is not usable; see below.
view_countnull on every reel we looked up. Views come from another call.

The full answer is larger: 17 to 100 KB in our lookups, with the caption, image and video links (signed, and they expire) and, for a carousel, every slide. A lookup took 0.9 to 2.2 seconds and costs 5 credits.

There is also /media/by_url, which takes the link itself. It is strict about the form of the link. We sent it eight:

Link, after instagram.comWhat /media/by_url answered
/reel/DdZOfSWiKJb/The reel
/p/DdZOfSWiKJb/The same reel: /p/ works for reels too
/jimseven/reel/DdZOfSWiKJb/?igsh=…The reel; the username and tracking parameters are ignored
/reel/DdZOfSWiKJb, sent without https://The reel
/p/DbLR0mzFuWj/?img_index=3The whole carousel, whichever slide the link points at
/reels/DdZOfSWiKJb/400 Invalid Instagram URL format.
/tv/DdZOfSWiKJb/400 Invalid Instagram URL format.
/jimseven/ (a profile)400 Invalid Instagram URL format.

The refusals are free, but /reels/ with an s is the address Instagram's own reels viewer shows, so it turns up in pasted lists. All of these forms carry the same shortcode. The script reads it out with one pattern and calls /media/by_shortcode, which then covers every one of them. Links that start instagram.com/share/ are different: the code in them is not a shortcode, so the script sets them aside and asks for the address the link opens.

Views come from the owner's reels list

The lookup has no views: view_count was null on all 11 reels we looked up, and no other field carried a play count. The number Instagram shows on a reel is in the owner's list of reels:

curl -X POST https://scrapingbot.io/api/v1/instagram \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endpoint": "/reels/by_user_id", "params": {"user_id": "971700", "page_size": 12}}'
{
  "success": true,
  "data": {
    "items": [
      { "media": { "code": "DdZOfSWiKJb", "taken_at": 1789660448, "play_count": 235093, "like_count": 10458, "comment_count": 256, … } },
      …
    ],
    "paging_info": { "max_id": "AQHTlrPNAjvg4NUGDj14WzBgBon7erloT217zpZr…", "more_available": true },
    "status": "ok"
  },
  "duration": "4.36",
  "statusCode": 200,
  "creditsUsed": 5
}

user_id is the owner.id from the lookup. We listed 374 reels from 32 accounts this way, and every one had a play_count. What to know before relying on it:

  • It is not guaranteed. In October this list often came back with the play counts empty: our profile and reels guide found them null on every NASA reel it read, and today NASA's twelve newest all had one. Instagram decides what it reports, so the script treats views as optional and writes "not reported" rather than a zero.
  • One call covers many reels. A page costs 5 credits whatever its size, so ask for more than the default 12. Time grows with the page: 12 reels took 4 to 8 seconds, 36 took 17 to 39, and 50 took 19 to 32 with one request timing out at 45 seconds (a 408, not charged). The script asks for 36.
  • Newest first. Older reels are on later pages, reached by sending paging_info.max_id back as max_id. A June reel from a creator who posts every other day was on the second page of 36. The script reads up to REEL_PAGES pages per owner and leaves views empty for reels it doesn't reach, rather than guessing.
  • The counts are exact, not rounded for display: Hoffmann's reel read 235,091 plays at 16:37 UTC and 235,093 at 16:53.
  • Other endpoints can disagree. /search/posts also returns a play_count. For three of the six reels we could compare, the two agreed to within a few dozen plays. For the other three the search result was higher: by 23% and 31%, and on one paid partnership reel 859,790 against 22,498, with the same 264 likes on both. Meta's own reference says its view_count "includes both paid and organic metrics", so the larger number may count plays of the brand's ad, but neither answer says. Use one source throughout a report. This guide uses the reels list.

Hidden like counts come back as a small number

An account can hide the like counts on its posts. One reel in our list, from a creator who does, came back like this:

{
  "code": "DcyYUWBIA02",
  "product_type": "clips",
  "like_and_view_counts_disabled": true,
  "like_count": 3,
  "comment_count": 533,
  …
}

Three likes on a reel with 533 comments is not a real count. When like_and_view_counts_disabled is true, the script writes the likes as unknown instead of copying that number into a report. Comments still come through.

Collabs and paid partnerships

Two things a campaign report wants are in the same answer.

Who published it. A collab post appears on more than one profile, and owner is the account that created it. The reel DcgSPGrvr9U sits on Lance Hedrick's grid, but its owner is decentespressomachines, with Hedrick under coauthor_producers. If you match posts to the creators you paid by owner alone, that one goes missing; the script writes both.

Whether it is labelled. Noah Stern's June reel about a Bosch espresso machine carries Instagram's paid partnership label:

{
  "code": "DZ-cbXoumTU",
  "is_paid_partnership": true,
  "sponsor_tags": [
    { "is_pending": false, "sponsor": { "id": "438318059", "username": "boschhomeus", "full_name": "Bosch Home Appliances US", … } }
  ],
  "coauthor_producers": [ { "id": "438318059", "username": "boschhomeus", "is_verified": true, … } ],
  "owner": { "id": "60524828106", "username": "noahsterncoffee", … },
  …
}

The brand is not always named. Another creator's reel had is_paid_partnership: true, an #ad in the caption and sponsor_tags: null. Nor does every sponsored-looking post carry the label: of six posts with #ad in the caption that one keyword search returned, four were labelled and two were not. The flag tells you which posts Instagram itself marks, nothing more.

When a link is dead

We changed the last letter of a real shortcode and looked it up:

{
  "success": false,
  "error": "Instagram media was not found."
}

That is a 404, and it is not charged. The answer does not say why Instagram has nothing at that address, so a mistyped link and a removed post look the same from here. The script prints the row as gone and carries on with the rest.

The script

Save this as ig_post_stats.py. It reads posts.csv (or the file you name), looks every post up, then reads the reels list of each account that owns a reel on your list, and appends one row per post to instagram_posts.csv.

import csv
import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone

import requests

API = "https://scrapingbot.io/api/v1/instagram"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
CONCURRENCY = 1  # your plan's limit: Free 1, Starter 10, Startup 50
REEL_PAGES = 2   # pages of 36 reels to read per owner when looking for a reel's views
OUT = "instagram_posts.csv"
FIELDS = ["checked", "shortcode", "type", "owner", "with", "posted", "likes", "comments",
          "views", "paid", "sponsor", "caption", "url"]
KINDS = {1: "photo", 2: "video", 8: "carousel"}
credits = []  # one entry per successful call: 5 credits each


class Gone(Exception):
    """Instagram shows nothing at this address: a wrong link, or a post that was removed."""


def call(endpoint, params, tries=4):
    """One Instagram call: 5 credits on success. Failed calls, a 404 included, cost nothing."""
    error = None
    for attempt in range(tries):
        try:
            res = requests.post(API, headers=HEADERS, timeout=90,
                                json={"endpoint": endpoint, "params": params})
            body = res.json()
        except (requests.RequestException, ValueError) as err:  # no answer, or not JSON
            error = err
        else:
            if body.get("success"):
                credits.append(5)
                return body["data"]
            if res.status_code == 404:
                raise Gone(body.get("error"))
            if res.status_code in (400, 401, 402):  # retrying will not help
                raise RuntimeError(f"{res.status_code} {body.get('error')}")
            error = body.get("error")
        time.sleep(2 ** attempt)  # 408, 429, 5xx or a network error: wait and try again
    raise RuntimeError(f"kept failing: {error}")


def shortcode(link):
    """The shortcode in a post link: /p/, /reel/, /reels/ or /tv/, with or without a username."""
    if "/share/" in link:
        raise ValueError("a share link: open it and copy the address it lands on")
    m = re.search(r"instagram\.com/(?:[\w.]+/)?(?:p|reels?|tv)/([\w-]+)", link)
    if not m:
        raise ValueError("not a link to one Instagram post")
    return m.group(1)


def stats(code):
    """One post or reel as a report row. Views are added later, from the owner's reels list."""
    try:
        d = call("/media/by_shortcode", {"shortcode": code})
    except Gone as err:
        return code, None, f"gone: {err}"
    except RuntimeError as err:
        return code, None, str(err)
    hidden = d.get("like_and_view_counts_disabled")  # the owner hides like counts
    return code, {
        "shortcode": code,
        "type": "reel" if d.get("product_type") == "clips" else KINDS.get(d.get("media_type"), "?"),
        "owner": d["owner"]["username"],
        "owner_id": d["owner"]["id"],
        "with": " ".join(c["username"] for c in d.get("coauthor_producers") or []),
        "posted": datetime.fromtimestamp(d["taken_at"], timezone.utc).strftime("%Y-%m-%d %H:%M"),
        "likes": "" if hidden else d.get("like_count"),
        "comments": d.get("comment_count"),
        "views": "",
        "paid": "yes" if d.get("is_paid_partnership") else "",
        "sponsor": " ".join(s["sponsor"]["username"] for s in d.get("sponsor_tags") or []),
        "caption": " ".join(((d.get("caption") or {}).get("text") or "").split())[:80],
        "url": f"https://www.instagram.com/p/{code}/",
    }, "likes hidden" if hidden else ""


def cell(value):
    """Text that starts like a formula gets a leading ' so a spreadsheet shows it, not runs it."""
    return "'" + value if isinstance(value, str) and value.startswith(("=", "+", "-", "@", "\t", "\r")) else value


def reel_views(job):
    """play_count for an owner's newest reels, read until every wanted reel has turned up."""
    owner_id, wanted = job
    plays, max_id = {}, None
    try:
        for _ in range(REEL_PAGES):
            params = {"user_id": owner_id, "page_size": 36}
            if max_id:
                params["max_id"] = max_id
            data = call("/reels/by_user_id", params)
            for item in data.get("items") or []:
                plays[item["media"]["code"]] = item["media"].get("play_count")
            paging = data.get("paging_info") or {}
            max_id = paging.get("max_id")
            if wanted <= plays.keys() or not paging.get("more_available") or not max_id:
                break
    except (Gone, RuntimeError) as err:
        print(f"reels of {owner_id}: {err}")
    return plays


if __name__ == "__main__":
    with open(sys.argv[1] if len(sys.argv) > 1 else "posts.csv", newline="", encoding="utf-8-sig") as f:
        links = [r["url"].strip() for r in csv.DictReader(f) if (r.get("url") or "").strip()]
    codes = []
    for link in links:
        try:
            code = shortcode(link)
            if code not in codes:  # the same post twice is one lookup
                codes.append(code)
        except ValueError as err:
            print(f"skipped {link}: {err}")

    checked = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M")
    with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
        results = list(pool.map(stats, codes))
        wanted = {}  # owner ID -> the reels of theirs on the list
        for _, row, _ in results:
            if row and row["type"] == "reel":
                wanted.setdefault(row["owner_id"], set()).add(row["shortcode"])
        plays = {}
        for found in pool.map(reel_views, wanted.items()):
            plays.update(found)

    print(f"{checked} UTC: {len(codes)} posts, {sum(credits)} credits")
    print(f"{'post':<13}{'type':<10}{'owner':<24}{'posted':<12}{'likes':>9}{'comments':>10}{'views':>11}  notes")
    rows = []
    for code, row, note in results:
        if row is None:
            print(f"{code:<13}{note}")
            continue
        notes = [note] if note else []
        if row["type"] == "reel":
            if plays.get(code) is not None:
                row["views"] = plays[code]
            else:
                notes.append("views: not reported" if code in plays else "views: not in the owner's newest reels")
        if row["with"]:
            notes.append("with " + row["with"])
        if row["paid"]:
            notes.append("paid partnership" + (": " + row["sponsor"] if row["sponsor"] else ""))
        n = {k: f"{row[k]:,}" if row[k] != "" else "-" for k in ("likes", "comments", "views")}
        print(f"{code:<13}{row['type']:<10}{row['owner']:<24}{row['posted'][:10]:<12}"
              f"{n['likes']:>9}{n['comments']:>10}{n['views']:>11}  {'; '.join(notes)}")
        rows.append({**row, "checked": checked})

    new = not os.path.exists(OUT)
    with open(OUT, "a", newline="", encoding="utf-8") as f:
        out = csv.DictWriter(f, fieldnames=FIELDS, extrasaction="ignore")
        if new:
            out.writeheader()
        out.writerows({key: cell(value) for key, value in row.items()} for row in rows)  # one row per post and run
    print(f"added {len(rows)} rows to {OUT}")
  • Set CONCURRENCY to your plan. Lookups don't depend on each other, so they run side by side up to your plan's concurrency limit: 1 on the free plan, 10 on Starter.
  • One reels list per owner, however many of that owner's reels are on your list. A collab reel is listed under its owner; in the two collabs we checked it was in the co-author's list as well.
  • Unknown stays empty. Hidden likes, views of a photo or carousel (Instagram shows none) and reels beyond REEL_PAGES are left blank in the CSV, never written as 0.
  • A failure skips one post, not the run. Timeouts (408), concurrency limits (429), server errors and dropped connections are retried with a growing pause. A post that still fails, or a 402 when the credits run out, becomes a line in the printout while the rest are saved. Failed calls are not charged.
  • No cell can run as a formula. Captions are written by strangers, and a spreadsheet treats a cell that starts with =, +, - or @ as a formula. cell() puts an apostrophe in front of such text so Excel or Sheets shows it instead; a caption that opens with an @mention gets one too.

Run it

A list the way people paste one: a reel link from the reels viewer, a carousel link pointing at a slide, a link with a username and tracking code, one post listed twice, one typo and one share link.

url
https://www.instagram.com/reel/DdZOfSWiKJb/
https://www.instagram.com/p/DbLR0mzFuWj/?img_index=1
https://www.instagram.com/p/DcTO39qFbGX/
https://www.instagram.com/lancehedrick/reel/Ddj3qyjN1c3/?igsh=MWQ1ZGUxMzBkMA==
https://www.instagram.com/reels/DcgSPGrvr9U/
https://www.instagram.com/p/DfVkfXvASwP/
https://www.instagram.com/reel/DfIfM-JS4EZ/
https://www.instagram.com/reel/DZ-cbXoumTU/
https://www.instagram.com/reel/DcyYUWBIA02/
https://www.instagram.com/p/DbLR0mzFuWX/
https://www.instagram.com/reel/DdZOfSWiKJb/?utm_source=ig_web_copy_link
https://www.instagram.com/share/reel/BAJqK0x9zQ
python ig_post_stats.py posts.csv
skipped https://www.instagram.com/share/reel/BAJqK0x9zQ: a share link: open it and copy the address it lands on
2026-11-10 16:53 UTC: 10 posts, 85 credits
post         type      owner                   posted          likes  comments      views  notes
DdZOfSWiKJb  reel      jimseven                2026-09-17     10,457       255    235,093
DbLR0mzFuWj  carousel  jimseven                2026-07-24      3,376       640          -
DcTO39qFbGX  photo     jimseven                2026-08-21      4,181        32          -  with bristolcoffeefestival
Ddj3qyjN1c3  reel      lancehedrick            2026-09-21      4,701        89    123,396
DcgSPGrvr9U  reel      decentespressomachines  2026-08-26        378        11     32,503  with lancehedrick
DfVkfXvASwP  carousel  bluebottle              2026-11-04        779        11          -
DfIfM-JS4EZ  reel      bluebottle              2026-10-30      1,165         7     46,746
DZ-cbXoumTU  reel      noahsterncoffee         2026-06-24        264        14     22,498  with boschhomeus; paid partnership: boschhomeus
DcyYUWBIA02  reel      lifewithgracefullyglam  2026-09-02          -       533          -  likes hidden; views: not in the owner's newest reels
DbLR0mzFuWX  gone: Instagram media was not found.
added 9 rows to instagram_posts.csv

Twelve links, ten posts, nine rows. The duplicate cost nothing, the typo came back 404 for free, and the 85 credits went to 9 lookups and 8 pages of reels. Four of those pages went on two prolific creators: the June reel turned up on its owner's second page, and the reel with hidden likes, posted September 2 by an account that posts more than one reel a day, was not among its newest 72. Raise REEL_PAGES for accounts like that, or leave it and accept the gap.

The CSV holds the same rows with the caption's first 80 characters and a clean link:

checked,shortcode,type,owner,with,posted,likes,comments,views,paid,sponsor,caption,url
2026-11-10 16:53,DdZOfSWiKJb,reel,jimseven,,2026-09-17 15:54,10457,255,235093,,,"I’m in Peru chasing a truly original specialty coffee experience. So naturally, ",https://www.instagram.com/p/DdZOfSWiKJb/
2026-11-10 16:53,DZ-cbXoumTU,reel,noahsterncoffee,boschhomeus,2026-06-24 16:42,264,14,22498,yes,boschhomeus,"Making café-quality coffee doesn’t have to be so tedious, and @boschhomeus 800 S",https://www.instagram.com/p/DZ-cbXoumTU/
2026-11-10 16:53,DcyYUWBIA02,reel,lifewithgracefullyglam,,2026-09-02 13:51,,533,,,,"☕️ Nespresso lovers, this little Amazon find is SUCH a smart coffee hack 👀 These",https://www.instagram.com/p/DcyYUWBIA02/
…

Run it daily and the file becomes the history

Each run appends rows with a new checked time, so scheduling the script (cron, or Task Scheduler on Windows) builds a history without a database:

0 9 * * * cd ~/campaign && python3 ig_post_stats.py posts.csv >> report.log 2>&1

A dozen lines read that file back and print what changed between the first and the latest check of each post:

import csv
from collections import defaultdict

runs = defaultdict(list)
with open("instagram_posts.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        runs[row["shortcode"]].append(row)

for code, rows in runs.items():
    first, last = rows[0], rows[-1]
    change = [f"{key} {int(last[key]) - int(first[key]):+,}" for key in ("likes", "comments", "views")
              if first[key] and last[key]]
    print(f"{code:<13}{last['owner']:<24}{first['checked']} to {last['checked'][11:]}  {', '.join(change)}")

Thirteen minutes after the run above, on the same list:

DdZOfSWiKJb  jimseven                2026-11-10 16:53 to 17:06  likes +1, comments +1, views +0
DbLR0mzFuWj  jimseven                2026-11-10 16:53 to 17:06  likes +0, comments +0
DfIfM-JS4EZ  bluebottle              2026-11-10 16:53 to 17:06  likes +0, comments +0, views +2
DZ-cbXoumTU  noahsterncoffee         2026-11-10 16:53 to 17:06  likes +0, comments +0, views +0
DcyYUWBIA02  lifewithgracefullyglam  2026-11-10 16:53 to 17:06  comments +0
…

One like and one comment on a two-month-old reel, two views on Blue Bottle's: small, but they register, because Instagram's counts come unrounded. (TikTok's don't.)

Cost and speed

  • 5 credits per post, and 5 per page of an owner's reels when you want views. Thirty posts by ten creators is 200 credits a run; daily for a month is 6,000, about $1.09 on Starter ($49.99 for 275,000 credits). The 100 free credits cover 20 lookups.
  • Dead links and failed calls are free. A 404, a 400 and a timeout cost nothing.
  • The reels pages set the pace. The run above took 2 minutes 49 seconds one call at a time, nearly all of it on the eight pages of 36 reels. With CONCURRENCY = 10 the same list took 36 seconds, and 54 on a second try.

Where to go from here

Common questions

Is there an official API that returns an Instagram post's stats from its URL?

No. Meta's Business Discovery reads another account's posts with like_count, comments_count and, for reels, view_count, but it takes a username, not a post link, and only works for business and creator accounts. Meta's oEmbed endpoint takes a post link and returns embed HTML with no counts (checked November 2026). For any public post, POST {"endpoint": "/media/by_shortcode", "params": {"shortcode": "<shortcode>"}} to https://scrapingbot.io/api/v1/instagram.

How do I get the view count of an Instagram reel?

Not from the single-post lookup: view_count was null on all 11 reels we looked up. The number is in the owner's reels list. /reels/by_user_id returned a play_count for every one of the 374 reels we listed from 32 accounts on November 10, 2026. In October the same field often came back empty, so treat it as optional: the script in this guide reads 36 reels a call for 5 credits, matches them to your links by shortcode, and leaves views blank when Instagram reports none.

Why is the like count missing or tiny on some posts?

The owner hides it. Such posts carry like_and_view_counts_disabled: true, and the like_count that comes with them is not the real one: a reel with 533 comments came back with like_count 3. Treat likes as unknown whenever that flag is true. Comments, and views from the reels list, still come through.

Which Instagram link formats work?

/media/by_url accepted /p/…/ and /reel/…/ links, with or without a username in front, https:// or tracking parameters. It refused /reels/…/ (plural) and /tv/…/ with a 400. Every form holds the same shortcode, so the script reads it out and calls /media/by_shortcode, which covers all of them.

Can I tell whether a post is a paid partnership or a collab?

Yes. is_paid_partnership is true on posts that carry Instagram's paid partnership label, and sponsor_tags names the brand when the creator tagged one. coauthor_producers lists the other accounts on a collab post. Note that owner is whoever published it, which on a collab can be the brand rather than the creator.

How much does a daily report on 30 posts cost?

Five credits per post, plus five per creator for their reels list when you want views: 30 posts by 10 creators is 200 credits a run, 6,000 a month, about $1.09 on the Starter plan ($49.99 for 275,000 credits). The 100 free credits cover 20 lookups. Posts that come back 404 are free.

Start scraping in the next five minutes.

100 free credits, no credit card. One API key works for websites, TikTok, Instagram, Google, Amazon and ChatGPT.