Bluesky Scraper — Profiles, Posts & Followers
Pricing
from $0.25 / 1,000 item returneds
Bluesky Scraper — Profiles, Posts & Followers
Scrape Bluesky as data. One row per profile, post, reply, follower, follow or like: text, engagement counts, images, links, mentions and quoted posts. Search the account directory, walk a thread, list who liked a post. No API key, no login, no app password.
Pricing
from $0.25 / 1,000 item returneds
Rating
0.0
(0)
Developer
Insight Solutions
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
13 hours ago
Last modified
Categories
Share
Bluesky Scraper — Posts, Profiles & Followers
Get Bluesky as data. Give this Actor a list of handles and get back one row per post: the text exactly as written, exact like, repost, reply and quote counts, the images with their alt text, the links, the mentions, and what it quotes. Ask for followers or follows and each is its own row. Paste a post URL and get the whole thread flattened, plus everyone who liked it.
No API key. No login. No app password. No cookies to paste. Bluesky's public AppView answers anonymously, and this Actor never asks it for anything else. $0.40 per 1,000 rows, proxy included, accounts that do not exist are free, and a run that returns nothing costs nothing at all.
Try it in 30 seconds
{"profiles": ["bsky.app", "https://bsky.app/profile/pfrazee.com"],"maxPostsPerProfile": 50,"feedFilter": "posts_no_replies"}
A bare handle and a profile URL — both work, and so do DIDs.
What comes back
{"ok": true,"rowType": "post", // profile | post | reply | follower | follow | like | repost | diagnostic"uri": "at://did:plc:xb2urvqt5f4zzccjs46hysbf/app.bsky.feed.post/3muplmkx56s2k","cid": "bafyreidu2rxt55wzm3wmadbp6fzfrik6idvmh34qk23gwsssx6fqdgxbqm","url": "https://bsky.app/profile/cee.wtf/post/3muplmkx56s2k","authorDid": "did:plc:xb2urvqt5f4zzccjs46hysbf","authorHandle": "cee.wtf","authorDisplayName": "cee","text": "throng.cee.wtf i've invented a new way to browse bluesky","createdAt": "2026-09-04T18:25:36.591Z", // when the author posted it"indexedAt": "2026-09-04T18:26:09.958Z", // when Bluesky first saw it"langs": ["en"],"likeCount": 7993, // exact — Bluesky publishes integers, not "8K""repostCount": 1883,"replyCount": 342,"quoteCount": 625,"isReply": false,"parentUri": null,"rootUri": null,"isRepost": true, // it is in this feed because bsky.app reposted it"repostedByHandle": "bsky.app","profileHandle": "bsky.app", // whose feed this row came from"embedType": "video", // images | video | external | record | recordWithMedia"images": [],"videoUrl": "https://video.bsky.app/watch/…/playlist.m3u8","externalLink": null,"quotedPostUri": null,"links": ["https://throng.cee.wtf"],"mentions": [],"tags": [],"labels": [],"position": 2,"error": null,"errorType": null,"scrapedAt": "2026-09-09T15:26:31.402Z","source": "bsky.app","sourceUrl": "https://bsky.app/profile/bsky.app"}
Every row carries the same keys, whatever it describes. A profile row fills did, handle, displayName, description, followersCount, followsCount, postsCount, isVerified and leaves the post columns null. A follower, follow, like or repost row is that account plus subjectDid or subjectUri, naming what it is a follower of or a like on.
Use cases
- Audience and competitor research — what an account posts, what lands, and who follows it.
likeCountandrepostCountare exact integers, not rounded display strings, so they aggregate honestly. - Lead lists from engagement — the accounts that liked one specific post are a warmer list than any follower dump.
includeLikesgives you them with handles, display names and bios. - Sentiment and topic analysis — one flat array of text with
langs,linksandmentionsalready split out, ready to hand to a model. - Thread and conversation mining — one post URL returns the whole reply tree flattened, with
depthandparentUri, six generations deep. - Community mapping —
includeFollowson a set of seed accounts gives you the edges of a graph in one run. - Brand and mention monitoring — walk the profiles you care about on a schedule and diff on
cid, which changes when a post is edited. - Alt-text and accessibility auditing —
images[].altis the author's own alt text, and Bluesky's culture of writing it makes this unusually rich.
How it works, and why it keeps working
Bluesky is not a walled garden. It runs on AT Protocol, and its public AppView at public.api.bsky.app serves every public read without a token — no OAuth, no app password, no session. That is a deliberate design decision by Bluesky, not a hole, which is why this Actor reads it directly rather than driving a browser.
| Step | Request | What it gets |
|---|---|---|
| Profile | app.bsky.actor.getProfile | The account: bio, avatar, banner, exact follower / following / post counts, verification, creation date |
| Posts | app.bsky.feed.getAuthorFeed | Up to 100 posts a page, with the filter you chose, paged on a cursor |
| Followers | app.bsky.graph.getFollowers / getFollows | One page of accounts at a time, paged the same way |
| Search | app.bsky.actor.searchActors | The account directory, ranked |
| Thread | app.bsky.feed.getPostThread | The post and its whole reply tree in one request, six generations deep |
| Likes | app.bsky.feed.getLikes / getRepostedBy | Who engaged with one post |
| Resolve | com.atproto.identity.resolveHandle | A handle to a DID, only when a post URL needs it |
Two shapes in that data are worth knowing about, because they are where cheaper scrapers go wrong:
- A post is published twice in the same object. The author's own record holds the text, the timestamp and the rich-text facets; the AppView's view of it holds the CDN URLs. A record's image is a content hash, not a link — only the view has one. So the text comes from the record and the media comes from the view, and reading either half alone gets you blob hashes or missing text.
- Links, mentions and hashtags are not in the text. The text is plain. They arrive as facets: byte ranges over the UTF-8 encoding of that text, plus a feature. A mention's feature carries only the DID, so the handle a person recognises has to be sliced out of the text at those byte offsets — byte offsets, which one emoji earlier in the post moves by four. Get that wrong and every mention in a post with an emoji comes back truncated.
Underneath: Apify proxy, one pinned session per parallel worker. When Bluesky refuses an exit IP — HTTP 429, HTTP 403, an HTML page where JSON belongs — that session is retired and the same page is asked for once more from a different address. Retrying on an address that was just refused only deepens the block, so it is never done. If the second address is refused too, the walk stops, keeps every row it already delivered, and files one free blocked row saying where it stopped.
Pages of the same entry are spaced 250–600 ms apart. Nothing forces that; it is the difference between reading a profile and hammering one.
How it compares
- No login, ever. No app password, no OAuth, no session token. The endpoints this reads are the anonymous public ones, so there is no account of yours to get rate-limited, suspended, or leaked.
- Datacenter proxy, not residential. Bluesky's AppView answers a datacenter address exactly as it answers a residential one — we captured the same profile through both and got byte-identical responses apart from the follower counter ticking. That is why this costs $0.40 per 1,000 rather than the $1–2 a residential-only scraper has to charge.
- Exact counts. Bluesky publishes real integers for likes, reposts, replies and quotes. Nothing in this dataset is expanded from "8K".
- Threads are rows, not a nested blob. One row per reply with
depth,parentUriandrootUri, so a spreadsheet or a SQL table holds the conversation without a JSON parser. - Failures are free and legible. A handle that does not exist, a deleted post, an empty search, a block — each is a diagnostic row with an
errorTypeyou can branch on, and no charge. A run that returns nothing at all finishes FAILED with the reason in its status message, never a green run containing an apology. - A partial walk is kept, not thrown away. Hit
maxRunSecsor your charge ceiling on page 12 and you keep pages 1–11.
Input reference
Any combination of the three input kinds runs in one job.
| Field | Type | Default | What it does |
|---|---|---|---|
profiles | array of strings | prefilled with bsky.app | Handles (bsky.app, @nasa.gov), DIDs, or profile URLs. Duplicates are read, and billed, once |
includePosts | boolean | true | Return each profile's posts. Off gives profile rows only — one request per account |
maxPostsPerProfile | integer | 100 | Posts per profile. 0 = the whole feed |
feedFilter | enum | posts_no_replies | posts_no_replies, posts_with_replies, posts_with_media or posts_and_author_threads |
includeFollowers | boolean | false | One follower row per account following each profile |
includeFollows | boolean | false | One follow row per account each profile follows |
maxFollowsPerProfile | integer | 100 | The cap on each of those two lists, per profile. 0 = everything |
profileSearchQueries | array of strings | [] | Search the account directory. One profile row per match |
maxResultsPerQuery | integer | 50 | Accounts per query. 0 = everything the directory returns |
postUrls | array of strings | [] | https://bsky.app/profile/<handle>/post/<id> URLs or at:// URIs |
maxRepliesPerPost | integer | 100 | Replies per post. 0 = the whole thread, six generations deep |
includeLikes | boolean | false | One like row per account that liked each post, with the time of the like |
includeReposts | boolean | false | One repost row per account that reposted each post |
maxEngagementPerPost | integer | 100 | The cap on each of those two lists, per post. 0 = everything |
maxConcurrency | integer | 4 | Entries walked in parallel, each with its own proxy session |
maxRunSecs | integer | 240 | Whole-run wall-clock budget. What is already written is kept and billed |
proxyConfiguration | object | Apify datacenter | Leave it alone unless you start seeing blocked rows |
Output reference
Every row carries the same keys. ok: true is data; ok: false is a free diagnostic row.
| Field | What it is |
|---|---|
rowType | profile, post, reply, follower, follow, like, repost or diagnostic |
input, query, position | The entry this row came from, the search that produced it, and where it fell in the order |
url | The bsky.app page for this profile or post |
did, handle, displayName, description, avatarUrl, bannerUrl | Account rows: who they are |
followersCount, followsCount, postsCount, isVerified | Account rows, from profiles only — Bluesky's list endpoints publish no counts |
uri, cid | Post rows: the AT URI, and the content hash that changes when a post is edited |
authorDid, authorHandle, authorDisplayName | Who wrote the post. On a repost, the original author |
text, createdAt, indexedAt, langs | The post, when it was written, when Bluesky saw it, what languages it declares |
likeCount, repostCount, replyCount, quoteCount | Exact integers |
isReply, parentUri, rootUri, depth | Where this post sits in its thread |
isRepost, repostedByHandle, profileHandle | Whether a repost put it in the feed, who reposted it, and whose feed it came from |
embedType, images, videoUrl, externalLink, quotedPostUri | What is attached: images with alt text, an HLS video playlist, a link card, a quoted post |
links, mentions, tags | Rich-text facets, split out |
subjectDid, subjectUri | Relationship rows: the account followed, or the post liked |
labels | Moderation label values on the account or the post |
ok, error, errorType | Whether this row is data, and if not, why not |
scrapedAt, source, sourceUrl | When, and from where |
errorType on a diagnostic row is one of:
| Value | Meaning | Charged? |
|---|---|---|
not-found | No such account, or a post that was deleted or is behind a block | No |
no-results | The search matched no accounts | No |
blocked | Bluesky refused our requests from two different exit IPs. Rows already returned for that entry are kept | No |
invalid-input | The entry was not a handle, DID, profile URL, post URL or query | No |
timeout | The run's maxRunSecs budget ran out before this entry was reached | No |
unavailable | An unexpected failure | No |
Pricing
$0.40 per 1,000 rows. Pay-per-event, with the proxy already inside that number — there is no separate proxy line on your bill for this Actor.
| Event | What triggers it | FREE | Starter | Scale | Business |
|---|---|---|---|---|---|
| Item returned (primary) | One profile, post, reply, follower, follow, like or repost row written to your dataset | $0.0004 | $0.0004 | $0.0003 | $0.00025 |
| Run started | Once per run, after the first row | $0.001 | $0.001 | $0.001 | $0.001 |
Worked example. 100 profiles at 50 posts each, of which 3 handles no longer exist:
- 97 profile rows + 97 × 50 posts = 4,947 rows × $0.0004 = $1.979
- 1 run start = $0.001
- 3 handles that do not exist = $0.00
- Total: $1.98
What you are never charged for: an account that does not exist, a deleted post, a search that matched nothing, an entry that was not a handle or a URL, an entry the run never reached before maxRunSecs, or a page Bluesky blocked. If a whole run comes back empty it finishes FAILED and bills nothing at all, start fee included.
Set ACTOR_MAX_TOTAL_CHARGE_USD on a run and the Actor stops walking once the ceiling is in sight, rather than handing you rows it cannot bill or billing you for rows it cannot hand over. It finishes SUCCEEDED with the ceiling named in its status message, and everything already delivered is yours.
Limits, and the ones that might bite
Keyword post search is not available. Bluesky's post-search endpoint answers every non-browser client with an HTML "403 Forbidden" page rather than results — we verified this on 2026-09-09 through residential proxy, through datacenter proxy and from an unproxied connection, and got the same page all three times. So this Actor does not offer it, rather than shipping a feature that returns an error row every time. What to do instead: search the account directory with profileSearchQueries to find the accounts in your topic, then read their feeds with profiles — usually better targeted than a keyword sweep anyway. For one known conversation, postUrls returns its whole thread. Bluesky also publishes a public firehose, which is the right tool if you genuinely need every post matching a term as it happens.
Follower and follow rows carry no counts. Bluesky's list endpoints return a lighter profile object than getProfile does: handle, display name, bio, avatar, labels, and nothing more. followersCount, followsCount, postsCount and bannerUrl are null on follower, follow, like, repost and search rows. Getting them means asking for each account by name, which is one request per account and a different job.
No timestamp for a follow or a repost. getFollowers, getFollows and getRepostedBy publish who, not when. Those rows carry the account's own creation date in createdAt instead, which is documented per row type in the dataset schema. like rows are the exception — getLikes does publish the moment of the like, and that is what the column holds there.
Threads reach six generations. One getPostThread request returns the post and its replies six levels deep, which covers essentially every real conversation. A reply below that is not returned and is not counted.
Deleted and blocked posts inside a thread are skipped. Bluesky substitutes a placeholder carrying only a URI. Those are counted and dropped rather than emitted as rows full of nulls, and anything below them is unreachable.
Content you can only see signed in. Accounts that opt out of the logged-out view carry the !no-unauthenticated label and this Actor returns what the AppView gives an anonymous reader. Direct messages, muted words and moderation queues need an account. This Actor does not log in, does not accept an app password and does not take a session token, and it never will — that is a deliberate line, not a missing feature.
Bluesky may change the format. This reads a public API that Bluesky versions but does not freeze. When a shape changes, rows stop arriving and you get free blocked or unavailable diagnostic rows rather than quietly wrong data, and a run that returns nothing bills nothing.
Rate and reliability. Requests go out through Apify proxy with per-worker sessions, one rotation per block, and a 250–600 ms pause between pages of the same entry. Four entries in parallel is the default because it is where throughput and rate-limit headroom balance; raising maxConcurrency speeds a long list up and makes 429s more likely.
Use it from an AI agent, or from code
One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The Integrations tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.
curl -X POST "https://api.apify.com/v2/acts/insight.solutions~bluesky-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"profiles":["bsky.app"],"maxPostsPerProfile":50}'
# pip install apify-clientfrom apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("insight.solutions/bluesky-scraper").call(run_input={"profiles": ["bsky.app", "pfrazee.com"],"maxPostsPerProfile": 100,"feedFilter": "posts_no_replies","postUrls": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"],"includeLikes": True,})for row in client.dataset(run["defaultDatasetId"]).iterate_items():if not row.get("ok"):print("skipped:", row["input"], row["errorType"])elif row["rowType"] == "post":print(f'{row["authorHandle"]} ({row["likeCount"]} likes): {row["text"][:80]}')elif row["rowType"] == "profile":print(f'{row["handle"]}: {row["followersCount"]} followers')
Turn includePosts off when you only need to enrich a list of handles — one request per account, and the profile row carries the exact follower count.
FAQ
Do I need a Bluesky account or an app password? No. Bluesky's AppView serves public reads to anyone, and this Actor uses nothing else. There is no account of yours involved at any point.
Can I search for posts containing a keyword? Not through this Actor. Bluesky's post-search endpoint refuses non-browser clients outright — see the first item under Limits, and the two things to do instead.
Why is this cheaper than most social scrapers? Because Bluesky lets us read it properly. There is no browser to drive, no login wall to work around and no need for residential proxy, so the cost per row is genuinely lower and the price reflects that rather than the market rate for the same data elsewhere.
How many posts can I get from one account?
As many as it has. Set maxPostsPerProfile: 0 and raise maxRunSecs. Bluesky serves 100 per page with a pause between pages, so a 10,000-post account is about a hundred requests — plan for minutes, and bound it with ACTOR_MAX_TOTAL_CHARGE_USD if you are not sure what you are asking for.
What is a DID, and should I use one?
It is the identifier behind a handle — did:plc:z72i7hdynmk6r22z27h6tvur is bsky.app. Handles are domain names and can change; DIDs never do. Use the did column as your join key, and feed DIDs back in as input when you are tracking an account over time.
Are followers and replies charged?
Yes, at the same rate as posts — each is a row. That is why includeFollowers, includeFollows, includeLikes and includeReposts are all off by default and every one of them is capped.
What happens if one entry fails?
The others still run. The failed one produces a free diagnostic row and the run finishes SUCCEEDED. If every entry fails, the run finishes FAILED and you are billed nothing at all.
Is the data fresh? Live. Every run reads Bluesky at that moment; nothing is cached.
Legal and data-protection notes
- Public data only. Every source is a public Bluesky endpoint served to anonymous readers. The Actor never logs in, never accepts an app password or session token, never takes an API key belonging to anyone else, and never touches direct messages or anything behind an account.
- Posts and profiles are personal data in most jurisdictions. A row carries a display name, a handle, a bio, an avatar and a DID, and under the GDPR and similar laws that is personal data about an identifiable person. You are the controller of whatever you collect: have a lawful basis, keep only what you need, honour deletion requests, and remember that a post deleted on Bluesky stays in your dataset until you remove it.
includePosts: falseand the four engagement switches exist so that analysis which does not need a category of data does not carry it. - Posts are their authors' words. Republishing them, or training on them, is your call and your responsibility, subject to Bluesky's terms and to the law where you operate. Aggregation, sentiment analysis and quotation are the ordinary uses and are what this is built for.
- Moderation labels are carried, not applied. The
labelscolumn passes through what Bluesky's own labellers published. It is not a judgement by this Actor, and it is not complete — labelling is a service a reader subscribes to, and an anonymous read sees only the defaults. - Not affiliated with Bluesky Social PBC, with the AT Protocol project, or with any account whose content you retrieve. All product names and trademarks belong to their respective owners and are used only to describe which public endpoints this Actor reads.
Our other Actors
Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.
Video, audio & social
- YouTube Transcript API — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- YouTube Comments API — comments and replies with likes, pinned and hearted flags, newest or top sort.
- YouTube Channel API — a channel's videos, Shorts and live streams, plus YouTube search.
- Podcast Search, Episodes & Charts API — Apple Podcasts search, charts and full episode feeds.
- Telegram Channel Scraper — posts, views and channel stats from public Telegram channels.
- Substack Scraper — posts with full free text, comments and publication profiles.
News, documents & the web
- Google News Search, Topics & Real Article URLs — news search and topic feeds with the publisher's real URL decoded.
- Website to Markdown — Content Extractor for LLMs & RAG — any site as clean Markdown, text and heading-aware chunks.
- Internet Archive API — archive.org search, item metadata, files and reviews.
- Wayback Machine Toolkit — archived URL inventories, snapshots and text diffs between dates.
- Website Technology Detector — the tech stack behind any site, with the evidence for each detection.
- Domain Intelligence API — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- SEO Page Audit — sitemap crawl with on-page checks, structured data and broken-link reports.
- Keyword Suggestions API — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- Website Contact Extractor — emails, phone numbers and social profiles from any list of websites.
Business, finance & jobs
- Congress & Insider Trades API — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- SEC EDGAR API — filings, XBRL financials and full-text search by ticker or CIK.
- Y Combinator Companies, Batches & Founders — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- Career Site Jobs API — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- New Job Postings Monitor — new, closed and changed postings on the career sites you watch.
- Shopify Products API — any Shopify store's catalogue, variants, prices and stock signals.
Apps & games
- App Store & Google Play Reviews API — reviews from both stores with ratings, versions and developer replies.
- App Store Top Charts & App Search API — Apple top charts by country and genre, plus app search and details.
- Steam Reviews API — Steam reviews with playtime, helpfulness and game details.
- Steam Game Data API — prices, tags, review scores, live player counts and top charts.