Social media comments API for sentiment analysis
A social media comments API returns public Instagram, TikTok and YouTube comments as JSON, 1 credit per post. Merge them and score sentiment with an LLM.
By Scrape Everything team · · 11 min read
Last updated: October 2026
TL;DR: A social media comments API returns the public comments on a post or video as structured JSON, so you can analyze them without opening the app. With Scrape Everything, one POST request returns a post's top-level comments from Instagram (up to 15), TikTok (up to 100) or YouTube (up to 100) for 1 credit.
Merge the three responses into one table and send the text to an LLM for sentiment and themes. That's the whole pipeline. The catch: each platform returns different fields, orderings and limits, and if you ignore them your numbers go wrong without telling you.
Two different products share this name. Some comments APIs manage comments on accounts you own (reply, hide, delete) through OAuth. A read-only comments API like ours fetches public comments on any post, competitors' included. This guide covers the read-only kind, which is part of our social media scraper catalog.
Which comments API fits your job?
| You need to | Best fit | Why |
|---|---|---|
| Reply to, hide or moderate comments on your own accounts | The platform's official API, or a social media management tool | Write access needs OAuth to your account. A read-only scraper can't do it. |
| Every comment and reply on a YouTube video, at no cost | YouTube Data API commentThreads.list | Free daily quota, 1 quota unit per call, up to 100 threads per page |
| TikTok comments and replies for academic research | TikTok Research API | Application-based and limited to qualifying non-commercial research |
| Public comments on any post on Instagram, TikTok and YouTube with one key | Scrape Everything | 1 credit per request, same auth, response envelope and error codes everywhere |
What does each platform's comments endpoint return?
A comment scraper API is a service that fetches the public comments shown on a social post and returns them as typed fields (text, likes, time, author) instead of HTML. Reference pages: the Instagram comments scraper, the YouTube comments scraper and the TikTok scrapers.
| TikTok | YouTube | ||
|---|---|---|---|
| Endpoint | POST /v1/instagram/comments | POST /v1/tiktok/comments | POST /v1/youtube/comments |
| Input | post or reel url, or shortcode | video url or id | video url or id |
| Max comments per request | 15 (newest top-level only) | 100 (default 20) | 100 (default 20) |
| Order | Newest first | TikTok's relevance order | top or newest via sort |
| Replies included | No | No (reply_count only) | No (reply_count only) |
| Comment time | created_at, Unix seconds | created_at, ISO 8601 | published_text, relative ("2 months ago") |
| Comment likes | like_count | like_count | like_count parsed from rounded text, plus raw like_count_text ("8.4K") |
| Language field | No | language | No |
| Creator signals | None | is_author_liked, is_pinned | is_pinned, is_hearted, author.is_channel_owner |
| Total comment count | comment_count | comment_count (as TikTok reports it) | comment_count (exact) |
| Credits per request | 1 | 1 | 1 |
Four things in that table shape the pipeline.
Instagram's cap is hard. Logged-out Instagram only shows the 15 most recent top-level comments of a post, so limit maxes out at 15 and older comments aren't reachable. has_more tells you the post has more than you got.
YouTube gives you no absolute timestamp, just "3 weeks ago", and its comment like_count is the rounded figure parsed back into a number. Read 8,400 as "about 8.4K". TikTok sorts by relevance rather than time and hides some comments, so you can get fewer than comment_count even when you're under your limit.
No endpoint returns replies. If the fight happens in the replies, you get the reply_count, not the fight.
What about the official APIs?
If you only need one platform, look at them first. The YouTube Data API commentThreads.list method returns replies and an ISO 8601 publishedAt per comment, costs 1 quota unit per call and is free within your daily quota. It needs a Google Cloud project and credentials. TikTok's official comment data sits behind the TikTok Research API, which you apply for and which is limited to qualifying non-commercial research. On Instagram, Meta's official API is built around accounts you manage, not other people's posts; we compared the two routes in Instagram Graph API vs a scraper API. Our case is narrower: one key and one schema for three platforms.
How do I collect comments with one request per post?
Every endpoint is a POST with a JSON body and a Bearer key. Here's the YouTube comments scraper API from a terminal:
curl -X POST https://api.scrapeeverything.com/v1/youtube/comments \
-H "Authorization: Bearer $SCRAPEEVERYTHING_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "sort": "newest", "limit": 50}'A trimmed response (real field names, placeholder values):
{
"success": true,
"data": {
"video_id": "dQw4w9WgXcQ",
"sort": "newest",
"comment_count": 2400000,
"comments_fetched": 50,
"has_more": true,
"comments": [
{
"id": "UgxEXAMPLE0000000000",
"link": "https://www.youtube.com/watch?v=dQw4w9WgXcQ&lc=UgxEXAMPLE0000000000",
"text": "Bought the v2 after this. Battery is better, the app still logs me out.",
"author": {
"name": "@example_user",
"handle": "@example_user",
"is_channel_owner": false,
"is_verified": false
},
"like_count": 8400,
"like_count_text": "8.4K",
"reply_count": 12,
"published_text": "2 days ago",
"is_pinned": false,
"is_hearted": false
}
]
},
"meta": {
"request_id": "req_example",
"cached": false,
"credits_charged": 1,
"credits_remaining": 99
}
}Errors return success: false, a stable code and a message. Expect COMMENTS_DISABLED (404) on TikTok and YouTube videos with comments switched off, PRIVATE_ACCOUNT or AGE_RESTRICTED (404) on Instagram posts that exist but aren't public, and NOT_FOUND. Per the errors reference, a 404 is final for that target, so a retry is charged again for the same answer.
Two TikTok gotchas. Short share links (vm.tiktok.com/..., tiktok.com/t/...) aren't supported, so expand them to the full tiktok.com/@user/video/<id> URL first. And send id as a string, never a number: JavaScript rounds a 19-digit integer, and you'll quietly ask for the wrong video.
How do I normalize three platforms into one table?
This step is what makes cross-platform sentiment analysis of social media comments possible. Pick one schema and force everything into it. Ours:
| Column | Instagram source | TikTok source | YouTube source |
|---|---|---|---|
platform | constant | constant | constant |
post_id | shortcode | video_id | video_id |
comment_id | comments[].id | comments[].id | comments[].id |
text | text (empty for GIF-only) | text | text |
likes | like_count | like_count | like_count |
likes_exact | true | true | false |
replies | reply_count (often null) | reply_count | reply_count |
posted | created_at to ISO | created_at | published_text kept as text |
creator_engaged | false | is_author_liked or is_pinned | is_hearted or is_pinned |
author_hash | sha256 of author.username | sha256 of author.username | sha256 of author.handle |
We hash handles on purpose. You need to know that two comments came from the same person (one angry user posting six times isn't six angry users), not who that person is.
Here's the collector in Python. It runs with pip install requests and your key in SCRAPEEVERYTHING_API_KEY, and it appends to comments.csv so you can rerun it without losing or duplicating rows:
import csv
import hashlib
import os
from datetime import datetime, timezone
import requests
API = "https://api.scrapeeverything.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SCRAPEEVERYTHING_API_KEY']}"}
OUT = "comments.csv"
FIELDS = ["platform", "post_id", "comment_id", "text", "likes", "likes_exact",
"replies", "posted", "creator_engaged", "author_hash"]
POSTS = [
("instagram", {"url": "https://www.instagram.com/reel/DYxz0A8Kinw/", "limit": 15}),
("tiktok", {"url": "https://www.tiktok.com/@khaby.lame/video/7694335322922044703", "limit": 100}),
("youtube", {"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "sort": "newest", "limit": 100}),
]
def fetch(platform, body):
r = requests.post(f"{API}/{platform}/comments", headers=HEADERS, json=body, timeout=90)
try:
j = r.json()
except ValueError:
print(f"skip {platform}: HTTP {r.status_code}")
return None
if not j.get("success"):
print(f"skip {platform}: {j.get('code')} {j.get('message')}")
return None
return j["data"]
def h(value):
return hashlib.sha256(value.encode()).hexdigest()[:16] if value else None
def normalize(platform, data):
for c in data["comments"]:
a = c.get("author") or {}
if platform == "instagram":
ts = c.get("created_at")
posted = datetime.fromtimestamp(ts, timezone.utc).isoformat() if ts else None
post_id, handle, engaged, exact = data["shortcode"], a.get("username"), False, True
elif platform == "tiktok":
posted = c.get("created_at")
post_id, handle, exact = data["video_id"], a.get("username"), True
engaged = c.get("is_author_liked") or c.get("is_pinned")
else:
posted = c.get("published_text")
post_id, handle, exact = data["video_id"], a.get("handle"), False
engaged = c.get("is_hearted") or c.get("is_pinned")
yield {
"platform": platform,
"post_id": post_id,
"comment_id": c["id"],
"text": c.get("text") or "",
"likes": c.get("like_count"),
"likes_exact": exact,
"replies": c.get("reply_count"),
"posted": posted,
"creator_engaged": bool(engaged),
"author_hash": h(handle),
}
# Load keys from earlier runs so polling only adds new comments.
seen = set()
is_new_file = not os.path.exists(OUT)
if not is_new_file:
with open(OUT, newline="", encoding="utf-8") as f:
seen = {(r["platform"], r["comment_id"]) for r in csv.DictReader(f)}
rows = []
for platform, body in POSTS:
data = fetch(platform, body)
if not data:
continue
for row in normalize(platform, data):
key = (row["platform"], row["comment_id"])
if row["text"].strip() and key not in seen:
seen.add(key)
rows.append(row)
with open(OUT, "a", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=FIELDS)
if is_new_file:
w.writeheader()
w.writerows(rows)
print(f"{len(rows)} new comments from {len(POSTS)} posts")It drops empty text rows on purpose: Instagram GIF-only comments come back as an empty string plus a gif_url, and a model will happily invent a sentiment for nothing.
How do I get more than 15 Instagram comments?
You can't reach older ones, but you can poll. On launch day, rerun the collector for the same post every couple of hours; it keeps only new comment_ids. If all 15 comments in a poll are new, you probably missed some, so halve the interval. Don't set cache_days on polling calls, or you'll get the stored copy back instead of fresh comments.
Should I fetch the post metadata too?
Yes, once per post. POST /v1/youtube/video returns the title, exact view count and like count, POST /v1/tiktok/video returns the caption plus play, like, comment, share and save counts, and POST /v1/instagram/post returns the caption, like count and comment count. Each costs 1 credit. Put the title or caption into the LLM prompt, because "it finally works" means one thing under a bug-fix announcement and another under a teaser.
How do I run sentiment analysis on social media comments with an LLM?
You have three realistic options, and they trade cost for nuance:
| Approach | Example | Good at | Weak at |
|---|---|---|---|
| Lexicon and rules | VADER | Free, fast, tuned for social media text | Sarcasm, slang it hasn't seen, non-English text |
| Fine-tuned transformer | cardiffnlp/twitter-roberta-base-sentiment-latest on Hugging Face | Runs locally, positive/neutral/negative labels | No themes, no question or objection types |
| General-purpose LLM | Any current chat model with JSON output | Sentiment, theme and type in one pass, uses post context | Costs per token, needs a fixed label set |
We use an LLM for launch posts because the useful output is the themes: who's unhappy, and about what. "This is sick" is praise on TikTok and a complaint in a support ticket, and you need context to tell.
Batch 50 to 100 comments per call, ask for JSON and pass the post context. The prompt we start from:
You are analyzing public comments on a product launch post.
Post context: {platform} | "{title_or_caption}"
For each comment, return:
- id: the comment_id
- sentiment: positive | negative | neutral | mixed
- theme: 2-4 word label (e.g. "price too high", "battery life", "shipping delay")
- type: praise | complaint | question | objection | spam | other
- language: ISO 639-1 code (only TikTok returns one natively)
Rules:
- Judge sarcasm by context. "Great, another subscription" is negative.
- Emoji-only comments: label by the emoji, type "other".
- If you can't tell, use neutral. Don't guess.
Return a JSON array only.
Comments:
{id}\t{text}
...Then run a smaller call over the theme labels alone: "Merge these into at most 8 clusters and return a mapping." That turns "too pricey", "price is high" and "$$$" into one bar on a chart.
Before aggregating, drop everything typed spam. Likes act as agreement votes, so weighting by likes makes sense on TikTok and Instagram; on YouTube likes_exact is false, so rank by likes but don't do arithmetic with them. Compare platforms by share of comments, never raw counts: 15 Instagram comments against 100 YouTube comments only tells you about the sampling caps.
Can I do this without writing code?
Yes. The comment-sentiment-mining skill in our Agent Skills pack runs the whole loop from a prompt: it fetches the comments, then clusters them into sentiment, themes, questions and objections. Install the pack with:
npx skills add scrapeeverything/skillsThen ask your agent "What are people saying in the comments of these 5 launch videos?" and paste the links. Same endpoints, same limits.
If your agent speaks the Model Context Protocol (MCP), our MCP server (@scrapeeverything/mcp, listed in the official MCP Registry) exposes the endpoints as tools. n8n users can build the same flow with the n8n-nodes-scrapeeverything node. Full request and response schemas live in our OpenAPI spec at https://scrapeeverything.com/openapi.json.
What does a social media comments API cost per post?
Each comments request costs 1 credit on every platform. Failed requests are refunded automatically. A definitive answer, such as a private or missing post, is billed because the lookup completed. Cache hits are free: send cache_days (1 to 14) and a repeat request for the same target inside that window returns the stored result for 0 credits. Per the caching docs, the match is on the endpoint plus its normalized parameters, so keep limit and sort the same.
Cost per 1,000 comments on each pack, best case with full pages (a video with 40 comments still costs 1 credit):
| Platform | Credits per 1,000 comments | Builder ($1.56 / 1k credits) | Growth ($1.19) | Scale ($0.90) |
|---|---|---|---|---|
| YouTube, TikTok (100 per request) | 10 | $0.016 | $0.012 | $0.009 |
| Instagram (15 per request) | 67 | $0.10 | $0.08 | $0.06 |
A launch budget, 10 posts per platform:
| Task | Requests | Credits |
|---|---|---|
| Comments, 30 posts, one sweep | 30 | 30 |
| Post metadata, 30 posts | 30 | 30 |
| Instagram polling, 10 posts, every 2 hours for 24 hours | 120 | 120 |
That's 180 credits, and a rerun inside a cache_days window costs 0. The 100 free credits on signup cover the first sweep plus metadata. After that, credits come in one-time packs that never expire: Builder $39 for 25,000 credits, Growth $119 for 100,000 and Scale $449 for 500,000. The pricing page has the details, the credit calculator turns a post count into a pack, and our social media scraping API pricing comparison puts those numbers next to other providers.
Start with one real launch post per platform. Get 100 free credits, run the collector and read the CSV before writing any LLM code. The LLM bill is separate and depends on your model.
What are the limits and ethics of scraping comments?
Scrape Everything returns public data only: no logins, no private accounts. We're not affiliated with Instagram, TikTok or YouTube.
Public isn't anonymous, though. Comment authors are people, and under the EU General Data Protection Regulation personal data is any information relating to an identifiable person, which a username often is. What we recommend:
- Collect only the fields you need (for sentiment: text, likes, time and a hashed author), and hash or drop handles and avatar URLs before the data leaves your collector.
- Report aggregates, like the share of comments that mention price, not quotes tied to a person.
- Set a retention period and delete raw comments when the analysis is done.
- Read the platform's terms and talk to your counsel before any commercial use.
We cover the legal background (not legal advice) in Is scraping public social media data legal?. See also the researchers use case for reproducible datasets and analytics SaaS for building comment analytics into a product.
FAQ
Is there a single social media comments API for Instagram, TikTok and YouTube?
Yes. Scrape Everything has one comments endpoint per platform (/v1/instagram/comments, /v1/tiktok/comments, /v1/youtube/comments), all with the same auth, the same response envelope and the same error codes. Each costs 1 credit per request.
Do the comments APIs return replies or older comments?
No. All three return top-level comments only, with a reply_count per comment, and Instagram stops at the 15 newest. For replies, use the official YouTube Data API or, if you qualify, the TikTok Research API.
Is there a free comments API?
The YouTube Data API is free within its daily quota, and the TikTok Research API is free for approved non-commercial researchers. Scrape Everything gives you 100 free credits on signup with no payment details, which at 1 credit per comments request covers 100 posts.
Can I get Facebook or X comments?
Not today. Our catalog has comments endpoints for Instagram, TikTok and YouTube only. For X we return profiles and posts, and for Facebook public page profiles, but no comment threads.
What happens if comments are turned off?
TikTok and YouTube return a 404 with the code COMMENTS_DISABLED. A video that simply has no comments returns an empty list. Treat both as final and don't retry.