Bluesky Scraper: Posts, Profiles & Followers
Pricing
from $1.50 / 1,000 post scrapeds
Bluesky Scraper: Posts, Profiles & Followers
Scrape Bluesky without a login: profile posts, search results, full threads with replies, quotes, likers and reposters, profiles and followers. Image alt text, video links, verification, reply settings and hidden replies included. Export to CSV, Excel or JSON, or use the API.
Pricing
from $1.50 / 1,000 post scrapeds
Rating
0.0
(0)
Developer
CHEN-SHU-CHEN
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
What is Bluesky Scraper?
Bluesky's own app shows you one profile, one thread or one search at a time, and copies nothing out. This Actor reads the same public data in bulk and hands it back as a table you can filter, export or feed into your own tools.
Give it any mix of:
- Profiles (handles, DIDs or profile URLs): one profile row each with exact follower, follow and post counts, then the profile's recent posts, and optionally its followers and follows.
- Post URLs: the post itself, plus as much of its reply thread, its quote posts, and the accounts that liked and reposted it as you ask for.
- Search queries: posts matching a word, phrase, hashtag or Bluesky search operator, newest first or by Bluesky's Top ranking, with date and language filters.
No Bluesky account or app password is needed. Everything comes from Bluesky's public API, the same one the logged-out web app uses.
Because it runs on Apify, you can schedule it (track a hashtag every hour, a list of accounts every morning), call it through the API, connect it to Make, Zapier or Google Sheets, and export as CSV, Excel or JSON.
What you get that most Bluesky scrapers leave out
- Image alt text and dimensions for every image, and the video playlist (HLS) and thumbnail for every video, with its alt text and file size.
- Verification details: not just whether an account is verified, but who verified it (Bluesky, or a trusted verifier such as a newsroom) and when.
- Reply settings and hidden replies: who is allowed to reply to a post (everyone, nobody, followers, people mentioned, a list), and which replies its author hid. Thread replies come with
isHiddenReply. - Thread position:
opThreadPostIndex/opThreadPostCountwhen a post is part of its author's numbered thread (1/5, 2/5...). - Bridged posts: for accounts bridged in from Mastodon and the fediverse through Bridgy Fed, the original post URL and text, and which network the account comes from.
- Handle history (optional): every handle an account has used and since when, from the public PLC directory, plus the server that hosts it.
- Links, mentions and hashtags exactly as the author's app marked them, and the full link-card (title, description, thumbnail) of any shared link.
- Labels: the author's own content warnings, and labels set by moderation services, kept apart.
- Emails and links found in bios, for profiles and for every listed follower.
- Bookmark counts next to likes, reposts, replies and quotes.
How to scrape Bluesky
- Open the Input tab. Fill in any of Profiles, Post URLs and Search queries; you can use all three in one run.
- Set how much you want for each: posts per profile, replies, quotes, likers and reposters per post, posts per search. Anything left at 0 is skipped and costs nothing.
- Click Start. A profile with 100 posts takes about a second; a thread with a few hundred replies a few seconds.
- Open the Output tab, or export the dataset. Every row has a
type(post,profile,userorstatus), so a spreadsheet filter separates them.
Input example
{"profiles": ["bsky.app", "@jay.bsky.team"],"maxPostsPerProfile": 50,"includeReposts": false,"maxFollowersPerProfile": 200,"postUrls": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"],"maxRepliesPerPost": 100,"maxLikersPerPost": 100,"searchQueries": ["#buildinpublic", "\"open social\" lang:en"],"maxPostsPerSearch": 500,"searchSince": "7 days"}
Output
One dataset, four row types. Fields that do not apply to a row are absent or null. Unknown is null, never a made-up 0. Times are ISO-8601 as Bluesky sends them; counts are numbers.
Post rows (type: "post")
| Field | Notes |
|---|---|
url, uri, cid, postId | Web link, AT URI, content hash, record key |
author | did, handle, displayName, avatar, profileUrl, createdAt, verification (verifiedStatus, trustedVerifierStatus, verifiedBy with issuer handle and date), selfLabels, moderationLabels, bridgedFrom, pronouns |
text, langs, createdAt, indexedAt | |
likeCount, repostCount, replyCount, quoteCount, bookmarkCount | |
links, mentions, hashtags, tags | From the post's rich-text facets; mentions has DID and handle |
embedType | images, video, external (link card), quote, quoteWithMedia, record (a quoted feed, list or starter pack), or null |
images | url, thumbnail, alt, width, height, mimeType, sizeBytes |
video | playlistUrl (HLS .m3u8), thumbnail, alt, width, height, mimeType, sizeBytes, captionLanguages |
externalLink | url, title, description, thumbnail |
quotedPost | The quoted post with its own author, text, counts, media and alt text; status is not_found, blocked or detached when it cannot be shown |
quotedRecord | A quoted feed, list, starter pack or labeler |
selfLabels, moderationLabels | Content warnings set by the author, and labels from moderation services (with labeler and date) |
isReply, replyParentUri/Url/AuthorHandle, replyRootUri/Url/AuthorHandle | |
threadgate | whoCanReply, allowedRepliers, allowedLists, hiddenReplies, hiddenRepliesCount |
isHiddenReply, threadDepth | Thread replies only |
opThreadPostIndex, opThreadPostCount | Position in the author's own numbered thread, when Bluesky reports it |
isBridged, bridgyOriginalUrl, bridgyOriginalText | Bridged fediverse posts |
isRepost, repostedBy, repostedAt, repostUri, isPinned | Profile feeds |
source, position | authorFeed, search, post, reply or quote, and the rank within it |
profileHandle, searchQuery, searchHitsTotal, targetPostUrl | What the row was found through |
recordExtras | Any other fields the author's app stored on the post |
Profile rows (type: "profile")
did, handle, handleIsValid, displayName, profileUrl, description, descriptionLinks, descriptionEmails, descriptionMentions, descriptionHashtags, avatar, banner, followersCount, followsCount, postsCount, createdAt, indexedAt, verification, selfLabels, moderationLabels, loggedOutVisibilityDisabled, pinnedPostUri, pinnedPostUrl, listsCount, feedsCount, starterPacksCount, isLabeler, chatAllowIncoming, activitySubscriptionsAllowed, joinedViaStarterPack, liveStatus, pronouns, website, bridgedFrom, and with Handle history on, handleHistory ([{handle, since}]) and pdsEndpoint.
User rows (type: "user")
Followers, follows, likers and reposters: relation (follower, follow, liker, reposter), did, handle, displayName, profileUrl, description with its links and emails, avatar, createdAt, verification, labels, position, relatedToHandle or targetPostUrl, and likedAt for likers. The list endpoints do not carry counts; turn on Full profiles for listed accounts to get each as a profile row with counts instead.
Status rows (type: "status")
One for every input that returned nothing or stopped short, so 10 inputs in means 10 visible outcomes. INPUT_SUMMARY in the key-value store lists all of them, including the ones that worked.
status | Meaning | Charged |
|---|---|---|
not_found | No such profile or post (deleted, or the account is gone) | No |
hidden_by_user | The account asked Bluesky not to be shown to logged-out visitors (see below) | No |
unavailable | Account taken down, deactivated or suspended | No |
invalid_input | Not a handle, DID, profile URL or post URL | No |
no_results | The search matched nothing | No |
blocked | Bluesky refused the logged-out request (see search below) | No |
rate_limited, error, parse_error | Bluesky kept failing or answered with something unexpected | No |
parse_error on a post, profile or user row | One item in a page Bluesky sent without what such a row needs (an author, a valid URI); the rest of the page is unaffected | No |
partial | The input returned some rows, then the run stopped (max charge reached, timeout) | Only the rows delivered |
not_processed | The run stopped before reaching this input | No |
Output example
A post row from a real run on 2026-10-18, trimmed:
{"type": "post","url": "https://bsky.app/profile/bsky.app/post/3mxyyerrxak2m","author": {"handle": "bsky.app","displayName": "Bluesky","verification": { "verifiedStatus": "none", "trustedVerifierStatus": "valid", "verifiedBy": [] }},"text": "v1.135 is live!\n\nYou can now browse quote posts by Top in addition to Latest. ...","createdAt": "2026-10-16T16:23:26.498Z","likeCount": 1230,"repostCount": 194,"replyCount": 171,"quoteCount": 77,"bookmarkCount": 77,"embedType": "video","video": {"playlistUrl": "https://video.bsky.app/watch/did%3Aplc%3Az72i7hdynmk6r22z27h6tvur/bafkreiat76ppqns6bcnxnt6etzlyoar4o75v6wrzwjbh342oxbsebyun5e/playlist.m3u8","thumbnail": "https://video.bsky.app/watch/did%3Aplc%3Az72i7hdynmk6r22z27h6tvur/bafkreiat76ppqns6bcnxnt6etzlyoar4o75v6wrzwjbh342oxbsebyun5e/thumbnail.jpg","alt": "A video showing the Bluesky interface as a user clicks through to see quotes on an individual posts. ...","width": 1000,"height": 1000,"sizeBytes": 3850070},"threadgate": { "whoCanReply": "everyone", "hiddenRepliesCount": 2 }}
How much does it cost to scrape Bluesky?
You pay per row returned, nothing per minute, and nothing for inputs that fail.
| Event | What it is | Price | Per 1,000 |
|---|---|---|---|
post-scraped | One post row: from a profile, a search, a post URL, a thread reply or a quote | $0.0015 | $1.50 |
profile-scraped | One profile row with counts (input profiles, and listed accounts when Full profiles is on) | $0.0015 | $1.50 |
user-listed | One follower, follow, liker or reposter | $0.0005 | $0.50 |
handle-history | Handle history added to one profile row (only with Handle history on) | $0.001 | $1.00 |
Status rows are free. A profile with 100 posts costs $0.1515; the same profile with 1,000 followers listed adds $0.50. Apify's free plan ($5 a month) covers about 3,300 posts or 10,000 followers.
How do I cap what a run can spend?
Set a maximum charge for the run. Rows are written and charged together, one API page at a time, through Apify's own charging call: it writes only as many rows as the remaining budget covers and charges exactly those, so you are never charged for a row that is not in your dataset. When the budget runs out, the run stops, the input in progress is marked partial, and every input it did not reach gets a free not_processed row. Handle history is charged only for profile rows that were delivered with it; a row the budget cannot pay both for goes out without it. If Apify cannot record a charge, the run stops and does no further work; a failed charge is never sent a second time, so nothing is charged twice. The run's timeout is watched inside every page and retry too: close to it, the run stops reading and still writes the status rows.
Limits and honest caveats
- Search uses an undocumented endpoint. Bluesky's documented public API refuses post search without a login (403). The host Bluesky's own app uses,
api.bsky.app, currently answers the first page of a search logged out but refuses its next-page cursor. So "Latest" searches page backwards by date instead, which works for any number of posts; "Top" stops at 100 posts, and so does a query made of several hashtags alone (#art #ai): search them one per line instead. If Bluesky closes logged-out search, search inputs come backblockedand free, and profiles and post URLs keep working. - Threads come in Bluesky's own reply order, up to the depth you set. Very large threads are returned as far as Bluesky's API returns them in one answer.
- Likers, reposters, followers and follows are paged until the number you asked for or the end of the list, newest first.
- Logged-out data only. Nothing that needs an account: no "followed by people you know", no muted words, no feeds you subscribe to.
- Accounts that asked not to be shown to logged-out visitors are left out by default. Bluesky lets people set this (a
!no-unauthenticatedlabel); its web app then hides them from anyone signed out, although the API still returns them. This Actor follows the same choice: a profile or post URL of such an account comes back as a freehidden_by_userrow, and their posts, replies, quotes and list entries are skipped (a quoted post of theirs shows onlystatus: "hidden_by_user"and its link). Turn on Include accounts hidden from logged-out visitors to read them anyway; their rows then carryloggedOutVisibilityDisabled: true. - No proxy is needed or used by default. If a very large run meets Bluesky's per-IP rate limits, the Actor waits as long as Bluesky asks and retries; turning on Apify's datacenter proxy makes each retry come from a fresh IP.
What happens if Apify restarts a run?
Apify sometimes moves a running Actor to another server, which restarts it. The Actor saves its place when each input starts and ends, and whenever Apify signals a migration. After a restart it reads back the rows the interrupted input already put in the dataset and skips them, so they are not published or charged a second time, then carries on. Relative dates such as 7 days keep the value they had when the run first started.
FAQ
Is scraping Bluesky allowed?
This Actor reads only what Bluesky publishes to anyone without an account, through Bluesky's public API, at a modest rate. Bluesky's data is public by design: every post and profile lives in an open network. Accounts that asked not to be shown to logged-out visitors are left out unless you opt in. You are still responsible for how you use what you collect, including personal data rules such as GDPR.
Do I need a Bluesky account or app password?
No.
Can I monitor a hashtag or keyword over time?
Yes. Save the input as a task with a search query, set Posted after to a relative value such as 1 day, and schedule it daily. Each run returns the posts of the last day.