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

Scrapes Bluesky posts from profiles, single posts and threads, custom feeds, lists and keyword searches, with likes, reposts, replies, quotes, images, link cards and hashtags. Optionally adds profile rows and follower or following lists. No login needed.

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

## Pricing

$0.50 / 1,000 rows

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

### What does Bluesky Scraper do?

**Bluesky Scraper** collects **posts from Bluesky** as clean, structured data:
the full text, **date, likes, reposts, replies, quotes and bookmarks**, the
author, **images with alt text, videos, link cards, quoted posts**, hashtags,
mentions and links. Paste **profiles, single posts, custom feeds or lists**, or
type **search words**, and get one row per post. Turn on extra rows for
**profiles** (follower, following and post counts), **followers and followed
accounts**, and **every reply in a thread**.

It reads [Bluesky](https://bsky.app)'s own public API, so it needs **no login,
no account and no cookies**, and it is fast: the prefilled example returns 40
posts in about 4 seconds. Running on the Apify platform also gives you API
access, scheduled runs, integrations (Google Sheets, Make, Zapier, webhooks),
run monitoring, and automatic retries through Apify Proxy if Bluesky is ever
slow to answer.

### Why scrape Bluesky?

- **Social listening and brand monitoring**: track every new post that
  mentions your brand, product, hashtag or competitor.
- **Research and journalism**: collect what politicians, journalists, scientists
  or any public account posts, with engagement counts, for analysis.
- **Influencer and audience research**: compare accounts by followers, posting
  rate and the likes and reposts their posts earn; export follower lists.
- **Trend tracking**: read custom feeds and curated lists to see what a
  community is talking about right now.
- **AI and NLP datasets**: build labelled text datasets with language tags,
  alt text and conversation structure (reply parent and root).

### How to scrape Bluesky

1. Open a profile, post, feed or list on [bsky.app](https://bsky.app) and copy
   the address bar, for example `https://bsky.app/profile/bsky.app`. A handle
   such as `@bsky.app` works too.
2. Paste it into **Bluesky links or handles**, one per line. To search instead,
   type words into **Search posts**.
3. Set **Max posts per input**, and optionally a language or a date range.
4. Click **Start**. Download the results as JSON, CSV or Excel when the run
   finishes.

### Input

Everything is set on the Input tab. In JSON, an input looks like this:

```json
{
    "startUrls": [
        "https://bsky.app/profile/bsky.app",
        "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l",
        "https://bsky.app/profile/bsky.app/feed/whats-hot",
        "https://bsky.app/profile/bsky.app/lists/3lvpca43j5z26"
    ],
    "searchQueries": ["open source", "#photography", "climate from:nytimes.com"],
    "maxPostsPerInput": 100,
    "includeThreadReplies": true,
    "searchSort": "latest",
    "language": "en",
    "since": "30 days",
    "includeProfiles": true
}
```

| Field | What it does |
|---|---|
| `startUrls` | Profiles (`bsky.app/profile/<handle>`, `@handle`, a bare handle, or a `did:plc:…` id), post links (`…/post/<id>`), custom feeds (`…/feed/<name>`), lists (`…/lists/<id>`), and bsky.app search or hashtag links. Plain strings or `{"url": "…"}` objects. |
| `searchQueries` | Words to search all of Bluesky for. Operators work: `from:handle`, `mentions:handle`, `#hashtag`, `"exact phrase"`, `domain:example.com`, `-word`, `lang:ja`, `since:2026-09-01`, `until:2026-10-01`. |
| `maxPostsPerInput` | Most posts saved per link or search. Default 100. For a post link, the most replies saved under the post. `0` saves only profile, follower and following rows. |
| `includeReplies` | For profiles and lists, also save replies to other people's posts. Default off: own posts and own threads only. |
| `includeReposts` | For profiles, also save posts the account reposted, with `repostedBy` and `repostedAt`. Default off. |
| `includeThreadReplies` | For post links, also save the replies in the thread: the post plus up to `maxPostsPerInput` replies, so up to 1 + N billed rows per link. Default off. |
| `searchSort` | `latest` (default) or `top`. |
| `language` | Only posts tagged with this language, such as `en`, `ja`, `pt-BR`. |
| `since` / `until` | Date range: a date (`2026-09-01`, UTC), a date and time, or an age (`7 days`, `12 hours`). Applies to profiles, feeds, lists and searches. |
| `includeProfiles` | One row per profile link with its counts, bio, avatar and join date. |
| `includeFollowers` / `includeFollowing` | One row per follower / followed account of each profile link, newest first. |
| `maxFollowsPerProfile` | Most followers, and most followed accounts, per profile. Default 100. |
| `proxyConfiguration` | Leave empty. Bluesky's API usually answers the Actor directly; when a request is refused or stalls, the Actor retries through Apify datacenter and then residential proxies by itself. |

### Output

Every post, profile and account becomes one row, marked by `type`. Here is a
real post row:

```json
{
    "type": "post",
    "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mxu3hsnebc2v",
    "url": "https://bsky.app/profile/bsky.app/post/3mxu3hsnebc2v",
    "cid": "bafyreihprzpev5lpm4wkevfteaurguvmtmzq4cmg6guzwxvmlumvfdcsmy",
    "text": "saw some cool clouds today",
    "createdAt": "2026-10-14T17:35:30.592Z",
    "indexedAt": "2026-10-14T17:35:33.661Z",
    "authorHandle": "bsky.app",
    "authorDid": "did:plc:z72i7hdynmk6r22z27h6tvur",
    "authorDisplayName": "Bluesky",
    "authorAvatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkrei…",
    "replyCount": 234,
    "repostCount": 410,
    "likeCount": 7650,
    "quoteCount": 44,
    "bookmarkCount": 156,
    "languages": ["en"],
    "hashtags": [],
    "mentions": [],
    "links": [],
    "images": [
        {
            "url": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkrei…",
            "thumbnailUrl": "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkrei…",
            "alt": "Photo taken from a hill showing a cloud inversion in the valley below, underneath a bright blue sky with a few streaks of clouds",
            "width": 1200,
            "height": 1600
        }
    ],
    "video": null,
    "externalLink": null,
    "quotedPost": null,
    "embeddedRecord": null,
    "replyParentUri": null,
    "replyRootUri": null,
    "labels": [],
    "repostedBy": null,
    "repostedAt": null,
    "sourceInput": "https://bsky.app/profile/bsky.app",
    "scrapedAt": "2026-10-20T15:13:22.838510+00:00"
}
```

And a profile row (with **Save profile rows** on):

```json
{
    "type": "profile",
    "did": "did:plc:z72i7hdynmk6r22z27h6tvur",
    "handle": "bsky.app",
    "displayName": "Bluesky",
    "description": "official Bluesky account (check username👆)\n\nBugs, feature requests, feedback: support@bsky.app",
    "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkrei…",
    "createdAt": "2023-04-12T04:53:57.057Z",
    "isVerified": false,
    "labels": [],
    "url": "https://bsky.app/profile/bsky.app",
    "banner": "https://cdn.bsky.app/img/banner/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkrei…",
    "followersCount": 35319612,
    "followsCount": 22,
    "postsCount": 883,
    "listsCount": 18,
    "feedsCount": 7,
    "starterPacksCount": 15,
    "pinnedPostUri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l",
    "sourceInput": "https://bsky.app/profile/bsky.app",
    "scrapedAt": "2026-10-20T15:13:21.690549+00:00"
}
```

Follower and following rows (`type` `follower` or `following`) have the same
account fields as a profile row, without the counts, plus `ofHandle` and
`ofDid`: the profile they follow or are followed by.

The Output tab has two views, **Posts** and **Profiles, followers and
following**. You can download the dataset in various formats such as JSON,
HTML, CSV, or Excel.

### Data fields

Post rows (`type` is `post`):

| Field | Description |
|---|---|
| `uri`, `cid` | The post's AT Protocol id and content hash. |
| `url` | Link to the post on bsky.app. |
| `text` | The full post text. |
| `createdAt` | When it was posted, as the author's app stated it (UTC, ISO 8601). |
| `indexedAt` | When Bluesky first saw it (UTC, ISO 8601). |
| `authorHandle`, `authorDid`, `authorDisplayName`, `authorAvatar` | Who posted it. |
| `replyCount`, `repostCount`, `likeCount`, `quoteCount`, `bookmarkCount` | Engagement counts, as numbers. |
| `languages` | Language tags the author's app set, such as `["en"]`. |
| `hashtags` | Hashtags, without the `#`. |
| `mentions` | Mentioned accounts as `{did, handle}`. |
| `links` | Web addresses linked in the text, then the link card's address. |
| `images` | Each image's full-size `url`, `thumbnailUrl`, `alt` text, `width` and `height`. |
| `video` | `playlistUrl` (HLS), `thumbnailUrl`, `alt`, `width`, `height`, or `null`. |
| `externalLink` | The link card: `url`, `title`, `description`, `thumbnailUrl`, or `null`. |
| `quotedPost` | The quoted post's `uri`, `url`, `text`, `createdAt`, author and counts; `status` is `available`, `notFound`, `blocked` or `detached`. |
| `embeddedRecord` | A shared feed, list or starter pack: `kind`, `uri`, `name`, `url`, `creatorHandle`. |
| `replyParentUri`, `replyRootUri` | For replies, the post replied to and the thread's first post. |
| `labels` | Moderation labels on the post (for example adult-content labels); usually empty. |
| `repostedBy`, `repostedAt` | For reposts saved from a profile, who reposted it and when. |
| `sourceInput` | The link or search the row came from. |
| `scrapedAt` | When the row was saved. |

Profile rows (`type` is `profile`): `did`, `handle`, `displayName`,
`description`, `avatar`, `banner`, `createdAt` (join date), `isVerified`,
`labels`, `url`, `followersCount`, `followsCount`, `postsCount`, `listsCount`,
`feedsCount`, `starterPacksCount`, `pinnedPostUri`. Follower and following rows
add `ofHandle` and `ofDid`. An account whose handle no longer resolves has
`handle` set to `null`, and its `url` uses its DID.

### How much does it cost to scrape Bluesky?

You pay per row saved; see the **Pricing** tab for the exact price. Rows are
cheap to make: the Actor uses Bluesky's JSON API with plain HTTP requests (no
browser), 512 MB of memory, and usually no proxy, so a run of 1,000 posts
typically finishes in seconds. Nothing is charged for links that give no data.

### Tips and advanced options

- **Searching an account's posts**: `cats from:bsky.app` searches one account.
  For *all* of an account's posts, paste its profile link instead: it pages
  back much further.
- **Search depth**: `latest` searches page back through time as far as your
  limit. `top` returns Bluesky's best-ranked posts, at most 100 per search.
  A search made only of operators (such as `from:bsky.app` or
  `mentions:bsky.app`, with no plain word) also returns at most about 100 posts,
  because Bluesky gives logged-out users only one page of those.
- **Dates**: `since` stops profiles, lists and searches as soon as posts get
  older than the date, so a small date range keeps runs short.
- **Lists**: Bluesky only lets logged-in users read a list's feed, so the
  Actor rebuilds it: it reads every member's own posts (up to 500 members)
  and merges them newest first. Reposts are left out, and replies only come
  in with **Include profiles' replies** on, so the result can differ a little
  from the list feed in the Bluesky app. Large lists take a little longer.
- **Custom feeds** are ranked by their creator, not by date, so `since` and
  `until` filter them instead of stopping them, and very deep pages of some
  feeds are refused by Bluesky.
- **Threads**: a post link with **Include replies under post links** saves the
  post first, then up to **Max posts per input** replies depth first (so up to
  1 + N rows, each billed), each with `replyParentUri` so you can rebuild the
  conversation.

### FAQ, disclaimers, and support

**Do I need a Bluesky account?** No. The Actor reads only what Bluesky shows to
logged-out visitors through its public API. Posts that their author restricted
to logged-in users are not visible to it.

**Why did an input give nothing?** Unknown handles, deleted posts, missing
feeds, moderation lists and searches with no results are skipped, not billed,
and listed with the reason in the **FAILED_INPUTS** record of the run's
key-value store. The run's status message sums them up.

**Is it legal to scrape Bluesky?** This Actor collects only publicly
available data. Posts and profiles can contain personal data, which is
protected by laws such as the GDPR in the EU. Follower and following rows in
particular hold other people's handles, names, bios and avatars, not just the
profile you asked for. Only collect personal data when
you have a legitimate reason to, and check with a lawyer if you are unsure.
Please also respect Bluesky's terms of service.

**Something does not work, or you need a feature?** Open an issue on the
**Issues** tab and it will be looked at quickly. If you need a custom
solution, get in touch through the same tab.

# Actor input Schema

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

One per line. A profile (https://bsky.app/profile/bsky.app, @bsky.app, a bare handle such as jay.bsky.team, or a did:plc:… id) gives that account's newest posts. A post link (…/profile/<handle>/post/<id>) gives that post, plus its replies if you turn them on below. A custom feed (…/profile/<handle>/feed/<name>) or a list (…/profile/<handle>/lists/<id>) gives its newest posts. A bsky.app search or hashtag link works too.

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

Words to search all of Bluesky for, one search per line. Bluesky's search operators work: from:handle (posts by an account), mentions:handle, #hashtag, "exact phrase", domain:example.com. Fill this, the links above, or both.

## `maxPostsPerInput` (type: `integer`):

Stops after this many posts for each link or search, newest first (top searches in Bluesky's ranking). For a post link it is the most replies saved under the post. Set 0 to save only profile, follower and following rows.

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

For profiles and lists, also save the account's replies to other people's posts. Off: only its own posts and its own threads.

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

For profiles, also save the posts the account reposted, marked with repostedBy and repostedAt.

## `includeThreadReplies` (type: `boolean`):

For post links, also save every reply in the thread under the post (up to the max posts above), each with its parent and root post.

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

Latest pages back through time as far as the limit allows. Top returns Bluesky's best-ranked posts, at most 100 per search.

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

Only posts tagged with this language, as a code such as en, es, ja or pt-BR. Applies to every input; leave empty for all languages.

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

Skips posts older than this date (for example 2026-09-01, in UTC) or age (for example 7 days). Applies to profiles, feeds, lists and searches; post links are always saved.

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

Skips posts from this date (UTC) or age onwards, for example 2026-10-01. Together with the field above it sets a date range.

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

For each profile link, adds one row with the account's follower, following and post counts, bio, avatar, banner and join date.

## `includeFollowers` (type: `boolean`):

For each profile link, adds one row per follower (newest first), up to the limit below.

## `includeFollowing` (type: `boolean`):

For each profile link, adds one row per account it follows (newest first), up to the limit below.

## `maxFollowsPerProfile` (type: `integer`):

Stops each follower list and each following list after this many accounts.

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

Leave this empty: Bluesky's public API answers the Actor directly, and if it ever refuses, the Actor retries through Apify's datacenter and then residential proxies on its own.

## Actor input object example

```json
{
  "startUrls": [
    "https://bsky.app/profile/bsky.app"
  ],
  "searchQueries": [
    "open source"
  ],
  "maxPostsPerInput": 20,
  "includeReplies": false,
  "includeReposts": false,
  "includeThreadReplies": false,
  "searchSort": "latest",
  "includeProfiles": false,
  "includeFollowers": false,
  "includeFollowing": false,
  "maxFollowsPerProfile": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (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 = {
    "startUrls": [
        "https://bsky.app/profile/bsky.app"
    ],
    "searchQueries": [
        "open source"
    ],
    "maxPostsPerInput": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("mina_safwat/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 = {
    "startUrls": ["https://bsky.app/profile/bsky.app"],
    "searchQueries": ["open source"],
    "maxPostsPerInput": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("mina_safwat/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 '{
  "startUrls": [
    "https://bsky.app/profile/bsky.app"
  ],
  "searchQueries": [
    "open source"
  ],
  "maxPostsPerInput": 20
}' |
apify call mina_safwat/bluesky-scraper --silent --output-dataset

```

## MCP server setup

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