Instagram's official API gets posts only from Business and Creator accounts: your own through the media edge, which returns at most the 10,000 most recent, and other accounts through Business Discovery. It does not read personal accounts. We read Meta's documentation on 2026-10-16. ToolzerHub reads a public account's posts by username. We paged three accounts to the end, one exactly and two 12 to 18 short of their profile count, and the posts list can omit reels (see the reels section).
What Meta returns for your own posts
The call is GET /17841405822304914/media, Meta's sample ID, on graph.facebook.com (Facebook Login) or graph.instagram.com (Instagram Login). Meta's reference (updated September 28, 2026) lists two limits: "Returns a maximum of 10K of the most recently created media." and "Story IG Media not supported", pointing to the separate stories endpoint instead. Meta's Instagram Login get-started page (updated December 2, 2024) describes the same call as returning "the IDs of all the IG Media objects on the IG User"; the reference page is the more recent of the two. We did not test either. The edge takes since and until for time ranges, which is how you ask for recent posts.
It works for professional accounts only. Meta's IG Media reference (updated August 12, 2026): "This API returns only data for media owned by Instagram professional accounts. It can not be used to get data for media owned by personal Instagram accounts." For why the personal-account APIs ended, see our Basic Display replacement post.
Meta's pages give no default or maximum page size for this edge. The Graph API paging page says only that "Some edges may also have a maximum on the limit value for performance reasons."
| Field | What Meta says |
|---|---|
comments_count |
Public |
like_count |
In Business Discovery's sample |
view_count |
Reels; "Available for Business Discovery API only." |
media_product_type |
FEED, REELS, STORY or AD; Facebook Login only |
saved_count |
Owner or accepted collaborator |
The IG User reference lists no reels edge. A reel is a media object with media_product_type REELS.
What Meta returns for other accounts' posts
Business Discovery is the only official route to another account's posts. You call it on your own professional account's ID, with the other account's username: GET /17841405309211844?fields=business_discovery.username(bluebottle){media{like_count,comments_count,view_count}}, Meta's sample. The target must be a Business or Creator account, age-gated accounts return nothing, and Meta documents it for Facebook Login only. Our Basic Display API post covers its fields; this post is about paging a whole posts list.
Its media edge pages with before and after cursors only: Meta says the response "will not include previous or next fields", so you build the next URL yourself. A direct GET on a returned media object fails with insufficient permissions. Meta's Business Discovery pages state no cap on this edge.
Get a public account's posts with ToolzerHub
We make ToolzerHub. Our Instagram API reads a public account's posts from a username, with one API key and no Meta app, Facebook Page or professional account of your own. GET /v1/instagram/user/posts takes username_or_url (or user_id), count up to 50 and a cursor, and costs 1 credit per call (our OpenAPI file, 2026-10-16). It returns more_available and next_max_id, which you pass back as cursor.
Each post has code, taken_at, product_type, like_count, comment_count and caption. view_count was null on the NASA reel we checked, while /v1/instagram/user/reels returned play_count 9,955,101 for the same post, the plays shown on the owner's Reels tab. This script pages every username you give it to one CSV.
import csv
import os
import sys
import time
from datetime import datetime, timezone
import requests
URL = "https://api.toolzerhub.com/v1/instagram/user/posts"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["username", "code", "posted", "type", "likes", "comments", "caption", "url"]
calls = 0
FAILED = object() # marks a 400 or a 503 that outlasted the retries, so the run can tell it from "empty"
def cell(value):
# Spreadsheets run text that starts with = + - or @ as a formula; an apostrophe keeps it text.
if isinstance(value, str) and value[:1] in ("=", "+", "-", "@", "\t", "\r"):
return "'" + value
return value
def get(params, tries=3):
# 429 and 503 are retryable; the wait comes from the error body or the Retry-After header.
global calls
for attempt in range(tries):
r = requests.get(URL, headers=HEADERS, params=params, timeout=90)
calls += 1
if r.status_code == 400: # bad username: skip this account, keep the rest of the run
print("bad request:", r.json()["error"]["message"].splitlines()[0])
return FAILED
if r.status_code not in (429, 503) or attempt == tries - 1:
if r.status_code == 503: # an unknown username also ends here
return FAILED
if r.status_code >= 400:
sys.exit(f"HTTP {r.status_code}: {r.text[:200]}")
return r.json().get("data") # None when there is nothing to return
try:
wait = r.json()["error"]["details"]["retryAfter"]
except (ValueError, KeyError, TypeError):
wait = None
try:
wait = int(wait or r.headers.get("Retry-After") or 5 * (attempt + 1))
except ValueError:
wait = 5 * (attempt + 1)
time.sleep(min(wait, 60))
def export_account(writer, name, max_pages):
params, seen, pages, more, failed = {"username_or_url": name, "count": 50}, set(), 0, False, False
while pages < max_pages:
data = get(params)
if data is FAILED:
failed = True
break
if not data or not data.get("items"):
break # null data, a private account or no posts
pages += 1
for m in data["items"]:
if m["code"] in seen: # pages can repeat a post
continue
seen.add(m["code"])
writer.writerow({
"username": cell(name), "code": m["code"],
"posted": datetime.fromtimestamp(m["taken_at"], timezone.utc).isoformat(timespec="minutes"),
"type": m.get("product_type"), "likes": m.get("like_count"),
"comments": m.get("comment_count"),
"caption": cell(((m.get("caption") or {}).get("text") or "").replace("\n", " ")),
"url": f"https://www.instagram.com/p/{m['code']}/",
})
more = bool(data.get("more_available") and data.get("next_max_id"))
if not more:
break
params["cursor"] = data["next_max_id"]
time.sleep(0.3)
if failed:
status = " (gave up: bad request or 503 after retries; unknown usernames end up here)"
elif not pages:
status = " (nothing returned: private or empty)"
else:
status = " (stopped at the page cap, more available)" if more else " (reached the end)"
print(f"{name}: {len(seen)} posts, {pages} pages{status}")
return len(seen)
def main(accounts, max_pages="30", out="posts.csv"):
total = 0
with open(out, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for name in [a.strip().lstrip("@") for a in accounts.split(",") if a.strip()]:
total += export_account(writer, name, int(max_pages))
print(f"wrote {total} posts to {out} in {calls} calls")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit("usage: python instagram_posts.py nasa,spacex [max_pages] [file.csv]")
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
The script strips spaces and a leading @, skips empty items, de-duplicates by code, and reports whether each account ended or hit your page cap. An unknown username returns 503, not an empty result, so the script retries it twice (about 20 seconds) before skipping it; a 400 is skipped at once. A 429 that outlasts the retries stops the run. We tested the retry branch against a stand-in server, not live. cell() puts an apostrophe in front of text that starts with =, +, - or @, so a spreadsheet shows it as text.
Run the script
export TOOLZERHUB_API_KEY=your_key
python instagram_posts.py "spacex, @mrbeast ,nobody_here_qq9zz,,bad handle" 30 posts.csv
Output on 2026-10-16, with a stray space, an @, an empty item and two bad names added to test the handling:
spacex: 1087 posts, 22 pages (reached the end)
mrbeast: 502 posts, 11 pages (reached the end)
nobody_here_qq9zz: 0 posts, 0 pages (gave up: bad request or 503 after retries; unknown usernames end up here)
bad handle: 0 posts, 0 pages (gave up: bad request or 503 after retries; unknown usernames end up here)
wrote 1589 posts to posts.csv in 39 calls
It took 232 seconds. clips are reels. The first three MrBeast rows are older than the fourth, probably pinned posts, so sort by posted for newest first.
How deep the list goes
We compared each list with the account's post count from /v3/instagram/user/details (media_count, 3 credits). /v1/instagram/user/info has no post count. All numbers are from 2026-10-16.
| Account | Profile count | Rows read | Calls | End |
|---|---|---|---|---|
| SpaceX | 1,087 | 1,087 | 22 | reached |
| MrBeast | 520 | 502 | 11 | reached |
| Taylor Swift | 713 | 701 | 15 | reached |
| NASA | 4,967 | 2,000 | 40 | not reached |
"Reached" means more_available turned false. SpaceX matched its profile count exactly, back to 2015-06-21. MrBeast ended 18 short and Taylor Swift 12 short, so expect a small gap; we don't know why. NASA's 40 calls gave 2,000 rows, 1,999 unique, back to 2020-08-28, with more_available still true, so we can't say where its list stops or whether it stops. One post repeated on page 30, which is why the script de-duplicates.
Reels
/v1/instagram/user/reels takes the same username and returns 12 reels per call. count=50 is accepted, but the page stayed 12 in our test. MrBeast's reels list gave 480 reels in 40 calls and was still going; paged to the end on 2026-10-17 it held 705. His posts list had 502 posts, 211 of them reels, and 396 of the 480 reels were not in it. We did not find out why. Eleven of the 480 belong to other accounts, which looks like collaborative reels. Our Instagram reels API post covers the reels list.
Where Meta's API is the better pick
- Your own account or your customers' accounts, with their consent. Meta does not return
saved_count,shares_countortotal_views_countthrough Business Discovery. Our posts route returns none of them either, and itsview_countis null; use the reels route'splay_count. - Time ranges. Meta's media edge takes
sinceanduntil; ours takes a cursor only. - Documented behaviour. Meta states its cap, 10K; we know only what we measured, up to 2,000 on NASA.
- Reliability on bad input. On 2026-10-16 an unknown username on our posts route returned HTTP 503 ("Upstream returned 502", retryable true) on all 15 tries across three names;
/v1/instagram/user/inforeturns{"data": null}for it. In the 7 days before, the posts route had 59 successes and 2 server errors, and reels 11 and 2 (61 and 13 calls, some of them our own tests).
We returned public data only and did not test private accounts.
What it costs
Each call is 1 credit. SpaceX's 1,087 posts took 22 credits, MrBeast's 502 took 11: 33 credits for the two, 3 to 6 cents at our rates of $0.99 to $1.90 per 1,000 credits ($1.45 on the $29 plan). Failed calls cost 0. At 50 posts per call, 1,000 posts are 20 calls: 20 credits, $0.02 to $0.04. At 12 reels per call, 1,000 reels are 84 calls: 84 credits, $0.08 to $0.16. The free tier gives 100 credits a month, enough for about 5,000 posts.
For comments on one post, see how to export Instagram comments.
We are not affiliated with Meta Platforms, Inc. or Instagram.
Sources
- Meta, IG User Media reference (updated September 28, 2026), https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/media, read 2026-10-16
- Meta, IG Media reference (updated August 12, 2026), https://developers.facebook.com/documentation/instagram-platform/reference/instagram-media, read 2026-10-16
- Meta, IG User Business Discovery reference (updated August 12, 2026), https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user/business_discovery, read 2026-10-16
- Meta, Business Discovery guide, https://developers.facebook.com/documentation/instagram-platform/instagram-api-with-facebook-login/business-discovery, read 2026-10-16
- Meta, IG User reference (updated April 22, 2026), https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-user, read 2026-10-16
- Meta, Graph API paging, https://developers.facebook.com/docs/graph-api/results, read 2026-10-16
- Meta, Get started with the Instagram API with Instagram Login (updated December 2, 2024), https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started, read 2026-10-16
- ToolzerHub OpenAPI file, https://api.toolzerhub.com/openapi.json, checked 2026-10-16
- ToolzerHub pricing, https://toolzerhub.com/pricing, checked 2026-10-16