# Bluesky Scraper (`zerobreak/bluesky-scraper`) Actor

Bluesky Scraper is a Python tool designed to extract public posts, user profiles, feeds, engagement metrics, and follower graphs via the AT Protocol API. It exports structured social data into clean CSV or JSON formats for sentiment analysis, social listening, and trend research.

- **URL**: https://apify.com/zerobreak/bluesky-scraper.md
- **Developed by:** [ZeroBreak](https://apify.com/zerobreak) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.99 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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: Export Posts, Profiles, Followers and Search Results

<p align="center">
  <img src="https://imgur.com/nZ1t8yK.png" alt="Bluesky Scraper" width="800" />
</p>

### Overview

The Bluesky Scraper collects publicly available posts, profiles, followers, following lists, threads, likes, reposts, and search results. No Bluesky account, app password, or login is required.

Each run produces a dataset that you can export as JSON, CSV, or Excel.

### Features

- Search public posts and users by keyword.
- Collect posts from specified user profiles.
- Look up profiles by handle, DID, or profile URL.
- Retrieve followers and following lists.
- Collect post threads and replies.
- Identify users who liked or reposted a post.
- Enrich profile records with follower and post counts.
- Extract available email addresses from profile bios.
- Preserve post text, timestamps, engagement counts, hashtags, mentions, links, images, and other metadata.
- Export results as JSON, CSV, or Excel.
- Save errors for individual inputs without stopping the entire run.
- Handle rate limits, retries, and request timeouts.

### Use Cases

- **Social listening:** Monitor public discussions about brands, products, or keywords.
- **Lead discovery:** Find relevant accounts and inspect public profile information.
- **Research datasets:** Collect posts and threads, including non-English text and emoji.
- **Competitor monitoring:** Track public posts from selected accounts.
- **Community analysis:** Examine followers, following relationships, likes, and reposts.

### Input Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `search_posts` | Collection mode: `search_posts`, `search_users`, `author_posts`, `profiles`, `followers`, `follows`, `post_thread`, `post_likes`, or `post_reposts`. |
| `searchQueries` | array | — | Keywords or phrases for post and user searches. |
| `searchSort` | string | `latest` | Search order: `latest` or `top`. |
| `handles` | array | — | Account handles, DIDs, or profile URLs. |
| `postUrls` | array | — | Post URLs or `at://` URIs. |
| `feedFilter` | string | `posts_no_replies` | Filter for author feeds. Reposts may still appear. |
| `includeReposts` | boolean | `false` | Include reposts in author feeds. |
| `enrichProfiles` | boolean | `false` | Add follower and post counts to profile rows, using batches of 25 profiles. |
| `threadDepth` | integer | `6` | Number of reply levels to collect. `0` collects only the root post. |
| `dateFrom` | string | — | Collect posts after a specified date where supported for author feeds and latest-sorted searches. |
| `maxItems` | integer | `100` | Overall result limit, capped at 5,000. |
| `maxItemsPerInput` | integer | `100` | Result limit per query, handle, or post. |
| `maxConcurrency` | integer | `5` | Maximum number of concurrent inputs. Cannot exceed 5. |
| `timeoutSecs` | integer | `300` | Maximum run duration in seconds. |
| `requestTimeoutSecs` | integer | `30` | Timeout for an individual request. |

### Example Inputs

#### 1. Search Posts

Search public posts for a keyword and collect the latest results.

```json
{
  "mode": "search_posts",
  "searchQueries": ["web scraping"],
  "searchSort": "latest",
  "maxItems": 100
}
```

#### 2. Collect Posts from a Profile

Collect posts from a selected account while excluding reposts where possible.

```json
{
  "mode": "author_posts",
  "handles": ["bsky.app"],
  "feedFilter": "posts_no_replies",
  "includeReposts": false,
  "maxItemsPerInput": 50
}
```

Some accounts repost frequently. Keep `includeReposts` disabled if you primarily want posts written by the account itself.

#### 3. Collect Profiles

Look up profiles using handles, profile URLs, or decentralized identifiers.

```json
{
  "mode": "profiles",
  "handles": [
    "bsky.app",
    "OPEN_PROFILE_URL",
    "did:plc:z72i7hdynmk6r22z27h6tvur"
  ]
}
```

#### 4. Search Users

Find accounts matching a keyword and enrich their profile records.

```json
{
  "mode": "search_users",
  "searchQueries": ["apify"],
  "enrichProfiles": true,
  "maxItems": 25
}
```

#### 5. Collect Followers

Retrieve an account's followers and enrich the resulting profile records.

```json
{
  "mode": "followers",
  "handles": ["apify.com"],
  "enrichProfiles": true,
  "maxItemsPerInput": 100
}
```

Use `mode: "follows"` to collect accounts that a selected user follows. Relationship records include fields such as `relation` and `subjectHandle`.

#### 6. Collect a Post Thread

Retrieve a post and its replies up to the specified depth.

```json
{
  "mode": "post_thread",
  "postUrls": ["SOURCE_POST_URL"],
  "threadDepth": 2,
  "maxItems": 100
}
```

Use `post_likes` or `post_reposts` to collect users who liked or reposted a specified post. Like records can include `likedAt` and `postUri`; repost records identify the relationship using `reposter`.

### Output Data

The dataset includes three table views:

- **Posts:** Post content, metadata, engagement metrics, and thread information.
- **Profiles:** Public profile details and available account statistics.
- **Errors:** Failed inputs and their associated error details.

#### Profile Fields

| Field | Description |
|---|---|
| `did` | Decentralized identifier of the account. |
| `handle` | Account handle. |
| `displayName` | Public display name. |
| `description` | Profile description or bio. |
| `profileBio` | Profile bio text used in the dataset view. |
| `followersCount` | Number of followers, when available. |
| `followsCount` | Number of accounts followed, when available. |
| `postsCount` | Number of posts, when available. |
| `createdAt` | Account creation timestamp, when available. |
| `url` | Profile URL. |
| `emailsInBio` | Email addresses detected in the public bio. |
| `verifiedStatus` | Available verification status. |

The `description` and `profileBio` fields contain the same profile text.

#### Post Fields

Post records can include:

- `uri`, `cid`, `url`, and `text`
- `createdAt` and `indexedAt`
- `langs` and author details
- `replyCount`, `repostCount`, `likeCount`, `quoteCount`, and `bookmarkCount`
- `isRepost` and `repostedBy`
- `isReply` and reply URIs
- `embedType` and image information, including URLs and alt text
- External link metadata, including URI, title, and description
- `quotedPostUri`, hashtags, mentions, links, and labels
- `depth` for thread records
- `searchQuery` and `sourceInput` to identify the input associated with a result
- `scrapedAt` to record when the item was collected

The fields available in each record depend on the source data and collection mode.

#### Error Records

A missing or invalid handle does not necessarily stop the run. The dataset can record an error row containing:

- `errorMessage`
- `httpStatus`
- `sourceInput`

Other inputs continue processing when possible.

### Limits and Performance

- **Public data only:** Private or inaccessible content is not collected.
- **Maximum results:** The free collection limit is 5,000 results per run.
- **Search coverage:** Results depend on what the platform has indexed and made available.
- **Large accounts:** Accounts with millions of followers can produce large datasets. Set `maxItems` and `maxItemsPerInput` carefully.
- **Feed ordering:** Author feeds are collected newest first. Reposts may appear even with the `posts_no_replies` filter.
- **Date filtering:** `dateFrom` is supported for author feeds and latest-sorted searches. It is not applied to top-sorted search results because that ordering is not chronological.
- **Concurrency:** The maximum concurrency is five.
- **Rate limiting:** Requests are spaced out, and HTTP 429 or 5xx responses trigger backoff. When available, server-provided retry timing is respected.
- **Profile enrichment:** Follower and post counts are retrieved in batches of 25 profiles.

### FAQ

#### Do I need a Bluesky account?

No. The scraper collects publicly available information without requiring a login or app password.

#### How is this different from the firehose?

The firehose is a live stream of network events. This scraper retrieves selected searches, accounts, and posts, then stops when the configured limits are reached. It is designed for targeted data collection rather than continuous monitoring of every event.

#### Can user search results include follower counts?

Yes. Enable `enrichProfiles` to retrieve follower and post counts for matching profiles. Without enrichment, those counts may not be included in the original search results.

#### What happens if a handle does not exist?

The scraper can record the returned error, including the message and HTTP status, while continuing with other inputs.

#### Can I export the results?

Yes. Download the dataset as JSON, CSV, or Excel after the run.

#### Can I automate collection?

You can schedule recurring runs using your preferred workflow platform and use the exported dataset for analysis or reporting.

### Trademark Notice

Bluesky and related names, logos, and trademarks belong to their respective owners. Their use in this documentation is solely to identify the source of the data and does not imply affiliation, sponsorship, endorsement, or partnership.

### Disclaimer

This is an independent data collection tool. It is not affiliated with, endorsed by, sponsored by, or officially associated with Bluesky.

### Is Web Scraping Legal?

Collecting information that is already public is generally legal. Scraping is not the same thing as hacking, and there is no single law that bans it. The limits are the usual ones: do not break into accounts, do not take private or confidential data, and be careful with personal data and copyrighted works. A site's terms can also restrict how you access it. Facts such as a public name, price, or rating are not the same thing as republishing someone's article or photo.

This actor collects information from public pages. It does not log in and it does not bypass access controls.
This is not legal advice. For the longer explanation, see Apify's article [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/).

# Actor input Schema

## `mode` (type: `string`):

Choose posts, profiles, search, followers, follows, a thread, likes, or reposts.

## `searchQueries` (type: `array`):

Queries for search_posts and search_users. One query per line.

## `searchSort` (type: `string`):

Sort order for search_posts. latest is newest first. top is Bluesky's top ranking. dateFrom only stops early when this is latest.

## `handles` (type: `array`):

Profiles, author feeds, followers, and follows. Accepts bsky.app, @jay.bsky.team, did:plc, did:web, or a https://bsky.app/profile/ URL.

## `postUrls` (type: `array`):

Threads, likes, and reposts. A bsky.app post link or an at:// URI.

## `feedFilter` (type: `string`):

Passed to getAuthorFeed. posts_no_replies still includes reposts of other people's posts. Use Include reposts to drop those.

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

When off, author feeds skip items whose reason is a repost. Search and threads are unchanged.

## `enrichProfiles` (type: `boolean`):

Extra getProfiles call per 25 users for search_users, followers, follows, likes, and reposts. Those lists do not include follower counts on their own.

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

How many reply levels to walk on a post thread. 0 keeps only the root post.

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

Skip older posts and stop paging. Applies to author feeds and to search_posts when sort is latest. YYYY-MM-DD or an ISO timestamp.

## `maxItems` (type: `integer`):

Stop after this many posts or profiles. Free version cap is 5,000 per run. Error rows do not count toward this cap.

## `maxItemsPerInput` (type: `integer`):

Cap for each search query, handle, or post URL. Also limited by max results (total).

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

How many handles, queries, or posts to fetch at once. Hard cap is 5.

## `timeoutSecs` (type: `integer`):

Stop starting new requests after this many seconds. The run still saves rows already collected.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for a single Bluesky API response before retrying.

## `proxyConfiguration` (type: `object`):

Optional proxy for Bluesky requests. Leave this off unless your IP is blocked. The public API answered from a datacenter without a proxy in testing.

## Actor input object example

```json
{
  "mode": "search_posts",
  "searchQueries": [
    "web scraping"
  ],
  "searchSort": "latest",
  "handles": [
    "bsky.app",
    "https://bsky.app/profile/apify.com"
  ],
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"
  ],
  "feedFilter": "posts_no_replies",
  "includeReposts": false,
  "enrichProfiles": false,
  "threadDepth": 6,
  "maxItems": 100,
  "maxItemsPerInput": 100,
  "maxConcurrency": 5,
  "timeoutSecs": 300,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Posts from search, author feeds, and threads, with text, counts, and the bsky.app URL.

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

Profiles and user rows, including followers, follows, likers, and reposters.

## `errors` (type: `string`):

Inputs that failed, with Bluesky's message and HTTP status. The run keeps going.

## `dataset` (type: `string`):

Every row from the run, without a table view applied.

# 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 = {
    "searchQueries": [
        "web scraping"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zerobreak/bluesky-scraper").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 = {
    "searchQueries": ["web scraping"],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("zerobreak/bluesky-scraper").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 '{
  "searchQueries": [
    "web scraping"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call zerobreak/bluesky-scraper --silent --output-dataset

```

## MCP server setup

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

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/BaeBBsGXy2bQNcQL8/builds/cdEiH7vkR0v8QFAuf/openapi.json
