# Bluesky Scraper: Posts, Search, Profiles, Followers (`charlesmarkette/bluesky-scraper-posts-search-profiles-followers`) Actor

Scrape Bluesky with its official public API, no login: search posts by keyword, hashtag, author or date, get any profile's posts, profiles, followers and follows, replies to a post, likers and reposters. Clean rows with links, hashtags, images and counts.

- **URL**: https://apify.com/charlesmarkette/bluesky-scraper-posts-search-profiles-followers.md
- **Developed by:** [Charles Markette](https://apify.com/charlesmarkette) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.10 / 1,000 post or profiles

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** collects public Bluesky data through Bluesky's **official public API**, with no login and no account needed. It can **search posts** by keyword, hashtag, author, language or date, get **every post from a profile**, get **profile details** with follower, following and post counts, list a profile's **followers** or the accounts it **follows**, get the **replies to a post**, and list **who liked or reposted** a post. Paste bsky.app links, handles or DIDs, or read them from another run's dataset. Each post comes as one clean row: text, date, author, like, repost, reply and quote counts, hashtags, mentions, links, images, link cards and quoted posts.

### Why this Actor

- **Official API, no login.** Every request goes to Bluesky's public AppView API (`api.bsky.app`), the same public data the Bluesky app shows to someone who isn't signed in. No cookies, no browser, no account of yours at risk.
- **Takes what you copy.** `https://bsky.app/profile/alice.bsky.social`, `@alice.bsky.social`, `alice` (read as alice.bsky.social), `did:plc:...`, post links like `https://bsky.app/profile/alice.bsky.social/post/3l...` and `at://` URIs all work.
- **Accurate rich text.** Hashtags, mentions and links are read from Bluesky's facets, which count UTF-8 bytes, so text with emoji or accents is cut correctly. Link cards, image alt text and quoted posts are parsed too.
- **Polite and reliable.** At most 3 requests at a time, retries when Bluesky is busy or rate-limits (honouring `Retry-After` and `RateLimit-Reset`), and a pause when the rate-limit window runs low. A failed input becomes a free row with an `error` message instead of stopping the run.
- **No duplicates.** A post found by two searches is output (and charged) once per run. If the run is restarted on the platform, rows already saved aren't output or charged again.

### Use cases

- **Brand and keyword monitoring:** search your brand, product or competitors every hour with *Posted after* = `2 hours`, and get alerts for new posts.
- **Social listening and research:** collect posts on a topic or hashtag, in a language, over a date range, with engagement counts.
- **Influencer and lead research:** profile details, follower counts and bios for a list of accounts; followers of a competitor or a community account.
- **Content analysis:** a creator's full posting history with likes, reposts, replies, quotes, links and images.
- **Conversation analysis:** all replies under a post, with their depth in the thread, or the accounts that liked or reposted it.

### How to use it

1. Choose **What to scrape**.
2. Add **Search terms** (Search posts), **Profiles** (Posts from profiles, Profiles, Followers, Following) or **Post links** (Replies, Who liked, Who reposted). Or pick a **Dataset to read inputs from**.
3. Set **Max items per input** (default 100) and, if you like, **Max output rows** for the whole run.
4. Run it. Export as CSV, Excel or JSON, or chain it with an integration.

**Input example: search**

```json
{
  "mode": "search",
  "searchQueries": ["web scraping", "#opendata"],
  "sort": "latest",
  "since": "7 days",
  "lang": "en",
  "maxItemsPerQuery": 200
}
```

**Input example: a profile's posts, without reposts**

```json
{
  "mode": "profilePosts",
  "handles": ["https://bsky.app/profile/bsky.app"],
  "authorFeedFilter": "posts_no_replies",
  "includeReposts": false,
  "maxItemsPerQuery": 500
}
```

**Output example: a post** (shortened)

```json
{
  "uri": "at://did:plc:abc123.../app.bsky.feed.post/3l6abcxyz2k2a",
  "cid": "bafyrei...",
  "url": "https://bsky.app/profile/alice.bsky.social/post/3l6abcxyz2k2a",
  "text": "Café ☕ cats 🐈 #caturday with @bob.bsky.social see example.com/cats",
  "createdAt": "2026-10-20T12:00:00.123Z",
  "indexedAt": "2026-10-20T12:00:01.000Z",
  "lang": "en",
  "langs": ["en"],
  "authorHandle": "alice.bsky.social",
  "authorDid": "did:plc:abc123...",
  "authorDisplayName": "Alice",
  "authorAvatar": "https://cdn.bsky.app/img/avatar/plain/...",
  "replyCount": 2,
  "repostCount": 5,
  "likeCount": 40,
  "quoteCount": 1,
  "isReply": false,
  "replyToUri": null,
  "rootUri": null,
  "isRepost": false,
  "repostedBy": null,
  "hashtags": ["caturday"],
  "mentions": ["bob.bsky.social"],
  "mentionDids": ["did:plc:bob..."],
  "links": ["https://example.com/cats"],
  "images": [{"url": "https://cdn.bsky.app/img/feed_fullsize/plain/...@jpeg", "alt": "A grey cat"}],
  "video": null,
  "externalLink": null,
  "quotedPostUri": null,
  "labels": [],
  "query": "cats",
  "sourceInput": "cats",
  "scrapedAt": "2026-10-22T09:00:00.000Z",
  "error": null
}
```

Other post columns: `replyToHandle`, `repostedAt`, `quotedPostText`, `quotedPostAuthorHandle`, `externalLink` (`{uri, title, description}` for link cards), `video` (`{playlist, thumbnail, alt}`) and `threadDepth` (in *Replies to posts*: 1 = a direct reply, 2 = a reply to a reply, 0 = the post itself).

**Output example: a profile** (shortened)

```json
{
  "did": "did:plc:abc123...",
  "handle": "alice.bsky.social",
  "displayName": "Alice",
  "description": "Cats and code.",
  "avatar": "https://cdn.bsky.app/img/avatar/plain/...",
  "banner": "https://cdn.bsky.app/img/banner/plain/...",
  "followersCount": 1200,
  "followsCount": 310,
  "postsCount": 4210,
  "createdAt": "2023-05-01T10:00:00.000Z",
  "indexedAt": "2024-01-01T00:00:00.000Z",
  "labels": [],
  "pinnedPostUri": "at://did:plc:abc123.../app.bsky.feed.post/3k...",
  "url": "https://bsky.app/profile/alice.bsky.social",
  "sourceInput": "alice",
  "error": null
}
```

Followers rows add `followerOf` and `subjectDid` (the profile they follow); Following rows add `followedBy` and `subjectDid`; Who liked rows add `likedPostUri` and `likedAt`; Who reposted rows add `repostedPostUri`. A run summary (per-input pages, rows, why each input stopped) is saved as `OUTPUT` in the key-value store.

### How much does it cost?

Pay per event: **$1.10 per 1,000 posts or profiles** ($0.0011 per output row), with Apify platform usage included. Each run also has Apify's run start fee of **$0.005** (one start event per GB of run memory; the default 256 MB run is charged one). These rows are free: inputs that aren't a valid handle, DID, link or search term, profiles or posts that don't exist, and inputs Bluesky couldn't answer for (each with an `error`). Duplicates skipped within a run cost nothing.

**Worked examples** (Actor-start charge not included):

- 1,000 posts from a keyword search: 1,000 × $0.0011 = **$1.10**.
- 500 profiles with follower counts: **$0.55**.
- An hourly brand monitor that finds about 20 new posts each run: about $0.022 per run, roughly **$16 per month** for 720 runs.

If you set a maximum cost per run, the Actor stops cleanly at that limit and keeps everything it saved.

### FAQ and limits

#### What data can it get?

Only public data that Bluesky's public API serves without signing in. It can't see private or signed-in-only content, blocked content, deleted posts or direct messages. Accounts that ask Bluesky to hide their content from logged-out viewers may return nothing, or a free error row with Bluesky's reason.

#### How far back does search go?

As far as Bluesky's search returns results. Search is run by Bluesky, and deep into a result list it can stop returning more pages; the Actor then stops that search cleanly and notes why in the run summary. For a long history, run several searches with narrower *Posted after* / *Posted before* ranges, or use *Posts from profiles* for specific accounts. *Top* sort is Bluesky's own ranking.

#### Are the like and follower counts live?

They are the counts at the moment of the run (see `scrapedAt`). Run again to refresh them.

#### Why are followersCount and postsCount empty in Followers mode?

Bluesky lists followers without their counts. Turn on **Add follower and post counts** to look each account up (one extra request per 25 accounts, same price per row).

#### Can it get every reply in a thread?

It gets the replies in Bluesky's public thread view, up to the **Reply depth** you set (default 6 levels). Replies Bluesky doesn't include there, such as deleted or blocked ones, are not output.

#### Does it watch for only new posts?

Each run outputs what it finds. For "only new posts" monitoring, schedule it with *Posted after* (for example `2 hours` on an hourly schedule), or pass the results through Merge & Deduplicate Datasets (below) to keep only posts you haven't seen before.

#### Is it fast?

It stays within Bluesky's public rate limits on purpose: at most 3 requests at a time, 100 posts or accounts per request. When Bluesky rate-limits, the Actor waits for the window to reset and continues.

### Related Actors

| Actor | Use it to |
|---|---|
| [Merge & Deduplicate Datasets](https://apify.com/charlesmarkette/merge-deduplicate-datasets-only-new-items) | Keep only posts you haven't seen in earlier runs, for a "new posts only" monitor. |
| [Send Email Report & Slack, Discord, Teams, Telegram Alerts](https://apify.com/charlesmarkette/send-email-report-slack-discord-teams-telegram-alerts) | Get an alert with the new posts after each scheduled run. |
| [Google Sheets Sync](https://apify.com/charlesmarkette/google-sheets-sync) | Send posts or profiles to a Google Sheet. |
| [Dataset Filter & Transform](https://apify.com/charlesmarkette/dataset-filter-transform-rename-flatten-fields) | Keep only posts above a like count, rename or flatten fields before export. |

Data source: [Bluesky's public AppView API](https://docs.bsky.app/). This Actor is not made by or affiliated with Bluesky Social PBC.

# Actor input Schema

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

Search posts: posts matching your search terms. Posts from profiles: the posts (and reposts) of each profile. Profiles: profile details with follower, following and post counts. Followers / Following: the accounts that follow each profile, or that it follows. Replies to posts: the reply thread under each post. Who liked / Who reposted: the accounts that liked or reposted each post.

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

For Search posts. One search per line. Bluesky's search syntax works: "exact phrase", #hashtag, from:handle, mentions:handle, domain:example.com, lang:en, since:2026-10-01.

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

For Posts from profiles, Profiles, Followers and Following. One per line: a handle (alice.bsky.social or @alice.bsky.social), a DID (did:plc:...) or a profile link (https://bsky.app/profile/alice.bsky.social). A name without a dot, like alice, is read as alice.bsky.social.

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

For Replies to posts, Who liked and Who reposted. One per line: a post link (https://bsky.app/profile/alice.bsky.social/post/3l6abcxyz2k2a) or an at:// post URI.

## `maxItemsPerQuery` (type: `integer`):

Most rows to output for each search term, profile or post (posts, followers, replies...). Not used in Profiles mode, which gives one row per profile.

## `maxResults` (type: `integer`):

Optional cap on the total number of output rows in this run.

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

Search posts only. Latest: newest first. Top: Bluesky's ranking of popular posts.

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

Optional, for Search posts and Posts from profiles. Only posts from this time on: a date like 2026-10-01 (UTC midnight), a time like 2026-10-01T08:00:00Z, or a period back from now like '24 hours', '7 days' or '2 weeks'. For profiles, reposts count from the time they were reposted.

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

Optional, same formats as 'Posted after'. Only posts from before this time.

## `lang` (type: `string`):

Search posts only, optional. A language code such as en, ja, de or pt. Bluesky matches the language the author's app tagged the post with.

## `searchAuthor` (type: `string`):

Search posts only, optional. Only posts by this account (handle, DID or profile link).

## `searchMentions` (type: `string`):

Search posts only, optional. Only posts that mention this account.

## `searchDomain` (type: `string`):

Search posts only, optional. Only posts that link to this domain, for example nytimes.com.

## `searchTags` (type: `array`):

Search posts only, optional. Only posts with all of these hashtags (without the #).

## `authorFeedFilter` (type: `string`):

Posts from profiles only. Which of the profile's posts to list.

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

Posts from profiles only. On: reposts appear too (isRepost: true, with the original author). Off: only the profile's own posts.

## `replyDepth` (type: `integer`):

Replies to posts only. How many levels of replies to follow: 1 = direct replies only, 6 = replies to replies, and so on (up to 1000).

## `includeRootPost` (type: `boolean`):

Replies to posts only. Also output the post the replies are under (threadDepth 0).

## `fullProfiles` (type: `boolean`):

Followers, Following, Who liked and Who reposted only. Bluesky lists these accounts without their counts and banner; this looks each one up to fill followersCount, followsCount, postsCount and banner. Uses more requests (one per 25 accounts), so it is a little slower; the price per row is the same.

## `datasetId` (type: `string`):

Optional. A dataset from another run (for example a list of handles, or posts from this Actor). For each item, the first field below that has a value is used as the search term, profile or post link for the chosen mode. Use {{resource.defaultDatasetId}} in an integration.

## `datasetFields` (type: `array`):

Item fields to read, in order of preference; dotted paths work (author.handle). Defaults: for profiles handle, authorHandle, did, authorDid, profileUrl, url; for posts url, postUrl, uri; for searches query, searchQuery, keyword, term.

## `keepItemFields` (type: `boolean`):

Profiles mode only: each output row is the dataset item plus the profile columns. Off: only the profile columns.

## Actor input object example

```json
{
  "mode": "search",
  "searchQueries": [
    "web scraping"
  ],
  "maxItemsPerQuery": 20,
  "sort": "latest",
  "authorFeedFilter": "posts_with_replies",
  "includeReposts": true,
  "replyDepth": 6,
  "includeRootPost": false,
  "fullProfiles": false,
  "keepItemFields": true
}
```

# Actor output Schema

## `items` (type: `string`):

No description

## `summary` (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": "search",
    "searchQueries": [
        "web scraping"
    ],
    "maxItemsPerQuery": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("charlesmarkette/bluesky-scraper-posts-search-profiles-followers").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": "search",
    "searchQueries": ["web scraping"],
    "maxItemsPerQuery": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("charlesmarkette/bluesky-scraper-posts-search-profiles-followers").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": "search",
  "searchQueries": [
    "web scraping"
  ],
  "maxItemsPerQuery": 20
}' |
apify call charlesmarkette/bluesky-scraper-posts-search-profiles-followers --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,charlesmarkette/bluesky-scraper-posts-search-profiles-followers"
        }
    }
}
```

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/MG0WGxVtCWy1WnBYi/builds/gjFoATAoD9rgzHcvL/openapi.json
