Guide · Python

Get a public Instagram profile and its reels with Python

Turn a username into a profile summary and a CSV of the account's reels with likes and comments, and write code that does not break when Instagram leaves a count empty.

ScrapingBot 8 min read
Endpoints
/user/by_username, /reels/by_user_id
Cost
5 credits per call
Language
Python 3.9+
Needs
requests
On this page

Checking a creator before a deal, tracking a brand's short-form output or feeding reels into a report all start with the same two lookups: who is this account, and what have they posted lately? This guide does both from Python, with no Instagram login.

You will look up a public profile by username, take its numeric ID, and page through its reels with a cursor. Along the way you will see which fields Instagram fills in reliably and which it often leaves empty, because that difference decides whether your script survives contact with real data. Every response below is real, from the @nasa account, trimmed with … where it runs long.

Before you start

  • A ScrapingBot API key. Create a free account for 100 credits (20 Instagram calls). No card needed.
  • Python 3.9 or newer and pip install requests.
  • A public Instagram username. This guide uses nasa.

Look up the profile

Every Instagram call is a POST to https://scrapingbot.io/api/v1/instagram with the endpoint and its parameters in the body. The username can have an @ in front, or you can send the profile URL instead.

curl -X POST https://scrapingbot.io/api/v1/instagram \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"endpoint": "/user/by_username", "params": {"username": "nasa"}}'

The profile comes back in data. The full object has several dozen keys; these are the ones most people use:

{
  "success": true,
  "data": {
    "id": "528817151",
    "username": "nasa",
    "full_name": "NASA",
    "biography": "Making the seemingly impossible, possible. ✨",
    "follower_count": 104285314,
    "following_count": 89,
    "media_count": null,
    "is_verified": true,
    "is_private": false,
    "external_url": "https://www.nasa.gov",
    "bio_links": [
      { "title": "NASA.gov Homepage", "url": "https://www.nasa.gov", "link_type": "external", … },
      { "title": "MAX POWER", "url": "https://www.nasa.gov/maxpower", … },
      …
    ],
    "profile_pic_url": "https://scontent-iad3-1.cdninstagram.com/…",
    …
  },
  "duration": "0.30",
  "statusCode": 200,
  "creditsUsed": 5
}

Two things to notice. First, id is a string of digits. That is the account's numeric ID, and every list endpoint (reels, posts, followers) takes it as user_id, so this lookup is always step one. Second, media_count is null. The account plainly has posts, but Instagram did not include the number in this response. Write p.get("media_count") and allow for None; do not assume the post count is there.

external_url is the main website link, and bio_links is the list of links shown under the bio, each with a title and url. NASA has five.

Fetch a page of reels

With the ID, ask for reels. page_size is how many you would like, from 1 to 50 (count works too):

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": "528817151", "page_size": 12}}'

Each reel sits under items[].media, and the cursor for the next page is in paging_info:

{
  "success": true,
  "data": {
    "items": [
      {
        "media": {
          "id": "3996242083173720380",
          "code": "Dd1gQBsCWE8",
          "taken_at": 1790609228,
          "media_type": 2,
          "product_type": "clips",
          "like_count": 104409,
          "comment_count": 898,
          "play_count": null,
          "ig_play_count": null,
          "view_count": null,
          "caption": { "text": "What happens when we detect an asteroid that could pose a threat to Earth?\n\n…" },
          "video_versions": [ { "url": "https://scontent-lga3-2.cdninstagram.com/…" }, … ],
          …
        }
      },
      …
    ],
    "paging_info": { "max_id": "3984690691148471393_4092263381", "more_available": true },
    "status": "ok"
  },
  "duration": "2.61",
  "statusCode": 200,
  "creditsUsed": 5
}

We asked for 12 and got 6. Instagram decides the final page size, so a short page does not mean the list has ended. Only more_available tells you that.

Play and view counts are often empty

Each reel has three fields that look like a view count: play_count, ig_play_count and view_count. On all six NASA reels in this response, all three were null. This is common, so do not build a report that depends on plays. like_count and comment_count were filled in on every reel; use those, and treat plays as a bonus when one of the three fields has a number.

The full script

Save this as reels.py. It looks up the profile, refuses private accounts, walks the reels with max_id, keeps one copy of each reel, writes a CSV, and adds up creditsUsed from every response so you can see what the run cost.

import csv
import sys
import time
from datetime import datetime, timezone

import requests

API = "https://scrapingbot.io/api/v1/instagram"
HEADERS = {"x-api-key": "YOUR_API_KEY"}
credits_used = 0


def call(endpoint, params, tries=3):
    """One Instagram call: 5 credits on success. Failed calls are refunded."""
    global credits_used
    for attempt in range(tries):
        res = requests.post(API, headers=HEADERS, timeout=60,
                            json={"endpoint": endpoint, "params": params})
        body = res.json()
        if body.get("success"):
            credits_used += body.get("creditsUsed", 0)
            return body["data"]
        if res.status_code in (400, 401, 402):  # bad input, bad key, no credits
            raise RuntimeError(body.get("error"))
        time.sleep(2 ** attempt)  # 408, 429 or 5xx: wait and try again
    raise RuntimeError(f"{endpoint} kept failing: {body.get('error')}")


def first_number(*values):
    """Return the first value that is a number, or None. Zero counts as a number."""
    for v in values:
        if isinstance(v, (int, float)):
            return v
    return None


def fmt(n):
    return f"{n:,}" if n is not None else "n/a"


def get_profile(username):
    p = call("/user/by_username", {"username": username})
    if p.get("is_private"):
        raise RuntimeError(f"@{p['username']} is private; only public accounts work")
    return p


def get_reels(user_id, max_pages=5, page_size=12):
    reels, max_id = {}, None
    for page in range(1, max_pages + 1):
        params = {"user_id": user_id, "page_size": page_size}
        if max_id:
            params["max_id"] = max_id
        data = call("/reels/by_user_id", params)
        for item in data.get("items", []):
            m = item["media"]
            reels.setdefault(m["code"], m)
        paging = data.get("paging_info") or {}
        print(f"page {page}: {len(data.get('items', []))} reels, {len(reels)} total")
        if not paging.get("more_available") or not paging.get("max_id"):
            break
        max_id = paging["max_id"]
    return list(reels.values())


def reel_row(m):
    caption = (m.get("caption") or {}).get("text") or ""
    return {
        "code": m["code"],
        "url": f"https://www.instagram.com/reel/{m['code']}/",
        "posted": datetime.fromtimestamp(m["taken_at"], timezone.utc).date().isoformat(),
        "likes": m.get("like_count"),
        "comments": m.get("comment_count"),
        # Often empty: Instagram leaves play and view counts null for many reels.
        "plays": first_number(m.get("play_count"), m.get("ig_play_count"), m.get("view_count")),
        "caption": caption.split("\n")[0][:100].strip(),
    }


if __name__ == "__main__":
    username = sys.argv[1] if len(sys.argv) > 1 else "nasa"
    max_pages = int(sys.argv[2]) if len(sys.argv) > 2 else 5

    p = get_profile(username)
    print(f"@{p['username']} ({p['full_name']}), id {p['id']}"
          f"{', verified' if p.get('is_verified') else ''}")
    print(f"followers {fmt(p.get('follower_count'))}, "
          f"following {fmt(p.get('following_count'))}, posts {fmt(p.get('media_count'))}")
    print("bio:", p.get("biography") or "")
    for link in p.get("bio_links") or []:
        print("link:", link.get("title") or "", link["url"])

    rows = [reel_row(m) for m in get_reels(p["id"], max_pages)]
    rows.sort(key=lambda r: r["likes"] or 0, reverse=True)
    for r in rows:
        print(f"{r['posted']}  {r['code']}  likes {fmt(r['likes']):>7}  "
              f"comments {fmt(r['comments']):>5}  plays {fmt(r['plays'])}")

    path = f"reels_{username}.csv"
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0]) if rows else ["code"])
        writer.writeheader()
        writer.writerows(rows)
    print(f"saved {len(rows)} reels to {path}; credits used: {credits_used}")

The parts that matter:

  • first_number() handles the empty counts. It returns the first of play_count, ig_play_count and view_count that is actually a number, or None. A plain a or b or c would also skip a real count of 0, so the helper checks the type instead.
  • fmt() prints n/a for None. Formatting None with :, raises a TypeError, which is exactly the crash this guide is trying to save you from. The CSV gets an empty cell instead.
  • Paging stops on more_available or a missing max_id. Reels are keyed on code, so a reel that shows up on two pages is stored once.
  • Credits come from the response. Each successful call returns creditsUsed; summing them is more honest than multiplying calls by 5, and refunded failures add nothing.
  • max_pages caps the spend. At 5 credits a page, the default of 5 pages plus the profile can never cost more than 30 credits.

Run it

Pass a username and, optionally, how many pages of reels to fetch. With one page:

python reels.py nasa 1
@nasa (NASA), id 528817151, verified
followers 104,285,314, following 89, posts n/a
bio: Making the seemingly impossible, possible. ✨
link: NASA.gov Homepage https://www.nasa.gov
link: MAX POWER https://www.nasa.gov/maxpower
…
page 1: 6 reels, 6 total
2026-09-14  DdRyQxKteC1  likes 237,399  comments 2,481  plays n/a
2026-09-12  DdMdxJhtdxh  likes 114,302  comments   225  plays n/a
2026-09-28  Dd1gQBsCWE8  likes 104,409  comments   898  plays n/a
2026-09-13  DdPDuIepVPf  likes  83,242  comments   318  plays n/a
2026-09-13  DdPsDCWRT-u  likes  60,560  comments   262  plays n/a
2026-09-20  DdhFkS7KGkZ  likes  48,221  comments   604  plays n/a
saved 6 reels to reels_nasa.csv; credits used: 10

The reels are sorted by likes, so the one at the top is the account's best performer in the window you fetched: the space station dance clip, with 237,399 likes and 2,481 comments. The first rows of reels_nasa.csv:

code,url,posted,likes,comments,plays,caption
DdRyQxKteC1,https://www.instagram.com/reel/DdRyQxKteC1/,2026-09-14,237399,2481,,Volume up and get your groove on! A bit of weekend dancing on the @iss to start your week off right.
DdMdxJhtdxh,https://www.instagram.com/reel/DdMdxJhtdxh/,2026-09-12,114302,225,,"Star and city trails with aurora. I shot this long exposure (30s) series with a Nikon Z9, 15mm lens"
…

To compare creators of different sizes, divide likes plus comments by follower_count. For NASA's top reel that is about 0.23% of 104 million followers, a useful baseline when you look at smaller accounts in the same niche.

Regular posts, too

Reels are only part of a profile. Photos and carousels come from /medias/by_user_id, which pages differently: it returns edges[].node and a page_info block, and you send page_info.end_cursor back as end_cursor while has_next_page is true. Add this to the same file, after the other functions:

KINDS = {1: "photo", 2: "video", 8: "carousel"}


def get_posts(user_id, max_pages=3, count=12):
    posts, cursor = {}, None
    for page in range(1, max_pages + 1):
        params = {"user_id": user_id, "count": count}
        if cursor:
            params["end_cursor"] = cursor
        data = call("/medias/by_user_id", params)
        for edge in data.get("edges", []):
            posts.setdefault(edge["node"]["code"], edge["node"])
        info = data.get("page_info") or {}
        if not info.get("has_next_page") or not info.get("end_cursor"):
            break
        cursor = info["end_cursor"]
    return list(posts.values())


for n in get_posts("528817151", max_pages=1):
    posted = datetime.fromtimestamp(n["taken_at"], timezone.utc).date()
    print(posted, n["code"], KINDS.get(n["media_type"], n["media_type"]),
          "likes", fmt(n.get("like_count")), "comments", fmt(n.get("comment_count")))
print("credits used:", credits_used)
2026-09-10 DdHyaYAifb6 carousel likes 119,578 comments 960
2026-09-16 DdWyDLcGiqo carousel likes 746,308 comments 1,783
2026-09-15 DdT_4GMka0z photo likes 345,844 comments 864
2026-10-02 DeAYvsnn4la carousel likes 491,879 comments 1,768
2026-10-01 Dd9gYJnDCKl photo likes 93,771 comments 483
2026-09-30 Dd7J2GbFx1V carousel likes 197,076 comments 1,990
credits used: 5

The first three are older than the rest because NASA pinned them; pinned posts carry a non-empty timeline_pinned_user_ids and come first. Sort by taken_at if you want strict date order. media_type is 1 for a photo, 2 for a video and 8 for a carousel, and a carousel's slides are in carousel_media.

Fields you can rely on

FieldWhereIn our responses
idProfileAlways present. The user_id for every list endpoint.
follower_count, following_countProfilePresent.
biography, external_url, bio_linksProfilePresent when the account has set them.
media_countProfilenull for NASA. Do not depend on it.
codeReel or postThe shortcode. The link is instagram.com/reel/<code>/ or /p/<code>/.
taken_atReel or postUnix timestamp in seconds.
like_count, comment_countReel or postPresent on every item we fetched.
caption.textReel or postRead it defensively, as (m.get("caption") or {}).get("text"), so a missing caption gives an empty string rather than an error.
play_count, ig_play_count, view_countReelOften null. All three were empty on every NASA reel.
video_versions[].urlReelA playable file. Signed and expires, so download soon if you need it.

Where to go from here

Common questions

How do I get an Instagram user's reels with Python?

Look the account up with /user/by_username to get its numeric id, then call /reels/by_user_id with that user_id. Send paging_info.max_id back as max_id while paging_info.more_available is true.

Can I get the play or view count of a reel?

Not reliably. The fields play_count, ig_play_count and view_count exist on each reel, but Instagram often leaves all three empty (null). Likes and comments are filled in, so build on those and treat plays as optional.

Why is media_count null in the profile?

Instagram does not always include the post count in the profile response. In our NASA lookup it was null while followers, following and the bio were all present. Count posts by paging /medias/by_user_id if you need the number.

Does this work for private accounts?

No. Only public profiles and their public posts and reels are available. The script checks is_private on the profile and stops before spending credits on reels.

How many credits does it use?

Every Instagram call costs 5 credits: 5 for the profile and 5 per page of reels. A profile plus one page is 10 credits, so the 100 free credits cover about 19 pages after the profile. Failed calls are refunded.

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.