Bluesky Posts Scraper & Keyword Monitor - Likes & Reposts
Pricing
from $2.19 / 1,000 post returneds
Bluesky Posts Scraper & Keyword Monitor - Likes & Reposts
For brand monitoring, social listening and research: Bluesky posts by keyword with text, author, likes, reposts, replies, links and hashtags, up to 2,000 per query. Monitoring returns only posts not returned before. A refused search is marked as refused, never as empty. No login or API key.
Pricing
from $2.19 / 1,000 post returneds
Rating
0.0
(0)
Developer
NeverEmpty
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Search Bluesky posts by keyword and get one row per post: the text, the author (handle, DID, display name, avatar), likes, reposts, replies, quotes and bookmarks, the post and reply links, images, video, link card, hashtags, mentions and links. No Bluesky login, app password or API key is needed.
Turn on monitoring mode, schedule the Actor, and each run returns only the posts it has not returned before for each keyword - a feed of new mentions of your brand, product or topic. Queries Bluesky refuses and empty results come back as free rows that say why.
What you can use it for
- Brand and product monitoring: schedule your brand names in monitoring mode and receive only new posts that mention them.
- Topic and trend research: collect the newest posts for a keyword with their engagement counts for analysis in a spreadsheet or BI tool.
- Social listening for launches and campaigns: track a hashtag or a phrase and see who is posting and how much the posts are liked and reposted.
- Lead and community discovery: find accounts that post about a subject (
authorHandle,authorDisplayName,postUrl).
How it works
- Enter one or more search queries. Each one is sent to Bluesky's public search API (
https://api.bsky.app/xrpc/app.bsky.feed.searchPosts, newest posts first, 100 per page). - Bluesky's search can only be paged back by time: its page cursor was refused (HTTP 403) on every try on 2026-09-17, so the Actor asks for the next page with
until= the time of the last post read, and drops the posts it already has. - Bluesky refuses a share of anonymous search requests with
403 Forbidden, including from cloud servers (on 2026-09-17, 0 of 12 direct requests from Apify were answered; through US residential proxies 26 of 56 were). The Actor therefore sends every request through an Apify residential proxy with a new session and tries each request up to 10 times in total, with a pause between tries. A query that is still refused gets a freeblockedrow - it is never reported as having no posts. - Up to 3 queries are searched at the same time, as Bluesky's
robots.txtallows "a handful" of concurrent requests to its public API.
Input
| Field | Default | What it does |
|---|---|---|
queries | example query | Search terms, one query per line. Bluesky's search syntax works: "exact phrase", #hashtag, from:handle, mentions:handle, domain:example.com. Repeated queries (ignoring letter case and spaces) are searched once. Up to 100 per run. Left out with monitoring off, the query web scraping is searched. |
maxPostsPerQuery | 100 | Monitoring off: rows returned per query, up to 2,000. Monitoring on: limits only a query's first run. |
language | empty | Bluesky's language filter, e.g. en, ja, pt-BR. Bluesky decides which posts match; each row keeps the post's own tags in langs. |
postedAfter | empty | Only posts created on or after this UTC date or time (2026-09-01, 2026-09-01T12:00:00Z). Checked before sending, because Bluesky silently ignores a date it cannot read. |
monitoringMode | false | Return only posts not returned before for each query (see below). |
resetMonitoringState | false | Forget what was returned for the queries in this input, once. |
Example:
{"queries": ["apify", "\"web scraping\"", "#opensource"],"maxPostsPerQuery": 200,"language": "en","monitoringMode": true}
Queries made only of hashtags or operators
A query with no plain word - for example #bookclub, from:bsky.app or domain:github.com - is answered by Bluesky only without a time range: with since or until it returned 400 Invalid request on 2026-09-17 (adding a word, as in books #bookclub, works). For such queries the Actor reads the newest 100 posts only, applies postedAfter itself, and adds a free incomplete-query row when older posts may exist. Add a plain word to the query to read further back.
Output
One row per post:
{"source": "bluesky-search","status": "ok","query": "climate","searchLanguage": null,"postUrl": "https://bsky.app/profile/reuters.com/post/3mvohc6ksst2q","postUri": "at://did:plc:jbvnehrrdqoulco4rf5gxg5r/app.bsky.feed.post/3mvohc6ksst2q","postCid": "bafyreicnwfzygeqfo5byd4vz3wgwudyur3lu5dqvlkfuqjcpxdzrohgsk4","text": "Climate change likely to have helped trigger Nepal glacier collapse, scientists say reut.rs/4AchDBX","textContainsQuery": true,"createdAt": "2026-09-17T01:00:44Z","indexedAt": "2026-09-17T01:00:45.261Z","langs": [],"authorHandle": "reuters.com","authorDid": "did:plc:jbvnehrrdqoulco4rf5gxg5r","authorDisplayName": "Reuters","authorAvatarUrl": "https://cdn.bsky.app/img/avatar/plain/did:plc:jbvnehrrdqoulco4rf5gxg5r/bafkreibl5cibo6e7epl3pomyqeu7pkvb6yo7zg6q3wf2o4syej5yycrepy","likeCount": 2,"repostCount": 0,"replyCount": 0,"quoteCount": 0,"bookmarkCount": 0,"isReply": false,"replyParentUri": null,"replyRootUri": null,"embedType": "external","imageUrls": [],"imageAltTexts": [],"videoPlaylistUrl": null,"videoThumbnailUrl": null,"linkCardUrl": "https://reut.rs/4AchDBX","linkCardTitle": "Climate change likely to have helped trigger Nepal glacier collapse, scientists say","quotedPostUri": null,"hashtags": [],"mentionedDids": [],"linkUrls": ["https://reut.rs/4AchDBX"],"labels": [],"isNewSinceLastRun": null,"scrapedAt": "2026-09-17T01:02:11.000Z"}
- Counts (
likeCount,repostCount,replyCount,quoteCount,bookmarkCount) are the numbers Bluesky's API returned atscrapedAt. A count Bluesky did not return isnull, not0. textContainsQuerysays whether every word of the query appears in the post text (a word made of letters and digits must not be part of a longer word, soAIdoes not match "said"; a#hashtagis checked as its word). Bluesky also matches words outside the text (for example in link cards or image descriptions), so it can befalsefor a correct match: on 2026-09-17, the share of posts whose text contained all the words was 81 of 100 forclimate change, 64 of 100 forapify, 70 of 99 formachine learning, 55 of 100 forTaylor Swiftand 99 of 100 for#bookclub. It isnullfor a query made only of operators (such asfrom:bsky.app).createdAtis the time the post says it was written (set by the posting app);indexedAtis when Bluesky indexed it. Bluesky orders "latest" results bycreatedAt.embedTypeisimages,video,external(link card),record(quote),recordWithMedia(quote with media),gallery, ornull.imageUrlsare the full-size images, with their descriptions inimageAltTextsin the same order.hashtags,mentionedDids,linkUrlscome from the post's rich-text data, not from guessing in the text.postUrluses the author's handle; if Bluesky marks the handle as invalid, it uses the DID instead.- Inside the post text, email addresses are replaced with
[email removed], and numbers written with a+country code or as 10 digits starting with 6-9 (the mobile number shape in India) are replaced with[phone removed]. Other phone number formats are not removed. - Rows from different queries can be interleaved, because queries run in parallel. Sort by
queryandcreatedAtif you need them grouped. - A post found by two queries in the same run is returned and charged once, under the first query that found it.
Rows that are never charged
Every row has a status. Only ok rows are charged. The others explain what happened in note:
status | What it means |
|---|---|
ok | A post. Charged. |
no-results | Bluesky returned an empty result for the query. |
blocked | Bluesky answered with 403, 429, a server error or nothing on all 10 attempts. The query is not reported as empty. Run it again. |
rejected-by-bluesky | Bluesky rejected the search with HTTP 400; its message is in note (for example an unknown language code). |
unreadable | Bluesky's answer was not a search result in the shape this Actor reads. |
invalid-input | The input could not be used (empty query list, a date or language code that cannot be read, monitoring without queries). Nothing was requested. |
duplicate-query | The same query appeared earlier in the list. |
incomplete-query | Not every post for the query could be read in this run: only the newest 100 posts can be read for hashtag- or operator-only queries; more than 100 posts shared the same second; or, in monitoring mode, more posts were created since the last run than one run reads. The note gives the time range that was not read. |
no-new-posts | Monitoring mode: no post that was not returned before. Only the check fee applies. |
budget-reached | The run's maximum total charge was reached; the note lists the queries that were not searched. |
not-checked | Queries beyond the first 100 in the list. |
Pricing
- $3.00 per 1,000 posts returned (
post-returned), with lower prices on higher Apify plans. - Monitoring mode only: $0.30 per 1,000 query checks (
query-checked) - one check each time a query is searched, whether or not it has new posts. Queries Bluesky refuses are not charged. Example: 20 queries checked every hour = 14,400 checks a month = $4.32, plus $3.00 per 1,000 new posts. - Free rows (the table above) are never charged.
- The Actor stops before it goes over the maximum total charge you set for a run. In monitoring mode it starts a query only if the remaining charge can pay for a check and one post, so a low limit does not keep paying check fees without returning posts.
Monitoring mode
With monitoringMode on, the Actor stores, for each query and language, which posts it returned and up to which post time it has read everything (in a named key-value store of your Apify account, so it is kept between runs).
- First run of a query: returns the newest posts, up to
maxPostsPerQuery, as a starting point.isNewSinceLastRunisfalseon these rows. - Later runs: search from 15 minutes before the last completed read (to catch posts Bluesky indexes late) and return only posts not returned before, with
isNewSinceLastRun: true.maxPostsPerQuerydoes not cut them; up to 1,000 posts (10 pages) are read per query per run, and if more were created anincomplete-queryrow names the time range that was skipped. Run the monitor more often for very busy queries. - A post written with an older
createdAtthan the last completed read is returned only if Bluesky indexed it after that time. Posts indexed more than 15 minutes late can be missed. - If a run stops early (maximum total charge, a refused request), posts that were read but not returned are not remembered and the read position is not moved. A later run returns them if they are within what it searches: a query that has never completed a run is searched again for its newest
maxPostsPerQueryposts, and a query that has is searched from its last completed read (up to 1,000 posts). - Letter case and extra spaces do not make a query different; a different
languagedoes. resetMonitoringStateforgets only the queries in the current input. Turn it off again after one run.- Do not put the same query in two schedules that can run at the same time: Apify's key-value store has no atomic update, so two runs finishing together can overwrite each other's record and return a post twice.
Limits
- Public posts only, as Bluesky's public search API returns them. No login is used, so posts that Bluesky shows only to logged-in users may be missing.
- Up to 100 queries and 2,000 posts per query per run (1,000 new posts per query per run in monitoring mode).
- Bluesky does not publish an exact result count for a search (it returned
hitsTotal10,000 for every busy query on 2026-09-17), so the Actor does not report one. - Requests go through Apify residential proxies; if the proxy is not available to the run, requests go out directly and most are refused (
blockedrows).