SFOLN

Catalogue / Social media data

Bluesky Scraper – Posts, Profiles & Search

sfoln/bluesky-scraper · Social media, Lead generation, News On request

Bluesky posts and profiles without login: keyword search (latest or top, date and language filters), the posts of any account, full reply threads, and profiles with follower, following and post counts. Text, links, images, likes, reposts, replies. $0.80 per 1,000 posts, $2 per 1,000 profiles.

About

What it does

Scrape Bluesky posts and profiles without logging in: keyword search (latest or top, with date and language filters), the posts of any account, full reply threads, and profiles with follower, following and post counts. One clean record per post or profile, for social listening, brand and competitor monitoring, research datasets, lead lists and AI agents. $0.80 per 1,000 posts, $2 per 1,000 profiles, no start fee; unknown accounts, deleted posts and errors are free.

Price

Price per delivered record

EventPricePer 1,000Charged for
Bluesky post
post
$0.0008$0.80 / 1,000One post row (search result, post of an account, or post of a thread) with its text, author, links, media and engagement counts.
Bluesky profile
profile
$0.002$2 / 1,000One profile record (handle, DID, name, bio, avatar, follower, following and post counts, creation date).

Apify free plan: up to 10 items per run. Paid plans have no per-run cap; set maxTotalChargeUsd to cap spend.

Run

Call it from code, an agent or the Console

This API is private on Apify for now. Email us and we will publish it for you, usually within two working days. The calls below work once it is public. contact@sfoln.com

curl · synchronous run, rows in the response
curl -X POST "https://api.apify.com/v2/actors/sfoln~bluesky-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"searchQueries":["bluesky"],"maxPostsPerInput":10}'
mcp.json · hosted Apify MCP server
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=sfoln/bluesky-scraper"
    }
  }
}
asynchronous run · for large batches
POST https://api.apify.com/v2/actors/sfoln~bluesky-scraper/runs?maxTotalChargeUsd=1
GET  https://api.apify.com/v2/actor-runs/{runId}            # until status is SUCCEEDED
GET  https://api.apify.com/v2/datasets/{defaultDatasetId}/items?format=json

Authenticate with an Apify API token. MCP clients can instead sign in with OAuth when prompted. Example prompt for an agent: Find the 50 latest Bluesky posts that mention "apify" and summarise what people say about it.

Input

Input fields

Pick a mode and fill its list: search queries, account handles (for their posts or their profiles) or post URLs (for reply threads). Each query, account or post is one item with its own summary in the dataset.

FieldTypeDefaultMeaning
modestring"search"Mode
search: posts matching keywords. author-feed: the posts of accounts. profiles: one profile record per account. threads: a post with its parents and replies. One of "search", "author-feed", "profiles", "threads".
searchQueriesarray—Search queries
For the search mode: keywords, phrases or hashtags, one per line. Bluesky's search syntax works too ("exact phrase", from:handle, domain:example.com). Accounts on the Apify free plan can process up to 10 items per run (an item is one query, account or post).
handlesarray—Accounts
For the author-feed and profiles modes: handles (jay.bsky.team, @bsky.app), DIDs (did:plc:…) or profile URLs, one per line. A bare name means name.bsky.social.
postUrlsarray—Post URLs
For the threads mode: post URLs (https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l) or at:// URIs, one per line.
maxPostsPerInputinteger100Max posts per item
Posts returned per query, account or thread (1–10,000). Range 1–10000.
sortstring"latest"Search order
Search mode: latest (newest first, pages back in time) or top (Bluesky's ranking; first 100 results only). One of "latest", "top".
sincestring—Since
Search mode: only posts from this date or date-time on (2026-01-31 or 2026-01-31T12:00:00Z).
untilstring—Until
Search mode: only posts before this date or date-time.
langstring—Language
Search mode: only posts in this language (en, de, ja, pt-BR…).
feedFilterstring"posts"Posts of accounts: filter
author-feed mode: posts (no replies), replies (posts and replies), media (posts with images or video), threads (posts and the account's own threads) or videos. One of "posts", "replies", "media", "threads", "videos".
includeRepostsbooleanfalseInclude reposts
author-feed mode: also return the posts the account reposted (marked isRepost).
threadDepthinteger6Reply depth
threads mode: levels of replies to follow (0 = the post only). Range 0–1000.
includeParentsbooleantrueInclude parent posts
threads mode: also return the posts above the given one, up to the thread's root.
maxItemsinteger1000Max items
Safety cap on queries, accounts or posts processed in this run; the rest is reported as dropped. Range 1–10000.
concurrencyinteger—Concurrency
Items processed in parallel (1–10). Default: 2 for search, 3 for posts and threads, 10 for profiles (fetched 25 per call). Range 1–10.
includeRawbooleanfalseInclude raw upstream response
Attach Bluesky's unmodified JSON to each record under raw (larger output).
Example input
{
  "searchQueries": [
    "bluesky"
  ],
  "maxPostsPerInput": 10
}

Output

One record per input item

Records land in the run's dataset; a synchronous call returns them directly. Each has a status (ok is charged; everything else is free) and a charged flag. The run's OUTPUT record totals items and charges.

FieldTypeMeaning
inputstring | objectInput
The input item this record belongs to (the app, keyword, chart, account or post, with its country or mode).
statusstringStatus
ok | not_found | error | skipped_invalid | skipped_budget | skipped_free_plan | skipped_unmetered e.g. "ok"
chargedbooleanCharged
Whether this record incurred a pay-per-event charge.
chargedEventstringCharged event
Pay-per-event event charged for this record (present only when charged).
chargedCountintegerEvents charged
Events actually charged when the charge came back short of the requested count (present only then; a warning explains it).
sourcestringSource
Upstream data source.
fetchedAtstringFetched at
ISO timestamp.
errorobjectError
{ code, message, provider?, status? } when status is not ok. Codes set by the runtime: budget_exhausted, free_plan_limit, unmetered, aborted, charge_failed, record_too_large, invalid_record, dataset_push_failed; upstream codes: auth, insufficient_credits, rate_limit, timeout, network, server, client, invalid_response, not_found.
warningsarrayWarnings
Notes about this record, e.g. a charge that came back short.
rawobjectRaw
Bluesky's unmodified post or profile view (only with includeRaw).
recordTypestringRecord type
post, profile, or summary (one per query, account or thread: what was delivered and why it stopped).
positionintegerPosition
Order of this row among the rows of its input (1-based).
uristringPost URI
at:// URI of the post.
cidstring | nullCID
Content ID of the post version.
urlstring | nullURL
bsky.app link of the post or profile.
postIdstring | nullPost ID
Record key of the post (last part of its URL).
authorDidstring | nullAuthor DID
Permanent ID of the author.
authorHandlestring | nullAuthor handle
Handle of the author, e.g. bsky.app.
authorDisplayNamestring | nullAuthor name
Display name of the author.
authorAvatarstring | nullAuthor avatar
Avatar image URL of the author.
textstringText
Post text.
createdAtstring | nullCreated
Posts: when the post was written (ISO). Profiles: when the account was created.
indexedAtstring | nullIndexed
When Bluesky indexed the post or profile (ISO).
langsarrayLanguages
Language codes the author set on the post.
replyCountintegerReplies
Replies at fetch time.
repostCountintegerReposts
Reposts at fetch time.
likeCountintegerLikes
Likes at fetch time.
quoteCountintegerQuotes
Quote posts at fetch time.
isReplybooleanIs reply
True when the post replies to another post.
replyParentUristring | nullReply to
URI of the post this one replies to.
replyRootUristring | nullThread root
URI of the first post of the thread.
isQuotebooleanIs quote
True when the post quotes another post.
quotedPostUristring | nullQuoted post
URI of the quoted post.
embedTypestring | nullEmbed
images, gallery, video, external (link card), record (quote) or recordWithMedia.
linksarrayLinks
URLs linked in the text and the link card.
mentionsarrayMentions
Handles (or DIDs) mentioned in the text.
hashtagsarrayHashtags
Hashtags of the post, without #.
imagesarrayImages
Attached images: { url, thumbUrl, alt }.
videoUrlstring | nullVideo
HLS playlist URL of an attached video.
videoThumbnailUrlstring | nullVideo thumbnail
Thumbnail URL of an attached video.
externalLinkobject | nullLink card
Link card of the post: { url, title, description, thumbUrl }.
labelsarrayLabels
Moderation and self labels on the post or profile.
isRepostbooleanIs repost
author-feed mode: true when the account reposted this post (includeReposts).
repostedByHandlestring | nullReposted by
author-feed mode: the account that reposted it.
repostedAtstring | nullReposted at
author-feed mode: when it was reposted.
threadRolestringThread role
threads mode: parent, anchor (the requested post) or reply.
threadDepthintegerThread depth
threads mode: -n for parents, 0 for the requested post, 1… for replies.
didstringDID
Profiles: permanent account ID.
handlestring | nullHandle
Profiles and author-feed summaries: the account handle.
displayNamestring | nullName
Profiles: display name.
descriptionstring | nullBio
Profiles: profile description.
avatarstring | nullAvatar
Profiles: avatar image URL.
bannerstring | nullBanner
Profiles: banner image URL.
followersCountinteger | nullFollowers
Profiles: followers.
followingCountinteger | nullFollowing
Profiles: accounts followed.
postsCountinteger | nullPosts
Profiles: posts written.
pinnedPostUristring | nullPinned post
Profiles: URI of the pinned post.
listsCountinteger | nullLists
Profiles: lists created.
feedsCountinteger | nullFeeds
Profiles: custom feeds created.
starterPacksCountinteger | nullStarter packs
Profiles: starter packs created.
isLabelerbooleanLabeler
Profiles: true for a moderation (labeler) service.
verifiedbooleanVerified
Profiles: true when Bluesky shows the account as verified.
querystringQuery
Summary rows (search mode): the query.
sortstringSearch order
Summary rows (search mode): latest or top.
hitsTotalintegerMatches
Summary rows (search mode): matches Bluesky reported for the query.
feedFilterstringFeed filter
Summary rows (author-feed mode): posts, replies, media, threads or videos.
postUristring | nullThread post
Summary rows (threads mode): URI of the requested post.
postsInThreadintegerPosts in thread
Summary rows (threads mode): posts Bluesky returned for the thread.
duplicatesSkippedintegerDuplicates skipped
Summary rows: posts already delivered for an earlier input of the run (not charged again).
resultsDeliveredintegerRows delivered
Summary rows: posts delivered (and charged) for this item.
stopReasonstringStop reason
Summary rows: limit, end, free_plan, budget, aborted, deadline, empty_pages, max_pages or error.
Example record
{
  "input": {
    "mode": "search",
    "query": "launch"
  },
  "recordType": "post",
  "position": 1,
  "uri": "at://did:plc:synthetic1aaaaaaaaaaaaaa/app.bsky.feed.post/3mxulfla27c2e",
  "cid": "bafyrei3mxulfla27c2ecid",
  "url": "https://bsky.app/profile/alex-example.bsky.social/post/3mxulfla27c2e",
  "postId": "3mxulfla27c2e",
  "authorDid": "did:plc:synthetic1aaaaaaaaaaaaaa",
  "authorHandle": "alex-example.bsky.social",
  "authorDisplayName": "Alex Example",
  "authorAvatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:synthetic1aaaaaaaaaaaaaa/bafkreiavatar1@jpeg",
  "text": "Launch day 🚀 Notes at example.com/launch, thanks @sam-example.bsky.social #webdev",
  "createdAt": "2026-10-14T21:56:03.120Z",
  "indexedAt": "2026-10-14T21:56:03.120Z",
  "langs": [
    "en"
  ],
  "replyCount": 2,
  "repostCount": 3,
  "likeCount": 10,
  "quoteCount": 1,
  "isReply": false,
  "replyParentUri": null,
  "replyRootUri": null,
  "isQuote": false,
  "quotedPostUri": null,
  "embedType": "external",
  "links": [
    "https://example.com/launch"
  ],
  "mentions": [
    "sam-example.bsky.social"
  ],
  "hashtags": [
    "webdev"
  ],
  "images": [],
  "videoUrl": null,
  "videoThumbnailUrl": null,
  "externalLink": {
    "url": "https://example.com/launch",
    "title": "Launch notes",
    "description": "What shipped today.",
    "thumbUrl": "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:synthetic1aaaaaaaaaaaaaa/bafkreithumb1@jpeg"
  },
  "labels": [],
  "status": "ok",
  "charged": true,
  "chargedEvent": "post",
  "source": "bluesky",
  "fetchedAt": "2026-10-14T22:00:00.000Z"
}

FAQ

Questions

Do I need a Bluesky account or an app password?
No. The Actor reads Bluesky's public API, the same data the Bluesky web app shows to logged-out visitors.
How far back does keyword search go?
With sort "latest", the Actor pages back in time until it has maxPostsPerInput posts or reaches since. Bluesky serves only the first page (up to 100 posts) of a "top" search to logged-out clients.
Why does a post appear only once when two of my queries match it?
Within one run a post is delivered and charged once, under the first input that found it; the summary record of the other input counts it in duplicatesSkipped.

Machine-readable

This page for agents