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 ofplay_count,ig_play_countandview_countthat is actually a number, orNone. A plaina or b or cwould also skip a real count of0, so the helper checks the type instead.fmt()printsn/aforNone. FormattingNonewith:,raises aTypeError, which is exactly the crash this guide is trying to save you from. The CSV gets an empty cell instead.- Paging stops on
more_availableor a missingmax_id. Reels are keyed oncode, 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_pagescaps 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
| Field | Where | In our responses |
|---|---|---|
id | Profile | Always present. The user_id for every list endpoint. |
follower_count, following_count | Profile | Present. |
biography, external_url, bio_links | Profile | Present when the account has set them. |
media_count | Profile | null for NASA. Do not depend on it. |
code | Reel or post | The shortcode. The link is instagram.com/reel/<code>/ or /p/<code>/. |
taken_at | Reel or post | Unix timestamp in seconds. |
like_count, comment_count | Reel or post | Present on every item we fetched. |
caption.text | Reel or post | Read 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_count | Reel | Often null. All three were empty on every NASA reel. |
video_versions[].url | Reel | A playable file. Signed and expires, so download soon if you need it. |
Where to go from here
- Showing an account's posts on a website? Replace the Instagram Basic Display API in Node.js builds a cached feed widget.
- Doing the same for TikTok creators: get TikTok follower and like counts with Python.
- The reels reference and the profile reference list every parameter, and the Instagram API page lists every endpoint, including comments, stories and search.
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.