Instagram's official API returns a post's comments only with an access token from the user who created that post. ToolzerHub's /v1/instagram/post/comments reads any public post from its shortcode, top-level comments only, so the list is shorter than the count Instagram reports, which includes replies.
What Meta's comments edge returns
The call is GET /17895695668004550/comments, Meta's sample media ID. Meta's reference (updated January 21, 2025) lists three limits: "Returns only top-level comments. Replies to comments are not included unless you use field expansion to request the replies field." "Returns a maximum of 50 comments per query." "Comments cannot be filtered by timestamp." Versions 3.2 and later return newest first.
The permission rule: "An access token from a User who created the IG Media object", with instagram_manage_comments (Facebook Login) or instagram_business_manage_comments (Instagram Login) among the permissions. So it reads posts on accounts you run, or whose owners give your app a token. Comments on ads are "of a different type and are not supported" here.
Meta's IG Media reference (updated August 12, 2026) adds: "This API returns only data for media owned by Instagram professional accounts." We did not call Meta's API ourselves; this is from its documentation.
Replies and counts
Replies have their own edge, GET /{ig-comment-id}/replies, which needs "An access token from a User who created the comment" (updated November 9, 2021).
Meta says comments_count on a media object "Includes replies on comments." So a list of top-level comments is shorter than the count whenever anyone replied.
Business Discovery, Meta's route to another account's media, returns counts in its sample (media{comments_count,like_count,view_count}), not comment text, and the page documents no comments edge for it (updated August 12, 2026; Facebook Login only; Business or Creator targets). Our post on paging a user's posts covers that route.
Get a public post's comments with ToolzerHub
We make ToolzerHub. Our Instagram API returns a public post's comments as JSON with one API key and no Meta app or professional account. GET /v1/instagram/post/comments takes code (the shortcode after /p/ or /reel/), cursor and count, and costs 1 credit per call (our OpenAPI file, 2026-10-17). You pass page_info.end_cursor back as cursor until has_next_page is false.
Each comment has pk, text, created_at (Unix seconds), comment_like_count and the author's profile fields. child_comment_count was null on every comment we read, and no reply rows came back. Save this as instagram_comments.py; it leaves the author out.
import csv
import os
import re
import sys
import time
from datetime import datetime, timezone
import requests
URL = "https://api.toolzerhub.com/v1/instagram/post/comments"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["post", "comment_id", "created_at", "likes", "text"]
calls = 0
FAILED = object() # marks a post given up on: a 400, a 5xx, or a 503 that outlasted the retries
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 shortcode(item):
# Accepts a shortcode or a post, reel or tv URL; returns None for anything else.
m = re.search(r"instagram\.com/(?:[^/]+/)?(?:p|reel|reels|tv)/([A-Za-z0-9_-]+)", item)
code = m.group(1) if m else item
return code if re.fullmatch(r"[A-Za-z0-9_-]{5,}", code) else None
def get(params, tries=4):
# 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: # malformed request: skip this post, keep the rest of the run
print("bad request:", r.text[:120].replace("\n", " "))
return FAILED
if r.status_code not in (429, 503) or attempt == tries - 1:
if r.status_code >= 500: # any 5xx, or a 503 that outlasted the retries: give up on this post
return FAILED
if r.status_code >= 400: # bad key, no credits or a rate limit that outlasted the retries
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_post(writer, code, max_pages):
params, seen, pages, more, failed = {"code": code, "count": 50}, set(), 0, False, False
while pages < max_pages:
data = get(params)
if not pages and data is not FAILED and not (data or {}).get("items"):
data = get(params) # re-ask once before calling a post empty
if data is FAILED:
failed = True
break
if not data or not data.get("items"):
break # null data, an unknown code, comments off, none yet, or an empty page
pages += 1
for c in data["items"]:
cid = str(c.get("pk") or c.get("id"))
if cid in seen: # keep one row per comment id
continue
seen.add(cid)
made = datetime.fromtimestamp(c["created_at"], timezone.utc).isoformat(timespec="seconds") if c.get("created_at") else ""
writer.writerow({
"post": code, "comment_id": cid, "created_at": made,
"likes": c.get("comment_like_count") or 0,
"text": cell((c.get("text") or "").replace("\n", " ")),
})
info = data.get("page_info") or {}
more = bool(info.get("has_next_page") and info.get("end_cursor"))
if not more:
break
params["cursor"] = info["end_cursor"]
time.sleep(0.4) # the free plan allows 3 requests a second
if failed:
status = " (gave up on this post: bad request or server error)"
elif not pages:
status = " (nothing returned: unknown code, comments off or none yet)"
elif more and pages < max_pages:
status = " (a page came back empty; more may be available)"
elif more:
status = " (stopped at the page cap, more available)"
else:
status = " (reached the end; top-level comments only, no replies)"
print(f"{code}: {len(seen)} comments, {pages} pages{status}")
return len(seen)
def main(posts, max_pages="50", out="comments.csv"):
total = 0
with open(out, "w", newline="", encoding="utf-8-sig") as f: # the BOM lets Excel read emoji
writer = csv.DictWriter(f, fieldnames=FIELDS)
writer.writeheader()
for item in [p.strip() for p in posts.split(",") if p.strip()]:
code = shortcode(item)
if not code:
print(f"{item}: skipped (not a post shortcode or URL)")
continue
total += export_post(writer, code, int(max_pages))
print(f"wrote {total} comments to {out} in {calls} calls")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit("usage: python instagram_comments.py CODE_OR_URL[,...] [max_pages] [file.csv]")
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
The script de-duplicates by comment ID and skips items that are not a shortcode or URL. A 400 or any 5xx gives up on that post and moves on; a 429 or 503 is retried first, waiting for retryAfter or Retry-After. Any other 4xx, or a 429 that outlasts the retries, stops the run. cell() puts an apostrophe in front of text that starts with =, +, - or @, so a spreadsheet shows it as text. We tested the retry and give-up branches against a stand-in server, not live.
Run the script
export TOOLZERHUB_API_KEY=your_key
python instagram_comments.py "https://www.instagram.com/p/DehZj9wggt0/ , DehUTymF6VL,Dek_RAKjMDJ,AAAAAAAAAAA,,bad code!" 50 comments.csv
Output on 2026-10-17, with a stray space, a URL, an unknown code, an empty item and a malformed code added to test the handling:
DehZj9wggt0: 274 comments, 19 pages (reached the end; top-level comments only, no replies)
DehUTymF6VL: 460 comments, 32 pages (reached the end; top-level comments only, no replies)
Dek_RAKjMDJ: 56 comments, 4 pages (reached the end; top-level comments only, no replies)
AAAAAAAAAAA: 0 comments, 0 pages (nothing returned: unknown code, comments off or none yet)
bad code!: skipped (not a post shortcode or URL)
wrote 790 comments to comments.csv in 57 calls
It took 120 seconds; the unknown code is asked twice before it is reported as empty. Counts grow while people comment, so the table below, from an earlier run that day, differs by a few rows. The file is written with a UTF-8 byte order mark, so Excel shows emoji and accents when you double-click it; Google Sheets opens it as is. About 2% of rows start with an apostrophe added by the cell() guard. We don't show rows from the file: the comments are ordinary people's, personal data about people who never signed up for your project, so collect only the fields you need and delete the file when the job is done.
created_at runs newest first on all three posts. 43 of the 790 rows have empty text; on the NASA post all 40 empty ones carry giphy_media_info, so they look like GIF comments.
How much of Instagram's count you get
On 2026-10-17 we paged five public posts to the end and compared the rows with the count Instagram shows. /v2/instagram/media/comments returns a reply count per comment, so we added those to its own rows to see how much the missing replies explain.
| Post | Shown | Rows | With replies | Share |
|---|---|---|---|---|
| DehZj9wggt0 | 319 | 274 | 302 | 95% |
| DehUTymF6VL | 536 | 458 | 513 | 96% |
| Deb8WwanBzo | 1,626 | 1,188 | 1,586 | 98% |
| DeSM5CYPDfd | 939 | 353 | 458 | 49% |
| DeTUk4sDSp- | 6,845 | 5,269 | n/a | n/a |
"Shown" is comment_count from /v2. "With replies" is /v2's top-level rows (274, 462, 1,197, 353 to 356) plus its child_comment_count sum, and "Share" is that column over "Shown". /v2 rows differ from ours by up to 9. On the first three, replies explain nearly all of the gap. DeSM5CYPDfd is an outlier we can't explain: /v1/instagram/user/posts gave it 590 comments, /v2 gave 939. The 6,845-comment post ended on its own after 384 calls, all HTTP 200.
No page held more than 15 comments, although count accepts 1 to 50. /v2 tells you which comments have replies but returns no reply text, and on the 6,845-comment post it stopped with a non-retryable HTTP 500 at page 101, after 1,373 comments. In the 7 days before our tests /v2 had 1,854 successes and 1 server error; /v1 had 16 successes and none, too few to judge, so we ran 10 posts on 5 public accounts: all 10 returned comments.
Where Meta's API is the better pick
- Your own account, or accounts whose owners connect to your app. A
POSTto the same edge writes a comment, which ours can't do. - Replies at no per-call cost on your own posts. For other public posts, our
/v3/instagram/post/comments/repliesreturns reply rows at 3 credits a call, one call per comment with replies; the script above doesn't call it, and we did not page it to the end. - A stated limit. Meta prints its page size, 50. We know only what we measured: 15 a page, and one post to the end at 5,269.
An unknown code returns an empty list, the same shape as a page with no comments. We returned public data only and did not test private accounts.
What it costs
Each call is 1 credit and returns about 14 top-level comments, so 1,000 comments are about 70 credits (replies on /v3 cost 3 credits a call on top): $0.07 to $0.13 at $0.99 to $1.90 per 1,000 credits ($0.10 on the $29 plan at $1.45). The 6,845-comment post took 384 credits for 5,269 rows: $0.38 to $0.73. Failed calls cost 0 credits. The free tier gives 100 credits a month, about 1,400 comments. Meta's reference pages we read print no per-call price.
See also Instagram's follower list limits and, for another platform, YouTube's comment limits.
We are not affiliated with Meta Platforms, Inc. or Instagram.
Sources
- Meta, IG Media Comments reference (updated January 21, 2025), https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-media/comments, read 2026-10-17
- Meta, IG Comment Replies reference (updated November 9, 2021), https://developers.facebook.com/documentation/instagram-platform/instagram-graph-api/reference/ig-comment/replies, read 2026-10-17
- Meta, IG Media reference (updated August 12, 2026), https://developers.facebook.com/documentation/instagram-platform/reference/instagram-media, read 2026-10-17
- 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-17
- ToolzerHub OpenAPI file, https://api.toolzerhub.com/openapi.json, checked 2026-10-17
- ToolzerHub pricing, https://toolzerhub.com/pricing, checked 2026-10-17