Bluesky Posts & Profiles Scraper (No Login)
Pricing
from $1.00 / 1,000 results
Bluesky Posts & Profiles Scraper (No Login)
Scrape Bluesky posts by keyword or hashtag, user feeds, profiles, followers and following via Bluesky's official public API. No login, no proxies. Monitor mode returns only new posts.
Pricing
from $1.00 / 1,000 results
Rating
0.0
(0)
Developer
Agent Factory
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
16 days ago
Last modified
Categories
Share
Bluesky Posts, Profiles & Followers: official API, no login, pay only for new posts
Collect public Bluesky data from Bluesky's own public API: posts matching keywords or #hashtags, any account's posts, profiles, and follower/following lists. No login, no cookies, no proxies, no browser.
Turn on monitor mode and schedule it. Each run returns only posts this monitor hasn't delivered before, so you never pay twice for the same post. It also tells you when you might have missed posts (Bluesky caps logged-out search at 100 posts per query), so you know when to schedule runs more often.
- ✅ Pay only for new posts. Monitor mode remembers what it already delivered (per query/account, per filter set). Re-runs with nothing new cost only the $0.00005 start fee.
- ✅ Honest about Bluesky's limits. Gap warnings, per-query
hitsTotal, and a clearOUTPUTrecord of what was skipped and why. No silent truncation. - ✅ Official API only:
public.api.bsky.app/api.bsky.app, the same AppView the Bluesky app uses. Nothing to break when the website changes. - ✅ Respects user privacy choices. Accounts that asked Bluesky not to show their content to logged-out viewers are never returned (and never charged).
- ✅ One clean row per result. No summary rows, no empty rows, no error rows in your dataset. You pay only for real posts, profiles and accounts.
What can I use it for?
- Brand and keyword monitoring: hourly runs on your brand, product or competitors, with only new mentions delivered to Slack, Sheets or a webhook.
- Hashtag and event tracking:
#hashtagwith a date window. - Creator and competitor tracking: new posts from a list of accounts, with engagement counts.
- Audience research and lead lists: profiles, followers and following, with bios and counts.
- Research and AI pipelines: clean JSON with language, hashtags, mentions, links, media and labels.
Modes
mode | Input | One result = |
|---|---|---|
searchPosts | queries: keywords, phrases or a single #hashtag per line | one matching post |
userPosts | accounts: handles, DIDs or profile URLs | one post from that account (reposts optional) |
profiles | accounts | one profile |
followers | accounts | one follower of that account |
following | accounts | one account it follows |
Input
| Field | Type | Description |
|---|---|---|
mode | string | searchPosts (default), userPosts, profiles, followers, following |
queries | array | searchPosts: one search per line. #tag = hashtag search. Bluesky search syntax works (e.g. "exact phrase") |
accounts | array | bsky.app, @bsky.app, did:plc:…, https://bsky.app/profile/…; a bare jay means jay.bsky.social |
onlyNewSinceLastRun | boolean | Monitor mode (searchPosts, userPosts): emit only posts not delivered before. The first run is the baseline |
stateKey | string | Monitor name. One per schedule/watchlist (default default) |
since / until | string | Post time window: YYYY-MM-DD, ISO timestamp, or relative 24h / 7d / 2w. since is inclusive, until exclusive |
sort | string | searchPosts: latest (default) or top |
lang | string | searchPosts: language code, e.g. en |
author / mentions | string | searchPosts: only posts by / mentioning this handle or DID |
domain | string | searchPosts: only posts linking to this domain |
feedFilter | string | userPosts: posts_and_author_threads (default, like the app's Posts tab), posts_with_replies, posts_no_replies, posts_with_media |
includeReposts | boolean | userPosts: include reposts (isRepost: true), default false |
skipAdultContent | boolean | Drop posts labeled porn / sexual / nudity / graphic-media (default true; skipped posts aren't charged) |
maxItemsPerSource | integer | Cap per query or account (default 100) |
maxItems | integer | Hard cap per run. Use it to control cost |
Examples:
{ "mode": "searchPosts", "queries": ["apify", "#opensource"], "since": "7d", "lang": "en", "maxItemsPerSource": 25 }
{ "mode": "searchPosts", "queries": ["\"my brand\"", "#mybrand"], "onlyNewSinceLastRun": true, "stateKey": "brand-hourly" }
{ "mode": "userPosts", "accounts": ["bsky.app", "https://bsky.app/profile/apify.com"], "onlyNewSinceLastRun": true, "includeReposts": true }
{ "mode": "followers", "accounts": ["apify.com"], "maxItemsPerSource": 1000 }
Output
One dataset item per result. Posts have the same shape in searchPosts and userPosts:
{"type": "post","uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mwolmfws5k2r","cid": "bafyreicquqnkjvugubmjyfiorc3nbrxuu7y4faicpulqmxb4ua73tpb564","postId": "3mwolmfws5k2r","url": "https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r","text": "Happy opening day of hockey season, NHL fans! \n\nKeep up with all 1,344 regular season games with this starter pack of hockey writers.","createdAt": "2026-09-29T19:43:14.855Z","indexedAt": "2026-09-29T19:43:15.262Z","langs": ["en"],"authorDid": "did:plc:z72i7hdynmk6r22z27h6tvur","authorHandle": "bsky.app","authorDisplayName": "Bluesky","authorAvatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihwihm6kpd6zuwhhlro75p5qks5qtrcu55jp3gddbfjsieiv7wuka","authorVerified": false,"replyCount": 72, "repostCount": 140, "likeCount": 983, "quoteCount": 17, "bookmarkCount": 56,"isReply": false, "replyParentUri": null, "replyRootUri": null,"isQuote": false, "quotedPost": null,"hashtags": [], "mentions": [], "links": [],"images": [], "video": null, "externalLink": null,"labels": [],"isRepost": false, "repostedByHandle": null, "repostedByDid": null, "repostedAt": null,"mode": "userPosts","input": "bsky.app","scrapedAt": "2026-10-08T02:18:34.662Z"}
| Field | Notes |
|---|---|
mentions | [{did, handle}], with handles as written in the post |
quotedPost | {uri, url, authorHandle, text, available} for quote posts |
images / video / externalLink | [{url, thumb, alt}] / {playlist, thumbnail, alt} / {uri, title, description} |
labels | Moderation/self labels on the post |
isRepost, repostedBy*, repostedAt | userPosts with includeReposts |
changeType, firstSeenAt | Monitor mode only ("new", and when this monitor first delivered it) |
input | The query or account this row came from |
Profiles (type: "profile"): did, handle, displayName, description, url, avatar, banner, followersCount, followsCount, postsCount, listsCount, feedsCount, starterPacksCount, isLabeler, isBot, verified, pinnedPostUri, labels, createdAt, indexedAt.
Followers / following (type: "follower" / "following"): ofHandle, ofDid, did, handle, displayName, description, url, avatar, isBot, verified, labels, createdAt, indexedAt.
Each run also writes an OUTPUT record to the default key-value store (free, not a dataset row): per query/account
results, errors (e.g. "Account not found"), skip counts (opted-out, adult, outside date range, already delivered),
Bluesky's hitsTotal for searches, monitor baseline / possibleGap flags, and whether a run limit was hit.
Export the dataset as JSON, CSV, Excel or XML, or read it through the Apify API.
Monitor mode (pay only for new posts)
Set onlyNewSinceLastRun: true and schedule the Actor (e.g. every hour).
- First run = baseline: current matching posts are delivered with
changeType: "new". - Later runs deliver only posts this monitor hasn't delivered before. Engagement-count changes don't re-deliver a post.
- Each query/account plus its filters has its own history, so changing
langor dates starts a fresh baseline for that combination. Use a differentstateKeyper schedule. - Gap warning: logged-out Bluesky search returns at most the newest 100 posts per query. If all 100 are new, older new posts may have been missed.
OUTPUTthen showspossibleGap: true, and you should schedule that monitor more often. For account feeds, the Actor pages back to where the last run stopped (up tomaxItemsPerSourcenew posts). - If a run is cut short by
maxItems, your spending limit, or an API error, the undelivered posts are offered again next run. - State lives in the named key-value store
bluesky-public-data-state. It holds only post IDs and timestamps, never post text.
Pricing (pay per result)
$0.001 per result ($1 per 1,000 dataset items), plus a $0.00005 start fee per run.
- One result = one post, profile, follower or followed account in the dataset. Nothing else is ever written to the dataset.
- Errors, accounts not found, opted-out accounts, adult-filtered posts and already-delivered posts produce no rows, so they cost nothing.
- Monitor runs with nothing new cost only the $0.00005 start fee.
Examples: 1,000 posts from a keyword search across several queries ≈ $1. A brand monitor that runs hourly and finds about 20 new posts per hour delivers about 480 posts a day ≈ $0.48/day (plus 24 start fees ≈ $0.0012). Without de-duplication, re-pulling the newest 100 posts every hour would be 2,400 rows a day.
Set maxItems and maxItemsPerSource to cap any run. The Actor also stops cleanly at your run's maximum charge limit.
Limits (Bluesky's, not ours)
- Search: max 100 posts per query per run. Bluesky doesn't allow logged-out clients to page further in post search. Use narrower queries, date windows (
since/until), or monitor mode on a schedule. - Search can be switched off by Bluesky. Bluesky sometimes disables anonymous post search under heavy load. The query then shows an error in
OUTPUT, and no rows (and no charges) are produced. Account feeds, profiles and follower lists are not affected. - Bluesky's search matching, language detection and
topranking are Bluesky's own. Results can differ slightly from the app. - Follower/following lists contain only accounts that are currently viewable. Deleted, deactivated and opted-out accounts are skipped, so counts can be lower than
followersCount. - No private data: no DMs, no mutes or blocks, no likes lists, nothing that needs a login.
Compliance
- Uses only Bluesky's official public AppView API without authentication, as documented in API Hosts and Auth and Rate Limits. No scraping, no login, no proxies.
- Read-only: it never posts, likes, follows or messages anyone, in line with the Bluesky Developer Guidelines.
- Honors the
!no-unauthenticatedself-label ("discourage apps from showing my account to logged-out users"), as Bluesky recommends. Those accounts' posts and profiles are never returned. - Polite by design: at most 3 concurrent requests and 4 requests per second, exponential backoff on 429/5xx, and an identifying User-Agent.
- You are responsible for how you use the data. Bluesky content belongs to its authors. Honor deletion requests, and follow applicable privacy laws (e.g. GDPR) and the terms of wherever you republish it.
Contact
Questions, bug reports, or a takedown/deletion request (including from Bluesky): unrealevents22@gmail.com You can also open an issue on the Actor's Issues tab.
FAQ
Why did a search return fewer than 100 posts? Opted-out accounts and (by default) adult-labeled posts are removed. OUTPUT shows the counts. Bluesky may also have fewer matches (hitsTotal).
Can I get more than 100 posts for one keyword? Run several narrower date windows (since/until) or language filters, or use monitor mode on a schedule to collect continuously.
Do I need a Bluesky account, cookies or proxies? No.
A search query failed with HTTP 403. Bluesky has temporarily disabled anonymous search. Try again later. Nothing was charged for that query.
Run locally (no Apify account needed)
npm installmkdir -p storage/key_value_stores/defaultecho '{"mode":"searchPosts","queries":["#bluesky"],"maxItemsPerSource":10}' > storage/key_value_stores/default/INPUT.jsonnode src/main.js # results in storage/datasets/default, OUTPUT in storage/key_value_stores/defaultnpm test # offline tests on recorded API responses