# Bluesky Scraper: Posts, Profiles & Followers (`cleanfeed-data/bluesky-data`) Actor

Scrape Bluesky posts, profiles, reply threads, followers, custom feeds and lists as clean JSON, CSV or Excel. Official public API, no login. Date and language filters plus a new-items-only monitoring mode. $0.80 per 1,000 posts.

- **URL**: https://apify.com/cleanfeed-data/bluesky-data.md
- **Developed by:** [CleanFeed Data](https://apify.com/cleanfeed-data) (community)
- **Categories:** Social media, Lead generation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 posts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bluesky Scraper: Posts, Profiles & Followers

**Scrape Bluesky** posts, profiles, followers and reply threads as clean, flat JSON/CSV/Excel: **profiles**, **author feeds**, **post threads with replies**, **followers / following**, **custom feeds** and **lists**. It uses only Bluesky's **official public AppView API** (`public.api.bsky.app`). No login, no cookies, no browser, and no keyword-search automation.

👉 **Try it in one click:** the input is prefilled with the official `bsky.app` account and capped at 25 items, so a first run takes seconds and costs about $0.03.

### What does this Bluesky scraper do?

- ✅ **Profiles:** bio, follower, following and post counts, avatar, created date, verification
- ✅ **Posts:** text, likes, reposts, replies, quotes, images, video, links, hashtags, language
- ✅ **Reply threads** for any post, with depth and parent author
- ✅ **Followers and following** lists for audience research and lead lists
- ✅ **Custom feeds and lists** (members or posts)
- ✅ **Only-new-items mode** for scheduled monitoring of accounts, threads and followers
- ✅ Runs on the Apify platform: API, scheduling, webhooks, and Slack, Google Sheets, Zapier, Make or n8n integrations

#### Use cases

- **Brand and creator monitoring:** track what key accounts post and how it performs.
- **Audience research and lead generation:** export the followers of accounts in your niche.
- **Community and newsrooms:** follow replies to an announcement or a breaking-news thread.
- **Research and data science:** collect posts from custom feeds and lists for analysis.
- **Social media reporting:** pull engagement counts into Google Sheets or a dashboard every day.

### What data can you get from Bluesky?

| Input | Output |
|---|---|
| A profile (handle, DID or `bsky.app/profile/…` URL) | The profile (bio, follower/following/post counts, avatar, banner, created date, verification), its recent posts (pinned post marked), and optionally followers and follows |
| A post URL or `at://` URI | The post plus its whole reply tree, with depth and parent author. Optionally the parent posts above it |
| A custom feed URL (e.g. *What's Hot*) | Posts in the feed's own ranking |
| A list URL | List members (profiles), the list's post feed, or both |

Paste any `bsky.app` link into **Profiles** and it is routed automatically: post links become threads, `/feed/` links become feeds, and `/lists/` links become lists. Duplicate inputs are processed once, even when one is a handle and another is a DID.

#### Post fields

`url`, `uri`, `cid`, `postId`, `authorHandle`, `authorDid`, `authorDisplayName`, `authorAvatar`, `text`, `createdAt`, `indexedAt`, `langs`, `likeCount`, `repostCount`, `replyCount`, `quoteCount`, `bookmarkCount`, `isReply`, `replyParentUri`, `replyRootUri`, `replyParentAuthorHandle`, `isRepost`, `repostedByHandle`, `repostedAt`, `isPinned`, `isQuote`, `quotedPost`, `embedType`, `images[]` (full-size URL, thumbnail, alt text, aspect ratio), `video` (HLS playlist, thumbnail), `externalLink` (link card), `mentions` (DIDs), `links`, `hashtags`, `labels`, `threadDepth`, `sourceType`, `source`, `isNew`, `firstSeenAt`, `scrapedAt`.

#### Profile fields

`url`, `did`, `handle`, `displayName`, `description`, `avatar`, `banner`, `followersCount`, `followsCount`, `postsCount`, `createdAt`, `verified`, `pinnedPostUri`, `listsCount`, `feedsCount`, `starterPacksCount`, `isLabeler`, `labels`, `relation` (`self` / `follower` / `follow` / `list-member`), `sourceType`, `source`, `isNew`, `firstSeenAt`, `scrapedAt`.

Follower, follow and list-member rows come from Bluesky's list endpoints. Those endpoints don't include counts, so the count fields on those rows are `null`; the profiles you put in **Profiles** have full counts.

### How to scrape Bluesky

1. Click **Try for free** (or **Start**) and open the **Input** tab.
2. In **Profiles**, paste handles (`bsky.app`), profile links, or any post, feed or list link.
3. Choose how many posts per profile, and optionally followers, threads, date or language filters.
4. Set **Max results** to 0 for full runs, or keep a cap while you test.
5. Click **Start**, then download the data as JSON, CSV or Excel, or use the API.
6. For monitoring, turn on **Only new items** and add a schedule.

### Example input

```json
{
  "profiles": ["bsky.app", "https://bsky.app/profile/jay.bsky.team"],
  "posts": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"],
  "feeds": ["https://bsky.app/profile/bsky.app/feed/whats-hot"],
  "postsPerProfile": 100,
  "maxFollowersPerProfile": 500,
  "postedWithinDays": 30
}
```

### Example output

Real items from a run on the `bsky.app` profile (a few fields left out for space). A post:

```json
{
  "itemType": "post",
  "url": "https://bsky.app/profile/bsky.app/post/3mgdifcgakc2e",
  "postId": "3mgdifcgakc2e",
  "authorHandle": "bsky.app",
  "authorDisplayName": "Bluesky",
  "text": "v1.118 rolls out today!\n\nNew in this update: You can now translate posts without leaving the app.\n\nSimply tap \"translate\" on mobile and your phone's built-in translation takes it from there.",
  "createdAt": "2026-03-05T19:03:11.893Z",
  "langs": [
    "en"
  ],
  "likeCount": 5918,
  "repostCount": 1030,
  "replyCount": 280,
  "quoteCount": 340,
  "bookmarkCount": 124,
  "isReply": false,
  "isRepost": false,
  "isPinned": false,
  "embedType": "images",
  "images": [
    {
      "url": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreieemkkxghh3rnfm374crqr7b42cg5blhp6qb26kt3n4yhwnblw2ya",
      "alt": "A screenshot of the Bluesky app demonstrating how you can now tap the \"Translate\" button…"
    }
  ],
  "hashtags": [],
  "sourceType": "profile",
  "source": "bsky.app",
  "isNew": true,
  "scrapedAt": "2026-10-31T05:13:09.881Z"
}
```

A profile:

```json
{
  "itemType": "profile",
  "did": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "handle": "bsky.app",
  "displayName": "Bluesky",
  "description": "official Bluesky account (check username👆)\n\nBugs, feature requests, feedback: support@bsky.app",
  "url": "https://bsky.app/profile/bsky.app",
  "followersCount": 35460128,
  "followsCount": 32,
  "postsCount": 907,
  "createdAt": "2023-04-12T04:53:57.057Z",
  "starterPacksCount": 15,
  "relation": "self",
  "sourceType": "profile",
  "source": "bsky.app"
}
```

### Filters

- **Author feed filter:** posts only, posts and replies, posts and self-threads, media only, or video only.
- **Include reposts** and **include pinned post** toggles.
- **Date window:** *Posted on or after*, *Posted on or before*, or *Posted within last N days*. Author feeds stop paginating as soon as they pass the start date, so you don't pay for pages you don't need.
- **Languages:** keep only posts tagged e.g. `en` or `ja`.
- **Limits:** a cap per source (posts, followers, replies, members), plus **Max results**, a total that is **split fairly** across sources so one big account can't crowd out the rest.

### Monitoring: only new posts and followers

Turn on **Only new items** and schedule the actor (for example hourly). Each run outputs only posts and profiles that earlier runs with the same **State store name** didn't output. That covers new posts, new replies in a thread, new followers, and new list members. Author feeds and follower lists stop paginating at the first page that is entirely already seen, so repeat runs are cheap. Each item also carries `isNew` and `firstSeenAt`, even when the mode is off. Use a different state store name for each monitoring job.

### How much does it cost to scrape Bluesky?

| Event | Price |
|---|---|
| Actor start (once per run, 512 MB default) | $0.005 |
| **Post** written to the dataset | **$0.0008** ($0.80 per 1,000) |
| Profile written to the dataset | $0.0005 ($0.50 per 1,000) |

You pay only for items actually written. Filtered-out, duplicate and already-seen items are free. Writes are batched and charged exactly, and the run stops cleanly at your *maximum cost per run*. Example: 1,000 posts plus 10 profiles cost about $0.81.

### Rate limits and good citizenship

- Requests go through one shared polite limiter, 4 requests/s by default. Bluesky publishes a limit of 3,000 requests per 5 minutes per IP for the public AppView. HTTP 429 responses are retried after the `RateLimit-Reset` / `Retry-After` time, and 5xx errors get exponential backoff.
- **No keyword search.** Bluesky has asked developers not to automate its search endpoint, so this actor doesn't do it. Use custom feeds, lists or accounts instead.
- **Opt-outs are respected.** Accounts that set "Discourage apps from showing my account to logged-out users" carry the `!no-unauthenticated` label. They are skipped wherever they appear and counted in `RUN_SUMMARY` as `skippedOptOut`.

### Run summary

The default key-value store has a `RUN_SUMMARY` record. For each source it shows fetched, matched, new, quota and output counts, opt-out skips and errors, plus input errors and HTTP stats. Bad or missing inputs are reported there and don't fail the run.

### FAQ

**Do I need a Bluesky account or login?** No. Everything comes from Bluesky's public API that serves logged-out visitors.

**Can I search Bluesky posts by keyword?** Not with this Actor. Bluesky has asked developers not to automate its search endpoint, so we don't. Use custom feeds (many topic feeds exist), lists or accounts instead.

**How many followers can I export?** Set **Max followers per profile**. Large accounts can have millions; the run pages through them politely and you pay $0.50 per 1,000 profiles.

**Can I export to CSV or Google Sheets?** Yes. Download any dataset as CSV, Excel or JSON, or send it to Google Sheets with Apify's integration.

**How many posts can I get on the free plan?** Apify's free plan includes $5 of monthly usage, which covers about 6,000 posts with this Actor.

**Why was an account skipped?** Accounts that ask apps not to show them to logged-out users are skipped on purpose and counted as `skippedOptOut` in `RUN_SUMMARY`.

### More scrapers from cleanfeed-data

All of our Actors use only official, documented public APIs and feeds, charge only for results, and have a new-items-only monitoring mode.

| Actor | What it does |
|---|---|
| [Greenhouse, Lever & Ashby Jobs Scraper](https://apify.com/cleanfeed-data/multi-ats-jobs) | Every open job from company career pages on 8 ATSs, with new-job alerts |
| [App Store Reviews Scraper](https://apify.com/cleanfeed-data/app-store-reviews) | Apple App Store reviews from many countries, with sentiment and topic tags |

### Legal note

This actor only reads data that Bluesky's official public API serves to anyone without logging in. It does not log in or get around any access control, and it honours account opt-out labels. You are responsible for how you use the data. Comply with the [Bluesky Terms of Service](https://bsky.social/about/support/tos), the [Bluesky developer guidelines](https://docs.bsky.app/docs/support/developer-guidelines), and applicable privacy laws (e.g. GDPR/CCPA) when you store or process personal data. Honour deletions: if you keep data long term, re-check it and delete posts or accounts that have been removed. Don't use the data for spam, harassment, surveillance of individuals, or to train models against users' stated wishes. This actor is not affiliated with or endorsed by Bluesky Social PBC.

# Actor input Schema

## `profiles` (type: `array`):

Bluesky accounts as handles (<code>bsky.app</code>, <code>@jay.bsky.team</code>), DIDs (<code>did:plc:…</code>) or profile URLs (<code>https://bsky.app/profile/…</code>). For each one you get the profile, recent posts and optionally followers/follows. Post, feed and list URLs pasted here are routed automatically.

## `posts` (type: `array`):

Post URLs (<code>https://bsky.app/profile/{handle}/post/{id}</code>) or <code>at://</code> URIs. Returns the post plus its reply tree.

## `feeds` (type: `array`):

Custom feed URLs (<code>https://bsky.app/profile/{handle}/feed/{name}</code>) or <code>at://…/app.bsky.feed.generator/…</code> URIs.

## `lists` (type: `array`):

List URLs (<code>https://bsky.app/profile/{handle}/lists/{id}</code>) or <code>at://…/app.bsky.graph.list/…</code> URIs. Returns members and/or the list's post feed (see List mode).

## `includeProfileDetails` (type: `boolean`):

Output one profile item (bio, follower/following/post counts, avatar, created date…) for each account in Profiles.

## `postsPerProfile` (type: `integer`):

Most recent posts to fetch from each profile's author feed (after filters). 0 = profile only.

## `authorFeedFilter` (type: `string`):

Which posts to take from each profile.

## `includeReposts` (type: `boolean`):

Include posts the account reposted (marked with isRepost, repostedByHandle and repostedAt).

## `includePinned` (type: `boolean`):

Include the profile's pinned post (marked isPinned). It does not count toward Posts per profile.

## `maxFollowersPerProfile` (type: `integer`):

Fetch up to this many followers (newest first) for each profile as profile items with relation = "follower". 0 = off.

## `maxFollowsPerProfile` (type: `integer`):

Fetch up to this many accounts each profile follows, as profile items with relation = "follow". 0 = off.

## `threadDepth` (type: `integer`):

How many levels of replies to fetch below each post in Posts / threads. 0 = only the post itself.

## `includeThreadParents` (type: `boolean`):

If the post is itself a reply, also output the posts above it (negative threadDepth).

## `maxRepliesPerThread` (type: `integer`):

Cap on replies output per thread (the root post is always included).

## `postsPerFeed` (type: `integer`):

Posts to fetch from each custom feed, in the feed's own order.

## `listMode` (type: `string`):

What to fetch for each list.

## `maxMembersPerList` (type: `integer`):

Max members to fetch from each list.

## `postsPerList` (type: `integer`):

Max posts to fetch from each list's feed (List mode = posts or both).

## `dateFrom` (type: `string`):

Only posts created on/after this date (YYYY-MM-DD or ISO timestamp). Author feeds stop paginating once they pass it. Thread root posts are always kept.

## `dateTo` (type: `string`):

Only posts created on/before this date (YYYY-MM-DD is inclusive of the whole day).

## `postedWithinDays` (type: `integer`):

Shortcut for a rolling date window. 0 = off. If both this and 'Posted on or after' are set, the later date wins.

## `languages` (type: `array`):

Only keep posts tagged with one of these language codes (e.g. en, ja, pt). Empty = all.

## `onlyNew` (type: `boolean`):

Output only posts/profiles not output by a previous run with the same state store. Author feeds and follower lists stop paginating as soon as they reach already-seen items. Ideal for scheduled runs.

## `stateStoreName` (type: `string`):

Named key-value store that remembers what was already output (used to set isNew and for Only new items). Use a different name per monitoring task.

## `maxResults` (type: `integer`):

Hard cap on items output in this run, split fairly across all sources (0 = no cap; per-source limits below still apply). Prefilled to 25 so your first test run is quick and cheap; set 0 for full runs. Also caps your cost ($0.80 per 1,000 posts).

## `maxConcurrency` (type: `integer`):

Sources processed in parallel. All requests share one polite rate limiter.

## `requestDelayMs` (type: `integer`):

Minimum gap between requests to the Bluesky API. 250 ms (4 req/s) stays far below Bluesky's published limit of 3,000 requests per 5 minutes per IP. 429 responses are retried after the RateLimit-Reset time.

## Actor input object example

```json
{
  "profiles": [
    "bsky.app"
  ],
  "includeProfileDetails": true,
  "postsPerProfile": 100,
  "authorFeedFilter": "posts_no_replies",
  "includeReposts": false,
  "includePinned": true,
  "maxFollowersPerProfile": 0,
  "maxFollowsPerProfile": 0,
  "threadDepth": 6,
  "includeThreadParents": false,
  "maxRepliesPerThread": 1000,
  "postsPerFeed": 100,
  "listMode": "members",
  "maxMembersPerList": 1000,
  "postsPerList": 100,
  "postedWithinDays": 0,
  "onlyNew": false,
  "stateStoreName": "bluesky-data-state",
  "maxResults": 25,
  "maxConcurrency": 3,
  "requestDelayMs": 250
}
```

# Actor output Schema

## `posts` (type: `string`):

All items from this run, opened in the Posts view.

## `profiles` (type: `string`):

Profile rows: input accounts, followers/follows and list members.

## `runSummary` (type: `string`):

JSON record with per-source fetched, matched, new, quota and output counts, opt-out skips, errors and HTTP stats.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "profiles": [
        "bsky.app"
    ],
    "maxResults": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanfeed-data/bluesky-data").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "profiles": ["bsky.app"],
    "maxResults": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("cleanfeed-data/bluesky-data").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "profiles": [
    "bsky.app"
  ],
  "maxResults": 25
}' |
apify call cleanfeed-data/bluesky-data --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cleanfeed-data/bluesky-data"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/oi56ZsnqIXDNWEI5v/builds/gDa8tt3m1tebaDNJd/openapi.json
