Bluesky Posts Scraper & Keyword Monitor - Likes & Reposts avatar

Bluesky Posts Scraper & Keyword Monitor - Likes & Reposts

Pricing

from $2.19 / 1,000 post returneds

Go to Apify Store
Bluesky Posts Scraper & Keyword Monitor - Likes & Reposts

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

NeverEmpty

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

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

  1. 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).
  2. 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.
  3. 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 free blocked row - it is never reported as having no posts.
  4. Up to 3 queries are searched at the same time, as Bluesky's robots.txt allows "a handful" of concurrent requests to its public API.

Input

FieldDefaultWhat it does
queriesexample querySearch 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.
maxPostsPerQuery100Monitoring off: rows returned per query, up to 2,000. Monitoring on: limits only a query's first run.
languageemptyBluesky's language filter, e.g. en, ja, pt-BR. Bluesky decides which posts match; each row keeps the post's own tags in langs.
postedAfteremptyOnly 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.
monitoringModefalseReturn only posts not returned before for each query (see below).
resetMonitoringStatefalseForget 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 at scrapedAt. A count Bluesky did not return is null, not 0.
  • textContainsQuery says 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, so AI does not match "said"; a #hashtag is checked as its word). Bluesky also matches words outside the text (for example in link cards or image descriptions), so it can be false for a correct match: on 2026-09-17, the share of posts whose text contained all the words was 81 of 100 for climate change, 64 of 100 for apify, 70 of 99 for machine learning, 55 of 100 for Taylor Swift and 99 of 100 for #bookclub. It is null for a query made only of operators (such as from:bsky.app).
  • createdAt is the time the post says it was written (set by the posting app); indexedAt is when Bluesky indexed it. Bluesky orders "latest" results by createdAt.
  • embedType is images, video, external (link card), record (quote), recordWithMedia (quote with media), gallery, or null. imageUrls are the full-size images, with their descriptions in imageAltTexts in the same order.
  • hashtags, mentionedDids, linkUrls come from the post's rich-text data, not from guessing in the text.
  • postUrl uses 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 query and createdAt if 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:

statusWhat it means
okA post. Charged.
no-resultsBluesky returned an empty result for the query.
blockedBluesky 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-blueskyBluesky rejected the search with HTTP 400; its message is in note (for example an unknown language code).
unreadableBluesky's answer was not a search result in the shape this Actor reads.
invalid-inputThe input could not be used (empty query list, a date or language code that cannot be read, monitoring without queries). Nothing was requested.
duplicate-queryThe same query appeared earlier in the list.
incomplete-queryNot 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-postsMonitoring mode: no post that was not returned before. Only the check fee applies.
budget-reachedThe run's maximum total charge was reached; the note lists the queries that were not searched.
not-checkedQueries 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. isNewSinceLastRun is false on 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. maxPostsPerQuery does not cut them; up to 1,000 posts (10 pages) are read per query per run, and if more were created an incomplete-query row names the time range that was skipped. Run the monitor more often for very busy queries.
  • A post written with an older createdAt than 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 maxPostsPerQuery posts, 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 language does.
  • resetMonitoringState forgets 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 hitsTotal 10,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 (blocked rows).