How to export Reddit comments to CSV or JSON
Export Reddit comments from any thread to CSV or JSON: fetch the thread, page through comments, rebuild replies with parentId and write both files. Script included.
Piyushh · October 14, 2026 · 5 min read
To export Reddit comments, fetch the thread through an API that returns its comments, keep requesting pages until none are left, then write one row per comment to CSV and the nested tree to JSON. Replies are the tricky part. APIs return comments as a flat list, so you rebuild the conversation from each comment's parentId. Below is a complete Python script that does all four steps, plus the output from running it on a real 186-comment thread on October 14, 2026.
If you only need one thread and don't want to write code, paste the URL into the free Reddit comment exporter. It shows the first 25 comments without an account, and once you sign in you can export the thread to CSV, JSON or PDF.
The fastest way to export Reddit comments
There are three ways to do it, listed roughly from least to most effort:
| Option | Setup | Good for |
|---|---|---|
| Comment exporter tool | Free preview; sign in to export | One thread, now |
| A hosted API plus the script below | An API key | Many threads, scheduled jobs, pipelines |
PRAW with replace_more(limit=None) | Approved Reddit API credentials | Python projects that already have keys |
If you have credentials, PRAW works. Its comment docs explain that replace_more loads hidden "load more comments" stubs, and "each replacement requires one network request." Since November 2025, new credentials need Reddit's approval, and Reddit stops accepting new requests on October 31, 2026. Our PRAW vs hosted API comparison covers that route. The other old standby is copying from the browser. Unauthenticated .json scraping stopped working in May 2026 (why).
This guide uses ChatterAPI's GET /v1/reddit/threads/{id}, which returns a post and its comments without a Reddit developer account. ChatterAPI is independent and not affiliated with Reddit.
What an export contains
Every comment comes back with the fields below, and you'll want most of them in your file:
| Field | Keep it? | Why |
|---|---|---|
id | Yes | Primary key; replies point at it |
parentId | Yes | The comment this one replies to, or null for a top-level comment |
author | Yes | Empty string when the account is deleted |
body | Yes | Comment text; empty when deleted or removed |
score | Yes | Upvotes minus downvotes as Reddit reports it |
createdAt | Yes | ISO 8601 UTC |
url | Yes | Link back to the comment |
bodyTruncated | Check | True when the text is a preview from a fallback source |
The thread response also includes two counts worth logging: reportedCommentCount (the comment count Reddit reports for the post) and unloadedReplyStubs (replies Reddit didn't load on this page).
Step 1: fetch the thread
One call returns the post and the first page of comments. The id can be the post id or the full post URL:
curl "https://api.chatterapi.com/v1/reddit/threads/1wdihhj" \
-H "Authorization: Bearer $CHATTERAPI_KEY"
Here is the trimmed response, a recorded sample from a test key (test keys return one real response per endpoint for free):
{
"data": {
"post": {
"id": "1wdihhj",
"title": "What CRM should a small business use?",
"author": "NotedWhisky",
"subreddit": "CRM",
"score": 52,
"numComments": 186
},
"comments": [
{
"id": "p9636y0",
"parentId": null,
"author": "OstrichSome3412",
"body": "Just a FYI, don’t buy for the company you imagine having in 5 years. Buy for the...",
"score": 6,
"createdAt": "2026-09-11T15:02:24.000Z"
},
{
"id": "p963opq",
"parentId": "p9636y0",
"author": "NotedWhisky",
"body": "Okay that’s probably the balance I need tbf. Enough room for revenue to grow...",
"score": 1,
"createdAt": "2026-09-11T15:04:37.000Z"
}
],
"unloadedReplyStubs": 0,
"reportedCommentCount": 186
},
"page": { "nextCursor": null, "hasMore": false },
"credits": { "charged": 0, "remaining": 719 }
}
The second comment is OP replying to the first, and parentId shows that. On a live key each page costs 3 credits.
Step 2: page through the comments
Each page holds up to about 100 comments. When page.hasMore is true, pass page.nextCursor back as cursor. Cursors are opaque strings like "AQx3Lr9dK2vQ…", so pass them through untouched. Store comments in a dict keyed by id, and a comment that appears on two pages is kept only once.
def fetch_thread(post_id):
"""Return the post and every comment the API loads, across all pages."""
post, comments, cursor = None, {}, None
while True:
params = {"cursor": cursor} if cursor else {}
r = requests.get(f"{BASE}/reddit/threads/{post_id}", headers=HEADERS, params=params, timeout=60)
r.raise_for_status()
body = r.json()
data = body["data"]
post = post or data["post"]
for c in data["comments"]:
comments[c["id"]] = c # dedupe across pages
if not body["page"]["hasMore"]:
return post, list(comments.values()), data["reportedCommentCount"]
cursor = body["page"]["nextCursor"]
Step 3: rebuild the tree with parentId
Comments come back as a flat list. To rebuild the reply tree, index them by id and attach each one to its parent. A null parentId marks a top-level comment. The endpoint docs describe parentId as a fullname, but the responses we got carried bare ids like p9636y0. The helper below accepts both and treats a post id (t3_) as top level:
def parent_key(c):
"""Normalise parentId: None or a post id means top level."""
p = c.get("parentId")
if not p or p.startswith("t3_"):
return None
return p.removeprefix("t1_")
def build_tree(comments):
by_id = {c["id"]: {**c, "replies": []} for c in comments}
roots = []
for c in by_id.values():
parent = by_id.get(parent_key(c))
(parent["replies"] if parent else roots).append(c)
return by_id, roots
def depth(c, by_id):
d, p = 0, parent_key(c)
while p in by_id:
d, p = d + 1, parent_key(by_id[p])
return d
If a reply's parent wasn't loaded, by_id.get returns None and the reply goes to the top level instead of disappearing. For an export that's the safe default, because you keep every comment and can see where the gap is.
Step 4: write CSV and JSON
CSV gives you one flat row per comment, the shape spreadsheets, pandas and SQL loaders expect. JSON keeps the nesting, so use it when you want to read a conversation or feed one to an LLM.
import csv
import json
import os
import sys
import requests
BASE = "https://api.chatterapi.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CHATTERAPI_KEY']}"}
# fetch_thread, parent_key, build_tree and depth from above go here
def main(post_id):
post, comments, reported = fetch_thread(post_id)
by_id, roots = build_tree(comments)
with open(f"{post_id}.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["id", "parent_id", "depth", "author", "score", "created_at", "body", "url"])
for c in sorted(comments, key=lambda c: c["createdAt"]):
w.writerow([c["id"], parent_key(c) or "", depth(c, by_id), c["author"] or "[deleted]",
c["score"], c["createdAt"], c["body"], c["url"]])
with open(f"{post_id}.json", "w", encoding="utf-8") as f:
json.dump({"post": post, "comments": roots}, f, ensure_ascii=False, indent=2)
print(f"{len(comments)} comments exported ({len(roots)} top level); Reddit reports {reported}")
if __name__ == "__main__":
main(sys.argv[1])
We ran it with python export_thread.py 1wdihhj against the r/CRM thread above. It printed:
101 comments exported (71 top level); Reddit reports 186
The first rows of the CSV:
id,parent_id,depth,author,score,created_at,body,url
p961pv9,,0,Prestigious-Swim69,3,2026-09-11T14:55:54.000Z,"I don't really have a recommendation but you should prioritize how easy the CRM is to keep useful...",https://www.reddit.com/r/CRM/comments/1wdihhj/what_crm_should_a_small_business_use/p961pv9/
p962kyb,,0,[deleted],5,2026-09-11T14:59:43.000Z,,https://www.reddit.com/r/CRM/comments/1wdihhj/what_crm_should_a_small_business_use/p962kyb/
Of the 101 comments, 4 were deleted (empty author and body), and the deepest reply chain went 5 levels down. On a live key the script costs 3 credits per page, so this thread cost 3 credits.
Edge cases
Don't expect the count to match. Reddit said 186 comments and we got 101. Reddit's number can include deleted and removed comments, and Reddit hides deep replies behind "load more" stubs. The endpoint docs say it plainly: "no thread API returns every comment on very large threads." Log len(comments) next to reportedCommentCount so whoever reads the file knows what's in it.
A deleted comment still has structure. It keeps its id and its place in the tree, and its replies still point at it. Keep the row with [deleted] as the author instead of dropping it, or its replies will look orphaned.
Reddit comments are full of emoji and curly quotes, which Excel can mangle. If you see garbled characters, write the CSV with encoding="utf-8-sig" so Excel detects UTF-8.
Comment bodies contain line breaks. Python's csv module quotes those fields correctly, and a CSV hand-built with string joins won't.
To export every thread that mentions a keyword, run GET /v1/reddit/search first and loop the ids through main. Our Reddit search API guide covers the query side. If you only want comments, without the thread around them, comment search is cheaper at 2 credits per page.
For agents, the same call is the reddit_thread tool on ChatterAPI's MCP server at https://api.chatterapi.com/mcp, so Claude or Cursor can pull a thread and read the JSON tree directly.
People export threads for everything from sentiment work to sports trivia. One r/CFB user scraped every game thread comment for a season to see which fanbase swears the most. The script above is the boring part of projects like that, and it should stay boring.
Export your first thread
Pick a thread you care about and run the script, or paste it into the comment exporter first to see the shape. Sign up with Google to get 100 free credits without a card, enough for about 33 thread pages. Plans start at $19 a month for 25,000 credits. For background on Reddit's own API and its limits, see the Reddit API guide.