How to export Bluesky posts to CSV (any public profile)
Bluesky's public API serves every public profile's posts as JSON, without logging in. Here is how to turn them into a spreadsheet.
1. One request
curl "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=bsky.app&limit=100"
actor is a handle (bsky.app, nytimes.com) or a DID. Each item in feed has a post with record.text, record.createdAt, likeCount, repostCount, replyCount and quoteCount. limit is at most 100; the response's cursor fetches the next page. filter can be posts_with_replies, posts_no_replies or posts_with_media.
Watch out: the feed also contains the profile's reposts of other people's posts and its pinned post. Those items carry a reason key. In our test the very first item was a repost of another account's post, so skip items with reason if you want only the profile's own posts, once each.
2. All posts to CSV (Python, standard library only)
import csv, json, time, urllib.parse, urllib.request
HANDLE = "bsky.app"
API = "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed"
rows, cursor = [], None
while True:
params = {"actor": HANDLE, "limit": 100, "filter": "posts_with_replies"}
if cursor:
params["cursor"] = cursor
data = json.load(urllib.request.urlopen(API + "?" + urllib.parse.urlencode(params)))
for item in data["feed"]:
if "reason" in item: # a repost or the pinned post: skip
continue
p = item["post"]
rows.append({
"created_at": p["record"]["createdAt"],
"text": p["record"].get("text", ""),
"likes": p.get("likeCount", 0),
"reposts": p.get("repostCount", 0),
"replies": p.get("replyCount", 0),
"quotes": p.get("quoteCount", 0),
"is_reply": "reply" in p["record"],
"url": "https://bsky.app/profile/%s/post/%s" % (p["author"]["handle"], p["uri"].split("/")[-1]),
})
cursor = data.get("cursor")
if not cursor or not data["feed"]:
break
time.sleep(0.5)
with open("posts.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0]))
w.writeheader()
w.writerows(rows)
print(len(rows), "posts")
For @bsky.app this wrote 865 rows in about 12 seconds, the same number as the profile's post count. The CSV is UTF-8: in Excel, open it with Data → From Text/CSV so emoji and accents display correctly.
3. Replies to one post
curl "https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mwolmfws5k2r&depth=10"
The uri is the post's at:// address: the author's DID plus the last part of the bsky.app post link (resolve a handle to its DID with com.atproto.identity.resolveHandle?handle=…). thread.replies nests replies up to depth levels. Expect slightly fewer replies than replyCount: in our test 66 of 68 came back, as deleted or hidden replies are left out.
4. Your own full account backup
For an archive of your own account (posts, likes, follows), use Settings → Account → Export my data in the Bluesky app. It downloads a .car file, the raw repository format. The same file is public for any account: com.atproto.sync.getRepo?did=… on the account's PDS host. It is complete, but needs an AT Protocol library to read, so the CSV route above is easier for analysis.
Many profiles or threads, without code
Our Bluesky Posts Scraper on Apify takes handles, profile links or post links and returns one flat row per post (text, counts, links, media, reply/repost flags), with a date cutoff and optional reposts, as CSV, Excel or JSON. Billed per post.
Frequently asked questions
Do I need a Bluesky account or app password to export posts?
Not for public profiles. public.api.bsky.app answers getAuthorFeed and getPostThread without logging in.
Why do I get posts from other accounts in the export?
The author feed includes reposts and the pinned post, marked with a 'reason' key. Skip those items to keep only the profile's own posts.
Is there a limit on how far back I can go?
Cursor paging reaches the profile's first post. Pause between requests on large profiles, as the API rate-limits heavy use.
Can I export Bluesky search results?
Not through the public endpoint: in our test (2026-10-01) app.bsky.feed.searchPosts on public.api.bsky.app answered 403, while profile feeds and threads worked without logging in.
Written with AI assistance. The Python export, the thread request and the repository download were run for @bsky.app on 2026-10-01 (865 rows, matching the profile's post count). The Apify tool is ours; the APIs are free to call directly.