Bluesky Scraper — Posts, Profiles, Feeds & Threads, No Login
Pricing
from $0.55 / 1,000 posts
Bluesky Scraper — Posts, Profiles, Feeds & Threads, No Login
Scrape Bluesky without login: search posts, account posts, profiles with follower counts, full reply threads and custom feeds. Text, likes, reposts, hashtags, mentions, links and image alt text in clean JSON.
Pricing
from $0.55 / 1,000 posts
Rating
0.0
(0)
Developer
Cliqto Media
Maintained by CommunityActor stats
0
Bookmarked
3
Total users
2
Monthly active users
5 days ago
Last modified
Categories
Share
Scrape public Bluesky posts, profiles, custom feeds, lists and reply threads with no login, and get clean JSON rows with text, engagement counts, links, mentions, hashtags, image alt text, quoted posts and author details.
Use it for social listening, research datasets, brand monitoring or AI pipelines: the smallest run is one search query, such as apify, which returns up to 30 recent posts in a few seconds for about $0.03 (100 posts cost about $0.09 on the Free plan, less on paid plans).

Table of contents
- What it does
- Quick start
- Use cases
- Input
- Output
- Pricing and limits
- Switching from another Bluesky scraper
- API and integrations
- Troubleshooting
- FAQ
- Learn more
- Support
What it does
Bluesky Scraper reads the public Bluesky AppView API (AT Protocol) without an account, a password or cookies. It has five modes:
| Mode | You give | You get |
|---|---|---|
searchPosts | Keywords, #hashtags, phrases, operators like from:bsky.app, lang:en, domain:example.com | Up to 100 matching posts per query, latest or top |
authorFeed | Handles, DIDs or profile links | Posts from each account, newest first, with optional replies and reposts |
profiles | Handles, DIDs or profile links | One profile row per account: bio, follower/following/post counts, avatar, pinned post |
thread | Post links or at:// URIs | The post, its parent posts and the replies below it, with threadRole and threadDepth |
feed | Custom feed links or list links | Posts from the feed or list |
An account, post or feed that does not exist returns a not_found row instead of silently disappearing, so the number of answers always matches your input.
Good fit
- Track what people post about a brand, product, hashtag or domain on Bluesky.
- Collect the posts and profiles of a list of accounts for research or lead lists.
- Pull a full reply thread for sentiment or discussion analysis.
- Feed clean, typed Bluesky rows into an AI agent, a spreadsheet or a database.
Not a fit / limits
- No private data: no DMs, no muted or blocked lists, nothing that needs login.
- No followers or following lists in this version.
- Search returns at most 100 posts per query, because Bluesky blocks further search pages without login. The Actor does not try to get around this.
- Counts (likes, reposts, followers) are a snapshot at fetch time.
Quick start
- Open the Input tab.
- Keep What to scrape on Search posts and add a word to Search queries, e.g.
apify. - Keep Max posts per query at the prefilled
30(the runtime default is100). - Click Start.
- Open the Posts view in the Output tab and check
text,author.handleandlikeCount.
Expected result: up to 30 of the newest posts that match the query, one row per post.
Use cases
| Buyer job | Input path | Output entity | Key fields | Next step |
|---|---|---|---|---|
| Brand or keyword listening | mode: searchPosts, queries | post | text, createdAt, likeCount, author.handle, links | Schedule the Actor, export to Sheets or a database |
| Account research | mode: authorFeed, handles, since | post | text, isRepost, replyCount, externalLink | Compare accounts, build datasets |
| Influencer or lead list | mode: profiles, handles | profile | displayName, description, followersCount, postsCount | CRM import |
| Discussion analysis | mode: thread, postUrls | post | threadRole, threadDepth, text, replyTo | Sentiment or topic analysis |
| Curated topic monitoring | mode: feed, feedUrls | post | text, hashtags, images[].alt | AI summarisation |
Input
Input fields
| Field | Plain name | Type | Required | Default / prefill | Allowed values | Effect | Example |
|---|---|---|---|---|---|---|---|
mode | What to scrape | string | yes | searchPosts | searchPosts, authorFeed, profiles, thread, feed | Picks the list that is read | "authorFeed" |
queries | Search queries | string[] | for searchPosts | prefill ["apify"] | up to 100 | One search per line | ["#bluesky", "from:bsky.app lang:en"] |
handles | Handles | string[] | for authorFeed, profiles | none | handle, @handle, DID, profile link; up to 1,000 (100 for authorFeed) | Accounts to read; a bare name becomes name.bsky.social | ["bsky.app", "did:plc:z72i7hdynmk6r22z27h6tvur"] |
postUrls | Post links | string[] | for thread | none | bsky.app post links or at:// URIs; up to 100 | Threads to read | ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"] |
feedUrls | Feed or list links | string[] | for feed | none | …/feed/… or …/lists/… links; up to 50 | Feeds or lists to read | ["https://bsky.app/profile/bsky.app/feed/whats-hot"] |
maxItems | Max posts per input | integer | no | 100, prefill 30 | 1–5,000 | Cap for each query, account, thread or feed; ignored in profiles | 500 |
since | From date | string | no | none | YYYY-MM-DD or ISO date-time, UTC | Only posts created on or after it | "2026-10-01" |
until | To date | string | no | none | same; must be after since | Only posts created before it | "2026-10-08" |
searchSort | Search order | string | no | latest | latest, top | Order of search results | "top" |
language | Post language | string | no | all | language code, e.g. en, pt-BR | Search only | "en" |
includeReplies | Include replies | boolean | no | true | Drop replies in search and account posts when off | false | |
includeReposts | Include reposts | boolean | no | false | Account posts and feeds: also return reposts, marked isRepost | true | |
threadDepth | Reply depth | integer | no | 10 | 0–100 | Reply levels below the post; 0 = post and parents only | 3 |
Lines pasted with several values are split, and duplicates are removed. A value that cannot be read (e.g. not a handle!) returns an invalid_input row, and the other values are still processed. A wrong field combination, such as profiles with no handles or since after until, stops the run at once with a clear message and no rows.
Interacting settings
| When | Input A | Input B | Effective behavior |
|---|---|---|---|
| Search with a lone hashtag and dates | queries: ["#ai"] | since / until | Sent to Bluesky as a tag search, which is the only form it accepts with dates |
| Account posts with dates | handles | since | Reading stops once the feed reaches posts older than since |
| Account posts with strict filters | includeReplies: false, since / until | — | If fewer than 1 in 100 feed items match after 1,000 items, the account is stopped as partial with a warning. Use a search with from:handle instead |
| Any mode | maxItems | search | Search never returns more than 100 posts per query, whatever maxItems says |
Input examples
Search posts about a topic
{"mode": "searchPosts","queries": ["apify", "#webscraping"],"maxItems": 50,"searchSort": "latest","language": "en"}
Expected output: up to 50 post rows per query with text, author, likeCount, links, hashtags.
Recent posts from accounts
{"mode": "authorFeed","handles": ["bsky.app", "@jay.bsky.team"],"since": "2026-10-01","includeReposts": true,"maxItems": 200}
Profiles
{"mode": "profiles","handles": ["bsky.app", "jay.bsky.team", "https://bsky.app/profile/pfrazee.com"]}
A reply thread
{"mode": "thread","postUrls": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"],"threadDepth": 3,"maxItems": 200}
Output
Every row goes to the default Dataset and has recordType: post, profile or status. The Output tab has Posts, Profiles and Not found & errors views. Views choose columns, not rows: in a posts run the Profiles view shows the same rows with empty profile columns, and status rows appear in the Posts view with empty post columns. To get only one type, filter by recordType in your export or code. The run summary is in the key-value store as RUN_SUMMARY.
Post fields
| Field | Meaning | Type | Can be empty? |
|---|---|---|---|
uri, cid | AT Protocol identifiers of the post | string | cid can be null |
url | bsky.app link to the post | string | rarely null |
author | did, handle, displayName, avatar, url | object | name and avatar can be null |
text | Post text | string | empty for image-only posts |
createdAt, indexedAt | Created (by the author's record) and indexed by Bluesky, ISO 8601 | string | can be null |
langs | Declared languages | string[] | often empty |
replyCount, repostCount, likeCount, quoteCount, bookmarkCount | Counts at fetch time | number | null when Bluesky does not report it (never a fake 0) |
isReply, replyTo, rootPost | Reply status, parent and root post | boolean, object | objects are null for top-level posts |
isRepost, repostedBy, repostedAt | Repost marker in account posts and feeds | boolean, object, string | null when not a repost |
isPinned | Pinned on the author's profile | boolean | |
isQuote, quotedPost | Quoted post: uri, url, authorHandle, text | boolean, object | null when not a quote |
embedType | images, video, external, record, recordWithMedia | string | null without an embed |
images | url, thumbnailUrl, alt, width, height | object[] | empty without images |
video | playlistUrl, thumbnailUrl, alt | object | null without a video |
externalLink | Link card: url, title, description | object | null without a card |
links | All links in the text and the link card | string[] | |
hashtags, mentions | Tags and mentioned accounts (did, handle) | array | |
labels | Moderation labels | string[] | |
threadRole, threadDepth | anchor, parent or reply, and level (thread mode) | string, number | null outside thread mode |
mode, input, fetchedAt, source | Which input produced the row and when | string |
Profile fields
did, handle, displayName, description, url, avatar, banner, followersCount, followsCount, postsCount, listsCount, feedsCount, starterPacksCount, isLabeler, verifiedStatus, pinnedPostUrl, labels, createdAt, indexedAt, plus mode, input, fetchedAt, source.
Status rows
status | When | errorCode examples |
|---|---|---|
not_found | The account, post, feed or list does not exist | NOT_FOUND |
invalid_input | The value cannot be read as a handle, link or query | INVALID_INPUT |
error | Bluesky failed or refused the request after retries | RATE_LIMITED, SOURCE_FORBIDDEN, SOURCE_UNAVAILABLE |
A source failure is never shown as "no results": it is an error row and the run status says so.
Run summary (RUN_SUMMARY)
| Field | Meaning |
|---|---|
status | COMPLETE, PARTIAL, NO_RESULTS, CAPPED, ERROR, INVALID_INPUT or CANCELLED |
errorCode | Set when the whole run failed for one reason, e.g. INVALID_INPUT or RUN_TIMEOUT_TOO_SHORT; otherwise null |
message | One plain-English sentence about the result |
counts | posts, profiles, notFound, errors, invalidInputs, duplicatesSkipped, chargedPosts, chargedProfiles, chargedEmptyResults, unprocessedInputs |
inputs | One entry per input value (input, status, items, message): complete, partial, no_results, not_found, error, invalid_input or skipped, with the row count |
warnings | Plain-English notes, e.g. the 100-per-query search limit |
Output example
A fictional post, trimmed to the main fields:
{"recordType": "post","status": "complete","mode": "searchPosts","input": "opendata","uri": "at://did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/app.bsky.feed.post/3mexample2abc","url": "https://bsky.app/profile/author.example/post/3mexample2abc","author": {"did": "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa","handle": "author.example","displayName": "Sample Author","url": "https://bsky.app/profile/author.example"},"text": "Our open dataset is live! Thanks @team.example #opendata https://data.example/downloads","createdAt": "2026-10-12T09:15:00.000Z","likeCount": 2431,"repostCount": 318,"replyCount": 154,"isReply": false,"isRepost": false,"embedType": "images","images": [{ "url": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:aaaaaaaaaaaaaaaaaaaaaaaa/bafkexample@jpeg", "alt": "Chart of monthly downloads" }],"links": ["https://data.example/downloads"],"hashtags": ["opendata"],"mentions": [{ "did": "did:plc:bbbbbbbbbbbbbbbbbbbbbbbb", "handle": "team.example" }],"quotedPost": null,"threadRole": null,"fetchedAt": "2026-10-13T01:52:14.278Z","source": "Bluesky AppView API (AT Protocol)"}
null means Bluesky did not provide the value; it never stands for zero.
Pricing and limits
Pricing unit
You pay per event. Platform usage (compute, proxy, storage) is included in the price.
| Event | Free plan | Starter (Bronze) | Scale (Silver) | Business (Gold) and higher |
|---|---|---|---|---|
| Post row | $0.0009 | $0.00085 | $0.0007 | $0.00055 |
| Profile row | $0.0027 | $0.0026 | $0.0021 | $0.0016 |
| Empty result | $0.0005 | $0.0005 | $0.0005 | $0.0005 |
| Actor start | $0.001 | $0.001 | $0.001 | $0.001 |
- Post row / profile row: one per data row written.
- Empty result: one per input value that produced no data: a search query with no matches, an account with no matching posts, a profile, post or feed that does not exist, or a value that cannot be read (for example a typo in a link). If every value is unreadable, the run ends as
NO_RESULTSwith a message saying so, and each value is one empty result. It covers the requests made for that value. - Actor start: charged by Apify once per run (per GB of memory; the default 256 MB counts as one).
- Free:
errorrows (Bluesky failed), skipped inputs, duplicate posts and duplicate input values (the same account, post or feed written another way, e.g.@Name.bsky.socialand its profile link).
A run whose input is rejected before any request (e.g. profiles without handles) costs only the start fee.
Cost examples (Free plan prices)
| Scenario | Input | Charged events | Cost |
|---|---|---|---|
| Prefill search | 1 query, 30 posts | start + 30 posts | about $0.028 |
| 1,000 posts from accounts | 2 accounts × 500 | start + 1,000 posts | about $0.90 |
| 100 profiles | 100 handles | start + 100 profiles | about $0.27 |
| Search with no matches | 1 query | start + 1 empty result | $0.0015 |
Spending limit
Set Maximum cost per run in the run options. The Actor checks the limit before every row; when the next row would exceed it, the run stops with status CAPPED and keeps everything written so far.
Limits
- Search: up to 100 posts per query (Bluesky's limit without login). Split the topic into narrower queries or date ranges to see more.
- Account posts: up to 10,000 feed items scanned per account. A scan also stops early when fewer than 1 in 100 items match the filters after the first 1,000 items, so a sparse filter does not read the whole history.
- Feeds: the same sparse stop applies.
- Speed at 256 MB: one account's 1,030 posts in about 5 s; 10,000 account posts took about 2–3 minutes in tests. Large jobs need a longer run timeout than the default 300 s; an input cut by the timeout is marked
partialwith its row count. since/untilcompare the post'screatedAt(for reposts,repostedAt). Account feeds are ordered by when Bluesky indexed the post, so posts imported later with an older date can be outside the scanned range.
Switching from another Bluesky scraper
Field names differ from other Bluesky Actors. Typical mapping:
| Other Actor | This Actor |
|---|---|
mode: search-posts | mode: searchPosts |
mode: posts | mode: authorFeed |
mode: profiles | mode: profiles |
mode: threads / post-urls | mode: thread |
sort | searchSort |
maxResults | maxItems |
authorHandle, authorName, authorDid, authorAvatar | author.handle, author.displayName, author.did, author.avatar |
lang | langs |
mediaUrls, hasMedia | images[].url, video.playlistUrl, embedType |
embedUrl, embedTitle | externalLink.url, externalLink.title (also in links) |
scrapedAt | fetchedAt |
Extra here: image alt text, video, quoted post, repost details, thread roles, custom feeds and lists, and not_found rows.
API and integrations
- API: start a run with
POST https://api.apify.com/v2/acts/cliqtomedia~bluesky-scraper/runsand the JSON input above; read rows from the run's default Dataset. - Export: the Dataset downloads as JSON, CSV, Excel, XML or RSS from the Output tab or the API.
- Scheduling: use Apify Schedules, e.g. a daily search with
sinceset to yesterday. - Integrations: Make, Zapier, n8n, Google Sheets and webhooks connect through the Apify integrations tab.
Use with AI agents (MCP)
- MCP server with only this Actor:
https://mcp.apify.com?tools=cliqtomedia/bluesky-scraper. - Smallest useful input:
{"mode": "searchPosts", "queries": ["apify"], "maxItems": 30}. - Key fields:
text(post text),author.handle(who posted),createdAt(when),likeCount(engagement),url(link to cite). - A small search finishes in a few seconds and costs about $0.03. At most 100 posts per search query;
not_foundanderrorrows explain missing data.
Troubleshooting
Search stops at 100 posts
Symptom: you set maxItems above 100, a query returns 100 posts, its entry in inputs is partial and there is a warning about 100 posts per query. With maxItems of 100 or less the query is complete.
Reason: Bluesky serves only the first search page without login.
Action: add operators (lang:en, from:, domain:) or a narrower since / until.
An account returns no posts
Check the account's entry in inputs in RUN_SUMMARY. not_found means the handle does not exist. no_results means the filters matched nothing; partial with the sparse-scan warning means the filters matched fewer than 1 in 100 items. Loosen since / until, turn on replies or reposts, or use search with from:handle.
The run ends as PARTIAL with skipped inputs
The run hit its timeout. Raise the timeout in the run options or split the input into smaller runs.
The run fails with RUN_TIMEOUT_TOO_SHORT
The run timeout leaves no time to fetch data and save the summary. Set the timeout to at least 60 seconds; the default 300 seconds is enough for small runs. Nothing is charged except the start.
Rows with error status
Bluesky failed or refused the request after retries (RATE_LIMITED, SOURCE_FORBIDDEN, SOURCE_UNAVAILABLE). These rows are free; run the failed values again later.
FAQ
Do I need a Bluesky account or app password?
No. The Actor reads only public data from the public Bluesky API and never asks for login.
Why is search limited to 100 posts per query?
Bluesky serves further search pages only to logged-in clients. The Actor respects that and tells you in the run summary when a query hit the limit.
Are the counts live?
They are a snapshot at fetchedAt. Run again to refresh them.
Is this data complete?
It is what the Bluesky API returns at fetch time. Deleted posts, blocked content and posts hidden by moderation are not included, and counts can be null when Bluesky does not report them.
Can I scrape followers or DMs?
Not in this version. DMs are private and will never be supported.
Is scraping Bluesky allowed?
The Actor uses only public endpoints at a modest request rate and does not bypass any limit. You are responsible for how you store and reuse the content, including personal data and authors' rights.
Learn more
- Bluesky API directory: the public AppView endpoints this Actor reads.
- Search API reference: query operators such as
from:,lang:anddomain:. - AT Protocol: how DIDs, handles and
at://URIs work. - Bluesky Terms of Service.
Support
Use the Issues tab of this Actor for bugs, questions and feature requests. Include the run ID, the mode and the error text. Never post tokens, cookies or private data.