# Bluesky Scraper - Posts, Profiles, Threads, Followers (`thirdwatch/bluesky-scraper`) Actor

Scrape Bluesky (bsky.app) without login: keyword post search (latest/top, date, language, author, hashtag, domain filters), author feeds with engagement counts and media, profiles, full reply threads, followers, follows and people search. Pure HTTP via the public AT Protocol API.

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

## Pricing

from $2.00 / 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 - Posts, Profiles, Threads, Followers

### What you get

Scrape **Bluesky (bsky.app)** without an account, an app password or a browser. This Actor talks to the public AT Protocol AppView over plain HTTP, so it is fast, cheap and does not risk any login of yours. One Actor covers seven jobs:

| Mode | You give | You get |
|------|----------|---------|
| `searchPosts` | keywords (+ your app password) | posts matching the query, newest-first or ranked by engagement, with date / language / author / hashtag / domain filters |
| `authorFeed` | handles | an account's posts with like, repost, reply, quote and bookmark counts, media, quoted posts and a repost flag |
| `profile` | handles or DIDs | one profile record per account: bio, follower / following / post counts, avatar, banner, verification |
| `thread` | post URLs | the post plus every reply below it, flattened with a `depth` field |
| `followers` | handles | who follows the account |
| `follows` | handles | who the account follows |
| `searchActors` | keywords | people search: profiles whose name, handle or bio matches |

It accepts plain handles (`bsky.app`), `@handles`, DIDs (`did:plc:...`), `at://` URIs and any bsky.app URL (profile, post, followers, follows, hashtag, search) and routes each one to the right mode automatically.

**One exception to "no login":** in 2026 Bluesky switched off post search for logged-out clients (bsky.app itself shows *"Search is currently unavailable when logged out"*). Six of the seven modes above are fully public. `searchPosts` alone needs a Bluesky **app password** of your own, passed in the optional `identifier` / `appPassword` fields; the Actor opens a session on your account's PDS and runs the search through it. Never use your main password - create an app password under *Settings -> Privacy and security -> App passwords* and revoke it whenever you like.

Pricing is **$0.50 per 1,000 records** on the free plan and drops on paid Apify plans. There is no platform fee on top; you pay only for what lands in the dataset.

### Why scrape Bluesky?

Bluesky has grown into one of the largest open social networks, and unlike most platforms its data model (AT Protocol) is designed to be read publicly. That makes it the easiest place to get real, timestamped, engagement-scored social data for:

- **Brand and keyword monitoring** - track mentions of your product, competitor or campaign hashtag as they happen with `searchPosts` sorted by `latest`.
- **Audience research** - pull an account's followers or the accounts it follows, then profile them.
- **Influencer discovery** - `searchActors` for a niche, then `profile` the shortlist to compare follower counts and posting volume.
- **Content analysis** - export an author's full feed with engagement counts to see what performs.
- **Conversation mining** - pull a viral post's whole reply thread with `thread` for sentiment or Q\&A datasets.
- **Academic and journalism datasets** - reproducible keyword pulls with `since` / `until` windows and language filters.

### Input

All fields are optional; give at least one of `queries`, `handles` or `postUrls`.

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `mode` | select | `auto` | `auto` derives the mode from your inputs: `postUrls` -> thread, `queries` -> searchPosts, `handles` -> authorFeed. Set `profile`, `followers`, `follows` or `searchActors` explicitly. |
| `queries` | list of strings | | Keywords for `searchPosts` / `searchActors`. Inline operators work: `from:handle`, `mentions:handle`, `lang:en`, `since:2026-01-01`, `#tag`, `domain:example.com`. |
| `handles` | list of strings | | Accounts for `authorFeed` / `profile` / `followers` / `follows`. Handle, `@handle`, DID or profile URL. |
| `postUrls` | list of strings | | Posts for `thread`. `https://bsky.app/profile/{handle}/post/{id}` or `at://` URI. Profile / followers / follows / hashtag / search URLs are accepted here too and routed by shape. |
| `maxItems` | integer | `100` | Cap **per keyword / handle / post URL** (1 - 10,000). `profile` mode always returns one record per handle. |
| `sort` | `latest` / `top` | `latest` | searchPosts ordering. |
| `since`, `until` | date | | searchPosts window. `YYYY-MM-DD` or full ISO 8601. |
| `language` | string | | searchPosts. BCP-47 code such as `en`, `pt`, `ja`. |
| `author` | string | | searchPosts. Only posts by this handle / DID. |
| `mentions` | string | | searchPosts. Only posts mentioning this handle / DID. |
| `hashtag` | string | | searchPosts. Only posts carrying this tag. |
| `domain` | string | | searchPosts. Only posts linking to this domain. |
| `identifier` | string | | searchPosts only. Your Bluesky handle or email. |
| `appPassword` | secret string | | searchPosts only. An app password for that account (encrypted at rest by Apify). |
| `includeReplies` | boolean | `false` | authorFeed. Include the account's replies to other people. |
| `includeReposts` | boolean | `true` | authorFeed. Include reposts (flagged `is_repost: true`). |
| `threadDepth` | integer | `6` | thread. Reply levels to collect (1 = direct replies only, max 100). |
| `minLikes`, `minReposts`, `minReplies` | integer | `0` | Drop posts / replies below these counts. Applied locally after fetching. |

#### Example: keyword search, English, last week, top posts

```json
{
  "queries": ["open source AI", "#rustlang"],
  "sort": "top",
  "since": "2026-09-01",
  "language": "en",
  "maxItems": 200,
  "identifier": "you.bsky.social",
  "appPassword": "xxxx-xxxx-xxxx-xxxx"
}
```

#### Example: an account's posts plus its followers

```json
{
  "handles": ["bsky.app"],
  "postUrls": ["https://bsky.app/profile/bsky.app/followers"],
  "maxItems": 500,
  "includeReposts": false
}
```

#### Example: full reply thread of a post

```json
{
  "postUrls": ["https://bsky.app/profile/cee.wtf/post/3muplmkx56s2k"],
  "threadDepth": 10,
  "maxItems": 2000
}
```

#### Example: profiles for a list of handles

```json
{
  "mode": "profile",
  "handles": ["bsky.app", "jay.bsky.team", "did:plc:z72i7hdynmk6r22z27h6tvur"]
}
```

### Output

Every record has a `kind` discriminator (`post`, `reply`, `profile`, `follower`, `follow`), a `query` field naming the input that produced it, and `scraped_at`. Records are deduplicated by post URI or account DID across the whole run.

#### Post and reply records

```json
{
  "kind": "post",
  "uri": "at://did:plc:xb2urvqt5f4zzccjs46hysbf/app.bsky.feed.post/3muplmkx56s2k",
  "cid": "bafyreidu2rxt55wzm3wmadbp6fzfrik6idvmh34qk23gwsssx6fqdgxbqm",
  "id": "3muplmkx56s2k",
  "url": "https://bsky.app/profile/cee.wtf/post/3muplmkx56s2k",
  "text": "throng.cee.wtf i've invented a new way to browse bluesky",
  "created_at": "2026-09-04T18:25:36.591Z",
  "indexed_at": "2026-09-04T18:26:09.958Z",
  "langs": ["en"],
  "author_did": "did:plc:xb2urvqt5f4zzccjs46hysbf",
  "author_handle": "cee.wtf",
  "author_display_name": "cee",
  "author_avatar": "https://cdn.bsky.app/img/avatar/plain/...",
  "author_verified": false,
  "author_url": "https://bsky.app/profile/cee.wtf",
  "like_count": 7838,
  "repost_count": 1856,
  "reply_count": 339,
  "quote_count": 622,
  "bookmark_count": 1492,
  "is_reply": false,
  "reply_parent_uri": null,
  "reply_root_uri": null,
  "is_repost": false,
  "reposted_by_did": null,
  "reposted_by_handle": null,
  "is_pinned": false,
  "links": ["https://throng.cee.wtf"],
  "mentions": [],
  "tags": [],
  "labels": [],
  "embed_type": "video",
  "images": [],
  "video": {
    "playlist": "https://video.bsky.app/watch/.../playlist.m3u8",
    "thumbnail": "https://video.bsky.app/watch/.../thumbnail.jpg",
    "alt": "",
    "width": 1874,
    "height": 1298
  },
  "external": null,
  "quoted_post": null,
  "query": "https://bsky.app/profile/cee.wtf/post/3muplmkx56s2k",
  "scraped_at": "2026-09-07T17:38:52+00:00"
}
```

Field notes:

- `embed_type` is one of `images`, `video`, `external` (link card), `record` (quote post), `recordWithMedia` (quote + media), `gallery`, or `null`.
- `images` is a list of `{thumb, fullsize, alt, width, height}`. `external` is `{uri, title, description, thumb}`.
- `quoted_post` carries the quoted post's `uri`, `url`, author, `text`, `created_at` and its own engagement counts, or `{"uri": ..., "unavailable": "viewNotFound" | "viewBlocked" | "viewDetached"}` when it cannot be shown.
- `links`, `mentions` (DIDs) and `tags` are parsed from the post's rich-text facets, so links are the real targets rather than truncated display text.
- `is_repost` / `reposted_by_handle` are set on `authorFeed` entries the account reposted; the record itself is the original post by its original author.
- Reply records from `thread` add `source_post_uri`, `source_post_url`, `source_post_author_handle` and `depth` (1 = direct reply to the post). The root post is included as `kind: "post"` with `depth: 0`.
- `labels` are moderation labels applied to the post (for example `!no-unauthenticated` on the author).

#### Profile, follower and follow records

```json
{
  "kind": "profile",
  "did": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "handle": "bsky.app",
  "display_name": "Bluesky",
  "description": "official Bluesky account ...",
  "avatar": "https://cdn.bsky.app/img/avatar/plain/...",
  "banner": "https://cdn.bsky.app/img/banner/plain/...",
  "url": "https://bsky.app/profile/bsky.app",
  "followers_count": 34817452,
  "follows_count": 14,
  "posts_count": 827,
  "created_at": "2023-04-12T04:53:57.057Z",
  "indexed_at": "2025-10-27T21:05:26.152Z",
  "verified": true,
  "is_labeler": false,
  "lists_count": 17,
  "feedgens_count": 7,
  "starter_packs_count": 14,
  "pinned_post_uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l",
  "labels": [],
  "query": "bsky.app",
  "scraped_at": "2026-09-07T17:38:50+00:00"
}
```

`follower` and `follow` records share this shape and add `subject_did`, `subject_handle` and `subject_display_name` (the account whose graph you asked for). Note that the Bluesky graph endpoints return the compact profile view, so `followers_count`, `follows_count`, `posts_count`, `banner` and the `*_count` association fields are `null` on those records; run `profile` mode on the DIDs if you need the counts.

### How it works

The Actor calls the unauthenticated public AppView (`public.api.bsky.app`) endpoints defined by the AT Protocol lexicons: `app.bsky.feed.searchPosts`, `app.bsky.feed.getAuthorFeed`, `app.bsky.actor.getProfiles`, `app.bsky.feed.getPostThread`, `app.bsky.graph.getFollowers`, `app.bsky.graph.getFollows`, `app.bsky.actor.searchActors`, and `com.atproto.identity.resolveHandle` to turn a handle in a post URL into the DID an `at://` URI needs. Pages of up to 100 items are walked with the server cursor until `maxItems` is reached or the feed ends. Between pages the Actor pauses briefly to stay far below Bluesky's published limit of 3,000 requests per five minutes per IP, and it backs off on `429`.

Because everything is a JSON API call, one run of 1,000 posts typically finishes in well under a minute on 256 MB of memory.

### Zero results and failures

The Actor distinguishes between "Bluesky answered and there was nothing" and "Bluesky could not be reached":

- A keyword with no matches, an account with no posts, or a deleted post produce an empty result for that input and the run still **succeeds**. The status message says so.
- A `searchPosts` input without `identifier` / `appPassword` is reported as unreachable with an explanation; if it was the only thing you asked for, the run **fails** with that message rather than returning an empty dataset.
- If an input is not a valid handle, DID or bsky.app URL it is skipped with a warning.
- If the AppView rejects or never answers **every** input (network outage, edge refusal), the run is marked **failed** so you are not billed for, and do not silently accept, an empty dataset.

### Limitations

- Only **public** data is returned. Accounts that opted out of unauthenticated viewing (`!no-unauthenticated`) show the label and may return no posts.
- `searchPosts` requires your own app password (see above) and is served by Bluesky's search index, which is not exhaustive for very old posts; use `since` / `until` windows for historical pulls. Bluesky rate-limits authenticated sessions per account as well as per IP.
- Followers / follows are returned in the AppView's own order (most recent first). Very large graphs (millions) are cut by `maxItems`.
- Like lists, feeds generators and starter-pack contents are not scraped by this Actor.
- No sentiment or "content category" scoring is added; the output is the raw, complete data so you can run your own models on it.

### Integrations

Use the Apify API or one of the Apify client libraries (Python, JavaScript) to run the Actor and read its dataset, or connect it to Zapier, Make, Google Sheets, Slack, Airbyte or a webhook. Schedules let you re-run a keyword pull hourly or daily to build a rolling archive.

### Related Actors

Other social scrapers by Thirdwatch: Twitter/X, Reddit, TikTok, Instagram, Pinterest, YouTube.

### Legal

This Actor only reads data that Bluesky serves publicly to anyone without an account, through the API the network was designed around. You are responsible for using the output in line with Bluesky's terms, applicable privacy law and the rights of the people whose posts you collect.

### Use cases

- Marketers monitoring social presence and engagement
- Researchers building social-media datasets
- Teams tracking public figures and trends

Last verified: 2026-09

More scrapers at [thirdwatch.dev](https://thirdwatch.dev).

# Actor input Schema

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

What to scrape. 'auto' picks the mode from the inputs you filled in: post URLs -> thread, handles -> authorFeed, keywords -> searchPosts (needs identifier + appPassword). Set it explicitly to use handles for profile / followers / follows, or keywords for searchActors (people search, no login).

## `queries` (type: `array`):

Keywords or phrases for post search (mode searchPosts) or people search (mode searchActors). NOTE: Bluesky disabled post search for logged-out clients, so searchPosts needs your own 'identifier' + 'appPassword' below; searchActors stays public. Inline operators work: "from:handle", "mentions:handle", "lang:en", "since:2026-01-01", "#hashtag", "domain:example.com".

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

Bluesky accounts for authorFeed / profile / followers / follows modes. Accepts a handle (bsky.app, jay.bsky.team), a DID (did:plc:...), an @handle, or a profile URL (https://bsky.app/profile/bsky.app).

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

Posts whose reply threads you want. Accepts https://bsky.app/profile/{handle}/post/{id} URLs or at://did:plc:.../app.bsky.feed.post/... URIs. Also accepted here: profile, followers, follows, hashtag and search URLs from bsky.app - each is routed to the matching mode.

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

Upper limit of records collected for each keyword, handle or post URL. Profile mode always returns one record per handle.

## `sort` (type: `string`):

searchPosts only. 'latest' = newest first (chronological). 'top' = ranked by engagement.

## `since` (type: `string`):

searchPosts only. Only posts created on/after this date. YYYY-MM-DD or full ISO 8601 (2026-01-15T00:00:00Z).

## `until` (type: `string`):

searchPosts only. Only posts created before this date. YYYY-MM-DD or full ISO 8601.

## `language` (type: `string`):

searchPosts only. BCP-47 language code such as en, es, pt, ja, de. Leave empty for all languages.

## `author` (type: `string`):

searchPosts only. Restrict results to one account (handle or DID). Use "me" is not supported - this actor is unauthenticated.

## `mentions` (type: `string`):

searchPosts only. Restrict results to posts that mention this account (handle or DID).

## `hashtag` (type: `string`):

searchPosts only. Restrict results to posts carrying this hashtag (with or without #).

## `domain` (type: `string`):

searchPosts only. Restrict results to posts linking to this domain, e.g. github.com.

## `identifier` (type: `string`):

Your Bluesky handle or email. Only needed for searchPosts: Bluesky blocks post search for logged-out clients. All other modes run without login. Use an app password, never your main password.

## `appPassword` (type: `string`):

An app password created at bsky.app -> Settings -> Privacy and security -> App passwords. Stored encrypted by Apify, used only to open a session for searchPosts.

## `includeReplies` (type: `boolean`):

authorFeed only. Include the account's replies to other people. Default off = original posts (and reposts) only.

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

authorFeed only. Include posts the account reposted (marked is_repost = true with reposted_by). Default on.

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

thread only. How many reply levels below the post to collect (1 = direct replies only).

## `minLikes` (type: `integer`):

Drop posts/replies with fewer likes than this (applied locally after fetching).

## `minReposts` (type: `integer`):

Drop posts/replies with fewer reposts than this (applied locally).

## `minReplies` (type: `integer`):

Drop posts/replies with fewer replies than this (applied locally).

## Actor input object example

```json
{
  "mode": "auto",
  "handles": [
    "bsky.app"
  ],
  "maxItems": 100,
  "sort": "latest",
  "includeReplies": false,
  "includeReposts": true,
  "threadDepth": 6,
  "minLikes": 0,
  "minReposts": 0,
  "minReplies": 0
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {
    "mode": "auto",
    "handles": [
        "bsky.app"
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("thirdwatch/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 = {
    "mode": "auto",
    "handles": ["bsky.app"],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("thirdwatch/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 '{
  "mode": "auto",
  "handles": [
    "bsky.app"
  ],
  "maxItems": 100
}' |
apify call thirdwatch/bluesky-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thirdwatch/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/fHQjfHh4lZTDURhRO/builds/NgjmiJo0bbBsEdddk/openapi.json
