# Bluesky Scraper — Posts, Profiles, Feeds & Followers (`tortuga/bluesky-scraper`) Actor

Scrape Bluesky without login: posts by keyword or hashtag, account feeds, likes, custom feeds, lists and reply threads, plus profiles, followers and following. Monitoring mode outputs only new posts.

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

## Pricing

$1.00 / 1,000 result items

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, Feeds & Followers

Scrape Bluesky without logging in: posts by keyword or hashtag, any account's posts, replies, media or likes, custom feeds, lists, starter packs and full reply threads, plus profile data, followers and following lists. It uses Bluesky's public AT Protocol API, so there is no app password to share and no browser to slow things down: a run of 2,000 posts finishes in about 20 seconds.

Export to JSON, CSV or Excel, or pull the results through the API into Google Sheets, a database or an LLM pipeline. You pay only for the items you get.

### What data does Bluesky Scraper extract?

Every item has `type: "post"` or `type: "profile"`, plus `source` (the search term, account or URL it came from) and `sourceType`.

**Posts**

| Field | Description |
|---|---|
| `uri`, `cid`, `postId` | AT Protocol identifiers of the post |
| `url` | Link to the post on bsky.app |
| `text` | Post text |
| `createdAt`, `indexedAt` | When the post was written and when Bluesky indexed it (UTC ISO 8601) |
| `langs` | Languages the author tagged |
| `authorHandle`, `authorDid`, `authorDisplayName`, `authorAvatar`, `authorUrl`, `authorLabels` | Who wrote it |
| `likeCount`, `repostCount`, `replyCount`, `quoteCount`, `bookmarkCount` | Engagement counts |
| `hashtags`, `mentions` (`did` + `handle`), `links` | From the post's rich-text facets |
| `embedType` | `images`, `video`, `external`, `record` (quote), `recordWithMedia` or null |
| `images` | `url` (full size), `thumbnail`, `alt` text, `width`, `height` |
| `video` | HLS `playlist` URL, `thumbnail`, `alt`, size |
| `externalLink` | Link card: `url`, `title`, `description`, `thumbnail` |
| `quotedPost` | Quoted post: `uri`, `url`, `text`, author, counts, its media |
| `isReply`, `replyParentUri`, `replyRootUri`, `replyToHandle` | Reply context |
| `labels` | Moderation and self-labels (e.g. `porn`, `graphic-media`) |
| `isRepost`, `repostedByHandle`, `repostedAt` | When reposts are included |
| `threadDepth`, `threadRootUri` | For post threads (0 = the post, 1 = direct reply, ...) |
| `likedByHandle`, `likedAt` | For an account's likes |

**Profiles** (profile data, followers, following, list members)

| Field | Description |
|---|---|
| `handle`, `did`, `displayName`, `url` | Who it is |
| `description` | Bio |
| `followersCount`, `followsCount`, `postsCount` | Counts |
| `createdAt` | When the account was created |
| `avatar`, `banner` | Image URLs |
| `labels`, `verifiedStatus`, `pinnedPostUri`, `listsCount`, `feedsCount`, `starterPacksCount`, `isLabeler`, `pronouns`, `website` | Extra profile data |
| `relation`, `relationToHandle` | `profile`, `follower`, `following` or `listMember`, and of which account |
| `listName`, `listUri`, `listPurpose` | For list and starter pack members |

### How to use Bluesky Scraper

1. Pick what you want, and combine as many as you like in one run:
   - **Search terms**: keywords, phrases or hashtags (`#python`, `"open source"`), with optional filters for date range, language, author, mentioned account and linked domain.
   - **Accounts**: handles, DIDs or profile URLs, and **What to get from accounts** (posts, posts and replies, media, videos, likes, followers, following, or profile data only).
   - **Bluesky URLs**: posts (with their reply thread), custom feeds, lists, starter packs, profile pages, `/followers` and `/follows` pages, search and hashtag pages.
2. Set **Max items** (and optionally **Max items per source**) to cap the run and the cost.
3. Click **Start**. Results appear in the **Dataset** tab, with separate views for posts and profiles.

### Input example

```json
{
  "searchTerms": ["#python", "climate change"],
  "searchSort": "latest",
  "since": "7 days",
  "language": "en",
  "profiles": ["nytimes.com"],
  "profileContent": "posts",
  "startUrls": [
    { "url": "https://bsky.app/profile/bsky.app/feed/whats-hot" },
    { "url": "https://bsky.app/profile/bsky.app/post/3mv6jm4auic2g" },
    { "url": "https://bsky.app/profile/bsky.app/followers" }
  ],
  "maxItemsPerSource": 200,
  "maxItems": 1000
}
```

### Output example

```json
{
  "type": "post",
  "uri": "at://did:plc:tartlcpup67v55wn7z35pet6/app.bsky.feed.post/3mxhxudjb5k25",
  "url": "https://bsky.app/profile/coredispatch.xyz/post/3mxhxudjb5k25",
  "postId": "3mxhxudjb5k25",
  "text": "It's been a while, but Core Dispatch 11 is out! Happy Python 3.15 final day to all that celebrate! Congrats to @hugovk.dev ...",
  "createdAt": "2026-10-09T21:58:59.110Z",
  "langs": ["en"],
  "authorHandle": "coredispatch.xyz",
  "authorDid": "did:plc:tartlcpup67v55wn7z35pet6",
  "authorDisplayName": "Core Dispatch",
  "likeCount": 3,
  "repostCount": 1,
  "replyCount": 0,
  "quoteCount": 0,
  "isReply": false,
  "hashtags": [],
  "mentions": [{ "did": "did:plc:3yxbv667vvngelzbmtphu6lf", "handle": "hugovk.dev" }],
  "links": ["https://coredispatch.xyz/editions/11"],
  "embedType": "external",
  "externalLink": { "url": "https://coredispatch.xyz/editions/11", "title": "Core Dispatch #11", "description": "Welcome back to Core Dispatch! ...", "thumbnail": "https://cdn.bsky.app/img/feed_thumbnail/plain/..." },
  "images": [],
  "video": null,
  "quotedPost": null,
  "labels": [],
  "isRepost": false,
  "source": "python",
  "sourceType": "search"
}
```

```json
{
  "type": "profile",
  "did": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "handle": "bsky.app",
  "displayName": "Bluesky",
  "description": "official Bluesky account (check username👆) ...",
  "url": "https://bsky.app/profile/bsky.app",
  "followersCount": 35206293,
  "followsCount": 17,
  "postsCount": 867,
  "createdAt": "2023-04-12T04:53:57.057Z",
  "avatar": "https://cdn.bsky.app/img/avatar/plain/...",
  "banner": "https://cdn.bsky.app/img/banner/plain/...",
  "relation": "profile",
  "source": "bsky.app",
  "sourceType": "profile"
}
```

### How to scrape Bluesky posts by keyword or hashtag

Add the words to **Search terms**, one per line. Hashtags (`#python`), exact phrases (`"machine learning"`) and Bluesky operators (`from:handle`, `lang:en`, `domain:nytimes.com`) all work. **Latest** returns posts newest first and keeps paging back in time until **Max items** or **Posted after** is reached. **Top** returns Bluesky's most relevant posts; without a login Bluesky serves only the first page of top results, so expect up to about 100 top posts per term. Narrow the search with **Posted after/before** (a date or a period such as `7 days`), **Language**, **From account**, **Mentioning account** and **Linking to domain**.

### Does it work without a Bluesky account or app password?

Yes. Everything comes from Bluesky's public AppView API, the same data the bsky.app website shows to logged-out visitors. You never share credentials. Posts that their authors restricted to logged-in users are not returned, and Bluesky's search refuses some filter combinations without a login: a search made only of operators (for example just `from:bsky.app`) returns a single page and cannot use date filters, so add a keyword or use **Accounts** to get someone's full history.

### How to export Bluesky followers and following lists

Put the accounts in **Accounts** and choose **Followers** or **Following** under **What to get from accounts**, or paste the `/followers` and `/follows` page URLs. Each follower comes out as a profile item with handle, display name, bio, avatar, created date and, with **Full profile details** on (default), followers/following/posts counts. Lists and starter packs work the same way with **What to get from lists** set to **Members**.

### Can I scrape a post's replies, custom feeds and lists?

Yes. Paste a post URL to get the post and its whole reply tree (set the depth with **Reply depth**), a custom feed URL (`https://bsky.app/profile/<creator>/feed/<name>`) to get the posts that feed serves, or a list URL to get the recent posts of its members. An account's liked posts are public on Bluesky too: choose **Liked posts** to get them, each with `likedAt`.

### Monitor changes on a schedule

Turn on **Only new posts (monitoring)** to get a feed of what appeared since the last run: the Actor remembers which posts (and followers, likes or list members) earlier runs of the same input returned and outputs only new ones. Items already seen are dropped before they are saved, so they are not charged, and newest-first sources (searches, account posts, followers) stop at the first page with nothing new, so a quiet watch costs a request or two. **Max items per source** then sets how many of the newest items are checked per source (50 is plenty for a daily schedule). Save the input as a task, add a Schedule in Apify (hourly or daily), and give each watch its own **Monitoring key** if several schedules use the same input. Every output item carries `changeType: "new"`; like and follower counts are not tracked, so `previousValues` does not appear.

### Pricing

Pay per result: one post or one profile is one result. There is no start fee and no subscription; Apify's free plan is enough to try it. Platform usage is billed separately and is small (about $0.005 per 1,000 items, mostly dataset storage).

### Integrations and API

Use the run in Zapier, Make, n8n or Google Sheets, or call it from Python/Node with the Apify client. See the **API** tab for ready-made snippets. Proxy: Apify datacenter proxy (the default) is enough; it spreads requests over IPs so large runs do not hit Bluesky's per-IP rate limit.

### Is it legal to scrape Bluesky?

This Actor collects only data that Bluesky publishes openly through its public API to anyone without an account: posts, public profile data (handles, display names, bios, avatars, counts) and public social-graph records such as follows and likes. It does not log in, read direct messages, or access content restricted to logged-in users. Public data can still include personal information, so you are responsible for how you use it and for complying with Bluesky's terms, the authors' copyright, and privacy laws such as the GDPR.

### Support

Found a bug or need a field added? Open an issue in the **Issues** tab; it is usually answered within a day.

# Actor input Schema

## `searchTerms` (type: `array`):

Keywords, phrases or hashtags to search posts for, one per line: `climate change`, `#python`, `"exact phrase"`. Bluesky search syntax works too (`from:handle`, `lang:en`). Each term is one source.

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

Latest = newest first, pages back as far as Max items allows. Top = Bluesky's most relevant posts; without login Bluesky returns at most about 100 top posts per term.

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

Only posts newer than this: a date (YYYY-MM-DD), a date-time, or a period such as `7 days` or `24 hours`. Applies to search terms.

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

Only posts older than this date (YYYY-MM-DD) or period (`30 days`). Applies to search terms.

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

Only posts in this language, as a 2-letter code: `en`, `de`, `ja`, `pt`. Applies to search terms.

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

Only posts written by this account (handle such as `bsky.app` or a DID). Applies to search terms.

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

Only posts that mention this account (handle or DID). Applies to search terms.

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

Only posts that link to this website, e.g. `nytimes.com`. Applies to search terms.

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

Bluesky accounts, one per line: a handle (`bsky.app`, `@jay.bsky.team`), a DID or a profile URL (`https://bsky.app/profile/bsky.app`). What is collected for each is set by "What to get from accounts".

## `profileContent` (type: `string`):

Posts (no replies), posts and replies, media posts, video posts, the account's likes, its followers, the accounts it follows, or only the profile data (followers/following/posts counts, bio, avatar, created date).

## `includeProfileInfo` (type: `boolean`):

When collecting posts or likes from accounts, also output one `type: "profile"` item per account (counts, bio, avatar, created date). It does not count toward Max items per source.

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

Include posts the account (or feed/list) reposted, marked `isRepost: true` with `repostedByHandle`. Off = only original posts.

## `profileDetails` (type: `boolean`):

Add followers/following/posts counts and pinned post to every follower, followed account and list member (1 extra request per 25 profiles). Turn off for faster, slimmer lists.

## `startUrls` (type: `array`):

Any bsky.app link: a post (scrapes it and its reply thread), a custom feed (`/profile/<handle>/feed/<name>`), a list (`/profile/<handle>/lists/<id>`), a starter pack, a profile (or its `/followers`, `/follows`, `/likes` page), a search (`/search?q=...`) or a hashtag page.

## `listContent` (type: `string`):

Posts = the list's feed (recent posts by its members). Members = the accounts on the list, as profile items.

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

For post URLs: how many levels of replies to collect below the post (1 = direct replies only).

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

Overall cap on dataset items across all sources. You pay per item, so this caps the cost.

## `maxItemsPerSource` (type: `integer`):

Cap per search term, account, feed, list or thread. Empty = each source can fill Max items. With monitoring on, this is how many of the newest items are checked per source.

## `onlyNewOrChanged` (type: `boolean`):

Remember what earlier runs of this input returned and output only new posts (and new followers, likes or list members). Use with a Schedule; items already seen are not charged. Newest-first sources stop at the first page with nothing new.

## `monitoringKey` (type: `string`):

Name for this watch, so several schedules on the same input stay separate; defaults to a key derived from the input

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

Apify datacenter proxy (default) spreads requests over IPs, which helps against Bluesky's per-IP rate limits on large runs. The API is public and needs no residential proxy.

## Actor input object example

```json
{
  "searchTerms": [
    "#python"
  ],
  "searchSort": "latest",
  "profileContent": "posts",
  "includeProfileInfo": true,
  "includeReposts": false,
  "profileDetails": true,
  "startUrls": [],
  "listContent": "posts",
  "threadDepth": 10,
  "maxItems": 100,
  "onlyNewOrChanged": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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 = {
    "searchTerms": [
        "#python"
    ],
    "startUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("tortuga/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 = {
    "searchTerms": ["#python"],
    "startUrls": [],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("tortuga/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 '{
  "searchTerms": [
    "#python"
  ],
  "startUrls": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call tortuga/bluesky-scraper --silent --output-dataset

```

## MCP server setup

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