# Bluesky Scraper - Posts, Threads, Likes & Profiles (`sweet_stickball/bluesky-scraper`) Actor

Bluesky scraper: extract public posts from author feeds and threads, with text, date, likes, reposts, replies, quotes, hashtags, mentions, links and images. Optional author profiles. Public AT Protocol API, no login, flat JSON.

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

## Pricing

from $0.46 / 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.
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, Threads, Likes & Profiles

Extract public Bluesky posts as flat JSON rows: from the feeds of the accounts you choose, from threads (a post with its replies), or from a one-page search. Each row has the post text, date, like, repost, reply and quote counts, hashtags, mentions, links, images and reply references. Author profiles are optional.

### What it does

- **Author feeds**: give handles (`bsky.app`, `@jay.bsky.team`), profile URLs or DIDs. The Actor follows the feed's pagination cursor until `maxItems` posts per account or the end of the feed. Replies by the author are left out unless `includeReplies` is on; reposts are kept and flagged (`isRepost`, `repostedBy`) unless `includeReposts` is off.
- **Threads**: give post URLs (`https://bsky.app/profile/<handle>/post/<id>`). The Actor returns the post, the posts above it in the thread (root first) and its replies, up to `maxItems` rows per thread.
- **Search**: give a `searchQuery`. Bluesky serves only the first page of search results without login, so the search returns at most 25 posts (see Limitations).
- **Profiles** (optional): with `includeProfiles`, one extra row per handle with followers, follows, post count, bio, avatar and creation date.
- A post is returned once per run, even if several inputs lead to it. An invalid handle or URL is skipped and named in the run's status message; the run fails with an explicit message only when no post at all could be returned.
- Uses the public Bluesky AppView API of the AT Protocol (`public.api.bsky.app`, `api.bsky.app` for search). No login, no API key, no browser, no proxy. Requests are sent one at a time; on HTTP 429 the Actor waits for the time the API asks for.

### Input

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `handles` | array of strings | One of the three sources | — | Handles, profile URLs or DIDs. A name without a dot gets `.bsky.social` appended |
| `postUrls` | array of strings | One of the three sources | — | Post URLs or `at://` URIs; each one gives its thread |
| `searchQuery` | string | One of the three sources | — | Full-text search, first page only (25 posts at most) |
| `includeReplies` | boolean | No | `false` | Author feeds: also return the author's replies to other posts |
| `includeReposts` | boolean | No | `true` | Author feeds: also return posts the author reposted |
| `includeProfiles` | boolean | No | `false` | Also return one profile row per handle |
| `maxItems` | integer | No | 50 | Maximum posts per handle, per thread and for the search |

Example:

```json
{
    "handles": ["bsky.app", "https://bsky.app/profile/pfrazee.com"],
    "postUrls": ["https://bsky.app/profile/bsky.app/post/3mz4cvpstb22v"],
    "includeProfiles": true,
    "maxItems": 100
}
```

### Output

One dataset row per post (`"type": "post"`). Real example (feed of `bsky.app`, run of 2 November 2026; this row is a repost by `bsky.app` of a post by `avclub.com`):

```json
{
    "type": "post",
    "uri": "at://did:plc:kwxpje5qog6wwndagwbssatp/app.bsky.feed.post/3mviutosqmk2t",
    "url": "https://bsky.app/profile/avclub.com/post/3mviutosqmk2t",
    "cid": "bafyreigyzxfb3tynf6hus4cshhzcrwr7sht4vfp2l5phbbfephvn7bcd7a",
    "authorHandle": "avclub.com",
    "authorDisplayName": "The A.V. Club",
    "authorDid": "did:plc:kwxpje5qog6wwndagwbssatp",
    "text": "Less than 4 hours until the #Emmys kick off! You can keep up with all the commentary, winners, joy—and, let's face it, disappointment—via our Emmys Watch feed right here on @bsky.app. Pin it now so you don't miss a thing!\n\nbsky.app/profile/avcl...",
    "createdAt": "2026-09-14T19:47:12.655Z",
    "indexedAt": "2026-09-14T19:47:15.764Z",
    "likeCount": 546,
    "repostCount": 42,
    "replyCount": 31,
    "quoteCount": 4,
    "langs": ["en"],
    "replyParent": null,
    "replyRoot": null,
    "hashtags": ["Emmys"],
    "mentions": ["bsky.app"],
    "links": ["https://bsky.app/profile/avclub.com/feed/emmys-watch-r"],
    "embedType": "images",
    "images": [
        {
            "url": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:kwxpje5qog6wwndagwbssatp/bafkreicwxzp6lnqvuetiytyeanq3wynihsneq4qqdvv7bzhkrmw4nv36oy",
            "alt": null
        }
    ],
    "videoUrl": null,
    "externalUrl": null,
    "externalTitle": null,
    "quotedPostUri": null,
    "isRepost": true,
    "repostedBy": "bsky.app",
    "source": "author:bsky.app",
    "scrapedAt": "2026-11-02T22:24:50+00:00"
}
```

| Field | Notes |
|-------|-------|
| `uri`, `cid` | AT Protocol identifiers of the post; `url` is the same post on bsky.app |
| `createdAt` | Time set by the author's app; `indexedAt` is when Bluesky indexed the post |
| `hashtags`, `mentions`, `links` | From the post's rich-text facets; `links` also contains the link card URL. Shortened links in `text` (`bsky.app/profile/avcl...`) are shown in full in `links` |
| `embedType` | `images`, `video`, `external` (link card), `record` (quote), `recordWithMedia` (quote with media), `gallery`, or `null` |
| `images` | Full-size image URLs and alt texts (`null` when the author wrote none) |
| `videoUrl` | HLS playlist (`.m3u8`) of an attached video |
| `quotedPostUri` | `at://` URI of the quoted post |
| `replyParent`, `replyRoot` | `at://` URIs of the post replied to and of the first post of the thread; `null` for top-level posts |
| `isRepost`, `repostedBy` | Set when the row comes from a repost in an author feed; the author fields are those of the original post |
| `source` | Input that produced the row: `author:<handle>`, `thread:<url>` or `search:<query>` |

Profile rows (`"type": "profile"`, only with `includeProfiles`) contain `did`, `handle`, `url`, `displayName`, `description`, `followersCount`, `followsCount`, `postsCount`, `avatar`, `banner`, `createdAt`, `indexedAt` and `scrapedAt`.

The dataset has two views: `posts` and `profiles`.

### Pricing

Pay-per-event, no start fee: **$0.77 per 1,000 posts** (event `POST`) on the Apify Free plan, lower on paid plans. You only pay for results pushed to the dataset.

| Apify plan | Price per 1,000 posts |
|---|---|
| Free | $0.77 |
| Starter | $0.65 |
| Scale | $0.58 |
| Business | $0.46 |

- Additional event `PROFILE` (profile): $0.77 per 1,000 on the Free plan, $0.46 on Business. Charged only when that data is returned.

Set a maximum cost per run in the run options: the Actor stops cleanly when it is reached.

### Limitations

- **Search returns at most 25 posts per run.** Without login, Bluesky's search API serves the first page of results and refuses the next pages (HTTP 403). The Actor does not log in and does not split the search into date ranges to get around this. For more posts, scrape the authors' feeds or the threads.
- Only public posts that the Bluesky AppView serves without login. Deleted posts and posts the AppView does not serve (blocked or removed by moderation) are not returned.
- A thread is read in one request. For very large threads the AppView may not return every reply, and `maxItems` caps the rows per thread.
- Counts (likes, reposts, replies, quotes, followers) are those of the moment of the run.
- `langs` is what the author's app declared, and can be empty.
- Likes, followers lists and direct messages are not included.

### Related Actors

- [Reddit Scraper API - Subreddit Posts & Comments, No Login](https://apify.com/sweet_stickball/reddit-scraper) — Scrape Reddit posts and comments from any subreddit: title, text, date, link and author. Reddit data API alternative for social listening, no login.
- [Telegram Channel Scraper API - Messages & Views, No Login](https://apify.com/sweet_stickball/telegram-public-channel-scraper) — Scrape public Telegram channel messages: text, dates, views, media and links. Telegram data API with no account, phone number or any API key needed.
- [Substack Scraper - Newsletter Posts, Authors and Engagement](https://apify.com/sweet_stickball/substack-scraper) — Substack scraper and API alternative: newsletter posts from any Substack publication or custom domain, or find publications by keyword, with title, authors, date, free or paid audience, word count, likes, comments, restacks, tags and optional full text of free posts.

# Actor input Schema

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

Bluesky accounts whose posts to scrape, one per line: a handle (bsky.app, @jay.bsky.team), a profile URL (https://bsky.app/profile/bsky.app) or a DID (did:plc:...). A name without a dot gets .bsky.social appended.

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

Bluesky post URLs (https://bsky.app/profile/<handle>/post/<id>) or at:// URIs. Returns the post, the posts it replies to, and its replies.

## `searchQuery` (type: `string`):

Full-text post search. Without login, Bluesky returns only the first page of results: at most 25 posts per run.

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

For author handles: also return the author's replies to other posts. Thread scraping (post URLs) always returns replies.

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

For author handles: also return posts the author reposted (flagged with isRepost = true and repostedBy).

## `includeProfiles` (type: `boolean`):

Also return one profile row per handle (followers, follows, post count, bio). Billed as a separate PROFILE event.

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

Maximum posts per handle, per thread and for the search (the search stops at 25).

## Actor input object example

```json
{
  "handles": [
    "bsky.app"
  ],
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3mz4cvpstb22v"
  ],
  "searchQuery": "web scraping",
  "includeReplies": false,
  "includeReposts": true,
  "includeProfiles": false,
  "maxItems": 10
}
```

# Actor output Schema

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

Dataset items shown with the 'posts' view.

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

Profile rows (when 'Include author profiles' is on), shown with the 'profiles' view.

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

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

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

```

## MCP server setup

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