# Bluesky Posts Scraper 🦋 Any Account, No Login (`cleanfeed/bluesky-posts-extractor`) Actor

Scrape posts from any Bluesky account. Pass handles or profile URLs; get post text, like, repost and reply counts, author handle, timestamp and a direct post URL as JSON or CSV. No login and no app password. Since date filter makes scheduled monitoring cheap.

- **URL**: https://apify.com/cleanfeed/bluesky-posts-extractor.md
- **Developed by:** [Yaniv van der Stigchel](https://apify.com/cleanfeed) (community)
- **Categories:** Social media, AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.43 / 1,000 post returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bluesky Posts Scraper — no login, no app password

Export posts from any Bluesky account. Text, likes, reposts, replies, quotes and
timestamps. The AT Protocol network is public, so no authentication is required.

### Scrape Bluesky posts in bulk

- Export Bluesky posts to JSON or CSV
- Download a Bluesky account's post history
- Track Bluesky engagement metrics over time
- Monitor Bluesky accounts for new posts
- Build a dataset from Bluesky

Pass several accounts at once. Accepts handles, DIDs, or `bsky.app` profile URLs.

### Incremental mode

Set `since` to the date of your last run and scraping stops as soon as it
reaches older posts. Scheduled monitoring costs a fraction of a full history
pull.

### What you get per post

Post text, author handle, DID and display name, like/repost/reply/quote counts,
creation and index timestamps, detected languages, the AT-URI, and a direct
`bsky.app` link.

### Honest failure reporting

Accounts that do not exist return a row with `reason: "account-not-found"` and
**cost you nothing**. You are only charged for posts actually delivered.

### Input

| Field | Required | Description |
|---|---|---|
| `actors` | yes | Handles, DIDs, or profile URLs |
| `maxPostsPerActor` | no | Newest first. Default 200. Your cost ceiling. |
| `since` | no | ISO date. Only posts after it. |

### Example input

```json
{
  "actors": ["bsky.app", "@jay.bsky.team"],
  "maxPostsPerActor": 500,
  "since": "2026-08-01"
}
```

### Output

Every row has the same fields whether it succeeded or failed, so you can
select columns without branching. Failed rows are never charged.

| Field | Type | Description |
|---|---|---|
| `success` | boolean | True when this row carries data. Failed rows are never charged. |
| `sourceAccount` | string | The account input this row came from. |
| `postUrl` | string | Public bsky.app link to the post. |
| `postUri` | string | The post's at:// record URI. |
| `authorHandle` | string | Handle of the account that posted. |
| `authorDisplayName` | string | Profile display name, if set. |
| `authorDid` | string | Decentralised identifier, stable across handle changes. |
| `text` | string | The post body. |
| `createdAt` | string | ISO 8601 timestamp set by the author's client. |
| `indexedAt` | string | ISO 8601 timestamp when the network indexed the post. |
| `languages` | array | BCP-47 language codes declared on the post. |
| `likeCount` | integer | Likes at the time of the run. |
| `repostCount` | integer | Reposts at the time of the run. |
| `replyCount` | integer | Replies at the time of the run. |
| `quoteCount` | integer | Quote posts at the time of the run. |
| `errorCode` | string | Machine-readable failure reason. Null on success. |
| `errorMessage` | string | Human-readable explanation of the failure. Null on success. |

#### Example — success

```json
{
  "success": true,
  "sourceAccount": "bsky.app",
  "postUrl": "https://bsky.app/profile/bsky.app/post/3k2aqvbhyq2b",
  "postUri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3k2aqvbhyq2b",
  "authorHandle": "bsky.app",
  "authorDisplayName": "Bluesky",
  "authorDid": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "text": "We just shipped a new way to browse feeds. Let us know what you think.",
  "createdAt": "2026-08-14T09:12:04.221Z",
  "indexedAt": "2026-08-14T09:12:05.884Z",
  "languages": [
    "en"
  ],
  "likeCount": 6042,
  "repostCount": 1180,
  "replyCount": 214,
  "quoteCount": 73,
  "errorCode": null,
  "errorMessage": null
}
```

#### Example — failure

A failure carries the same fields, so nothing downstream has to branch.

```json
{
  "success": false,
  "sourceAccount": "not-a-real-handle.bsky.social",
  "postUrl": null,
  "postUri": null,
  "authorHandle": null,
  "authorDisplayName": null,
  "authorDid": null,
  "text": null,
  "createdAt": null,
  "indexedAt": null,
  "languages": null,
  "likeCount": null,
  "repostCount": null,
  "replyCount": null,
  "quoteCount": null,
  "errorCode": "account-not-found",
  "errorMessage": "No Bluesky account exists for this handle or DID, or the request was refused."
}
```

#### Error codes

- `account-not-found`
- `unparseable-actor`
- `no-posts-found`
- `error`

### Use it for

- **Bluesky account posts** — a profile's full public timeline
- **Bluesky profile monitoring** — scheduled runs with a since filter
- **AT Protocol (atproto) post data** — public records from any handle
- Social listening and brand tracking
- Research corpora and training datasets

### Use it from an AI agent (MCP)

This Actor is callable as a tool through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so Claude, ChatGPT, Cursor and VS Code can run it directly.

Add the server to your MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com",
      "headers": {
        "Authorization": "Bearer <YOUR_APIFY_TOKEN>"
      }
    }
  }
}
```

Then ask for what you want in plain language — for example *“get the recent posts from this Bluesky account”* — and the agent calls `cleanfeed/bluesky-posts-extractor` with the right input. Every output field is described in the dataset schema, so the agent knows what it is getting back before it runs anything.

### Call it from code

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("cleanfeed/bluesky-posts-extractor").call(run_input={
    "actors": ["bsky.app"],
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["success"]:
        print(item)
```

#### JavaScript

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });

const run = await client.actor('cleanfeed/bluesky-posts-extractor').call({
    actors: ["bsky.app"],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.filter((i) => i.success));
```

#### cURL

```bash
curl -X POST "https://api.apify.com/v2/acts/cleanfeed~bluesky-posts-extractor/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"actors": ["bsky.app"]}'
```

### Related actors

| If you need | Use |
|---|---|
| Posts from public Threads profiles | [Threads Posts Scraper](https://apify.com/cleanfeed/meta-threads-posts-scraper) |
| Messages from public Telegram channels | [Telegram Channel Scraper](https://apify.com/cleanfeed/telegram-channel-archive-scraper) |

### Limitations

- **Public accounts only.** Bluesky's public API is used, so no login and no app password are needed — and nothing non-public is reachable.
- **The feed is the author's timeline**, which includes their reposts. Filter on `authorHandle` if you want originals only.
- Engagement counts are a snapshot at run time and will drift.

### FAQ

#### Do I need a Bluesky account or app password?

No. Bluesky's public API is used, so there is no login and nothing to get rate-limited against your account.

#### What handle formats work?

A handle such as `alice.bsky.social`, a full `did:plc:...`, or a `bsky.app/profile/...` URL.

#### Can I fetch only new posts?

Yes — set `since` to an ISO date and each run returns only posts after it, which keeps scheduled monitoring cheap.

#### Does this include reposts?

The author feed includes their reposts. Filter on `authorHandle` if you only want their own posts.

#### Can I read private accounts?

No. Only public posts are reachable, by design.

### Notes

Only publicly available posts are collected, via the official public AT Protocol
AppView. No login, no app password, no private data.

# Actor input Schema

## `actors` (type: `array`):

Bluesky accounts to fetch posts from: handles such as alice.bsky.social, full DIDs, or bsky.app profile URLs. Public accounts only.

## `maxPostsPerActor` (type: `integer`):

Newest first. You are charged per post returned, so this is your cost ceiling.

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

ISO date, e.g. 2026-08-01. Scraping stops as soon as older posts appear, so scheduled runs stay cheap.

## Actor input object example

```json
{
  "actors": [
    "bsky.app"
  ],
  "maxPostsPerActor": 200
}
```

# Actor output Schema

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

One row per post: text, author, engagement counts, timestamps, and a direct bsky.app link.

## `summary` (type: `string`):

Accounts processed and posts delivered.

# 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 = {
    "actors": [
        "bsky.app"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanfeed/bluesky-posts-extractor").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 = { "actors": ["bsky.app"] }

# Run the Actor and wait for it to finish
run = client.actor("cleanfeed/bluesky-posts-extractor").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 '{
  "actors": [
    "bsky.app"
  ]
}' |
apify call cleanfeed/bluesky-posts-extractor --silent --output-dataset

```

## MCP server setup

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

```

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/lMWlRUU2oOvfcJbmG/builds/ZZAIHHYdNLxdKY8Hq/openapi.json
