Comments are the most valuable and the most awkward part of Instagram data. The caption tells you what an account wanted to say; the comments tell you what actually happened. Unlike a profile lookup, comments are paginated, they arrive in fixed-size pages, and replies to a comment live behind a second endpoint. If you get the ordering wrong, you either fetch the same page forever or silently drop half the thread.
This guide uses the GramScraper API — an Instagram-specific wrapper that returns structured JSON — to walk through the whole path: resolve a post to its media ID, pull the comment page, follow the cursor, and expand replies. Every field name and cost below is taken from the live endpoint contract in the API docs.
Why comments need their own endpoint
A post and its comments are different resources. The post object carries a comment_count (a number). The comments themselves are a separate, paginated list that can run into the tens of thousands on high-traffic posts. Instagram pages that list, which is why the API returns a bounded number of comments per call plus a cursor.
That structure is good news for you: you pay per page, you can stop whenever your analysis is complete, and you never download a 90,000-comment thread just to measure sentiment on the newest 200.
The endpoints
Four endpoints cover comment work. All are GET requests against the Instagram base URL:
| Endpoint | What it does |
|---|---|
/v2/media/comments | Top-level comments for one media. Returns a page of comments plus a next_page_id cursor. The documented page size is 15 comments per request. |
/v1/media/comments/chunk | The older cursor-based variant, paged with min_id / max_id instead of page_id. |
/v2/media/comments/replies | Replies to a specific comment. Paged with min_id. |
/v2/media/comments/infos | Comment metadata for up to 10 media IDs in one call: counts and toggles, without the comment bodies. |
Base URL: https://gramscraper.com/api/instagram. Every request needs Authorization: Bearer gs_your_key. You can get a key after creating a free account with 100 credits.
Step 1: resolve the post to a media ID
The comments endpoint takes a numeric media ID, not the shortcode you see in the URL. If you only have the post URL or the code from instagram.com/p/<code>/, resolve it first with one of the media lookups:
curl -s -H "Authorization: Bearer gs_your_key" \
"https://gramscraper.com/api/instagram/v1/media/by/url?url=https://www.instagram.com/p/CA2aJYrg6cZ/"Alternatively use /v2/media/info/by/code?code=CA2aJYrg6cZ or /v1/media/by/code?code=CA2aJYrg6cZ. Any of them returns a media object containing pk or id — that number is the media ID the comments endpoint wants. This resolution is one credit.
If you are coming from a reels analysis, note that the same media ID powers reels, comments, and likers. See the Reels API guide for fetching reels in the first place.
Step 2: fetch the first page of comments
curl -s -H "Authorization: Bearer gs_your_key" \
"https://gramscraper.com/api/instagram/v2/media/comments?id=3162273595027945978"You can also pass a shortcode or url directly to /v2/media/comments. When you do, the API resolves the media ID for you, which is one extra credit on top of the comments call (the response _meta block breaks the two costs out).
Node.js 18+
const BASE = "https://gramscraper.com/api/instagram";
const KEY = process.env.GRAMSCRAPER_API_KEY;
async function getComments(mediaId, pageId = null) {
const url = new URL(`${BASE}/v2/media/comments`);
url.searchParams.set("id", mediaId);
if (pageId) url.searchParams.set("page_id", pageId);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
const page = await getComments("3162273595027945978");
for (const c of page.response.items) {
console.log(c.user.username, "—", c.text, `(${c.like_count ?? 0} likes)`);
}
console.log("next cursor:", page.next_page_id);Python 3
import os, requests
BASE = "https://gramscraper.com/api/instagram"
HEADERS = {"Authorization": f"Bearer {os.environ['GRAMSCRAPER_API_KEY']}"}
def get_comments(media_id, page_id=None):
params = {"id": media_id}
if page_id:
params["page_id"] = page_id
r = requests.get(f"{BASE}/v2/media/comments", headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
page = get_comments("3162273595027945978")
for c in page["response"]["items"]:
print(c["user"]["username"], "—", c["text"])
print("next cursor:", page["next_page_id"])The response shape
Comment list endpoints return a common envelope. The comments live under response.items; the cursor lives at the top level in next_page_id:
{
"response": {
"items": [
{
"pk": "17900000000000001",
"text": "this is the comment body",
"created_at_utc": "2026-09-22T10:04:11+00:00",
"content_type": "comment",
"status": "ok",
"has_liked": false,
"like_count": 12,
"user": {
"pk": "50117947544",
"username": "some_user",
"full_name": "Some User",
"profile_pic_url": "https://scontent.cdninstagram.com/...",
"is_private": false,
"is_verified": false
}
}
],
"paging_info": { "max_id": "cursor_string", "more_available": true },
"status": "ok"
},
"next_page_id": "QVFCcnhIWE82OUswLXBtNzlpZ0ZaUnhWZEhr"
}The fields you will actually use:
| Field | Meaning |
|---|---|
pk | The comment’s own ID. Store it — it is how you deduplicate across pages and how you request replies. |
text | The comment body, plain text (emoji included). |
created_at_utc | ISO-8601 timestamp in UTC. |
like_count | Likes on the comment. Can be null when Instagram withholds it. |
user | A short user object: pk, username, full_name, profile_pic_url, is_private, is_verified. |
next_page_id | The cursor for the next page. null means you have reached the end. |
One field is deliberately absent: the comment object does not carry the reply text. Replies are their own resource, which is the next section.
Pagination that actually terminates
The loop is small, but two details matter. First, stop when next_page_id is null or empty — not when a page comes back shorter than expected, because Instagram does not guarantee a fixed page size. Second, always pass the cursor back as page_id.
async function allComments(mediaId, maxPages = 20) {
const out = [];
let cursor = null, pages = 0;
do {
const page = await getComments(mediaId, cursor);
out.push(...page.response.items);
cursor = page.next_page_id || null;
pages += 1;
} while (cursor && pages < maxPages);
return out;
}
// 20 pages × 15 comments ≈ 300 comments, then stop and reassess.
const comments = await allComments("3162273595027945978");A maxPages guard is not optional. Popular posts can carry hundreds of pages, and each page is billed. Cap the walk at the volume your analysis needs, then decide whether to continue. This mirrors the backoff discipline in the rate limits guide.
Replies: the second request
Replies are fetched per comment with /v2/media/comments/replies. Pass both the media ID and the comment pk you stored earlier, and page with min_id:
curl -s -H "Authorization: Bearer gs_your_key" \
"https://gramscraper.com/api/instagram/v2/media/comments/replies?media_id=3162273595027945978&comment_id=17900000000000001"A practical pattern is to fetch top-level comments first, then request replies only for the comments you care about — the ones with the most likes, or the ones from verified accounts. Fetching replies for every comment multiplies your request count and your cost for little analytical gain.
Credit cost per call
- Resolve a URL or shortcode to a media ID: 1 credit (
/v1/media/by/url,/v1/media/by/code). - One page of top-level comments: 1 credit when you already have the media ID.
- Pass
shortcodeorurlstraight to/v2/media/comments: 2 credits total, because the media lookup and the comments call are billed separately. - One page of replies: 1 credit per comment you expand.
The response includes a _meta object with credits_consumed, credits_remaining, and a split of lookup_credits versus og_reqs, so you can reconcile your own accounting without guessing. Decide the order of operations against your pack size: 100 free credits, then Starter at $9 for 10,000, Growth at $40 for 50,000, and Scale at $140 for 200,000. Purchased credits do not expire.
What people build with this
Sentiment analysis and topic mining
Pull the newest two or three comment pages, run them through a classifier, and you have a live read on how a launch was received. Storing pk lets you append new comments on each run instead of re-scraping the same thread.
Brand monitoring
Watch comments on your own posts and on competitor posts for product names, complaints, or support requests. Pair /v2/media/comments/infos with a schedule to poll only the posts whose comment_count changed — you skip pages you have already seen.
Influencer vetting
Before signing a creator, read the comments rather than the follower count. A comment thread full of generic praise from low-quality accounts reads very differently from one with real questions and replies. Comments on a creator’s last ten posts are a fast, cheap signal.
Audience research
Comment authors are a sample of an audience. Aggregate user.username, capture is_verified, and count repeat commenters to understand who actually engages.
Honest limitations
- Private accounts return nothing. If the account is private, the comments are not accessible. Check
is_privateon the profile before spending credits. - Page size is not guaranteed. The documented page is 15 comments, but do not code against that number. Always drive the loop off
next_page_id. - Deleted and hidden comments disappear. A comment present yesterday may be gone today. Persist what you collect if you need history.
- Replies are a separate, per-comment cost. There is no single call that returns a full thread with all replies.
- Engagement fields can be withheld.
like_countandhas_likedare nullable. Treat them as optional, not guaranteed. - Ordering is Instagram’s. Comment order is whatever Instagram returns, which may not be strictly chronological. Sort by
created_at_utcyourself if you need a timeline. - Respect the platform and the law. Only collect public data you have a lawful basis to process, and honour Instagram’s terms. Anti-automation walls are covered in the blocked and challenge guide.
Get 100 free API credits
Resolve a post, pull a page of comments, and inspect the JSON before you spend a cent. Starter is $9 for 10,000 non-expiring credits.
Create a free account →