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

Bluesky scraper API for posts and profiles by handle, or a keyword search through topic feeds, live capture or, with your app password, the full archive. Each post row holds the text, time, likes, reposts, replies, quotes, link and author. $2.00 per 1,000 posts.

- **URL**: https://apify.com/dami_studio/bluesky-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Social media, Integrations, AI
- **Features:** Limited permissions
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.94 / 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

**Give it Bluesky handles and get each author's profile and recent posts, with no login**. Give it a
keyword instead and it returns matching posts with engagement counts and links.

Keyword search is the part worth reading before you buy. Without your own app password, a keyword
run reads public topic feeds or a live capture window, not the full archive. And a post on a topic
feed can be there without containing your keyword at all.

| | |
|---|---|
| **Input** | Bluesky handles, a keyword, or both |
| **Output** | One row per post: text, time, likes, reposts, replies, quotes, link and author, plus one profile row per handle with follower counts and bio |
| **Ceiling** | 1,000 posts per handle or per query |
| **Account needed** | None for handles. Your own app password for full archive search |
| **Price** | $2.00 per 1,000 posts on the Free plan, a little less on paid plans. The free plan's $5 a month covers about 2,500 |

### 🔍 What Bluesky Scraper does

Point it at handles and it is simple: one profile row with the follower counts and bio, then that
author's recent posts, each with text, timestamps and the like, repost, reply and quote counts.
Nothing to sign in to.

A keyword is where it forks, and `source` on every row tells you which lane produced it:

| `source` | What it is | Needs |
|---|---|---|
| `archive_search` | The real archive search, back through history | Your own app password |
| `topic_feed` | Recent posts from public Bluesky topic feeds whose own name or description matches your keyword | Nothing |
| `live_capture` | Bluesky's public post stream watched for a set number of seconds, keeping the posts that match | Nothing |

On `auto` it takes the archive when you signed in, topic feeds when a feed matches your keyword, and
live capture when neither applies. Every keyless keyword run also writes one free notice row saying
which lane ran and why.

### 📋 What data you get from each Bluesky post

| What you get | Field |
|---|---|
| The post link and its permanent id | `postUrl`, `uri` |
| The author | `authorHandle`, `authorName`, `authorDid` |
| The text, and when it was posted | `text`, `createdAt` |
| Likes, reposts, replies and quotes | `likeCount`, `repostCount`, `replyCount`, `quoteCount` |
| Language tags the author's app set | `langs` |
| Which lane found it, and whether the text contains your keyword | `source`, `matchedQuery`, `matchesQueryText` |
| The topic feed it came from, and who runs it | `feedName`, `feedUri`, `feedCreator` |
| The handle you asked for, or the live watch window | `queriedHandle`, `capturedDuringSecs` |
| Profile rows: name, bio, followers, following, posts, avatar, banner, join date, link | `displayName`, `description`, `followersCount`, `followsCount`, `postsCount`, `avatar`, `banner`, `createdAt`, `profileUrl` |

### ▶️ How to scrape Bluesky posts

1. Open [Bluesky Scraper](https://apify.com/dami_studio/bluesky-scraper) and click **Try for free**.
2. For authors, put handles into **Author handles** and clear **Search query**.
3. For a keyword, type it into **Search query**. For the full archive, add your handle and an app
   password from bsky.app.
4. Set **Max items per query/author**, then click **Start**.
5. If you ran a keyword without credentials, read the notice row first. It says which lane answered.

Start it with nothing filled in and you get one labelled sample row, free.

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

**$2.00 per 1,000 posts** on the Free plan. Paid plans pay a little less: $1.98 on Starter, $1.96 on Scale and $1.94 on Business. On the free plan, the $5 Apify
gives you each month covers about 2,500 posts.

Posts are what you pay for. Profile rows, notice rows, the sample row and diagnostic rows are all
free, duplicates and posts left out by `language` are dropped before they reach you, and a run that
returns no posts costs you nothing.

One thing to know before you budget: **on the `topic_feed` lane you pay for every post the feed
returned, including ones where `matchesQueryText` is `false`.** If you only want literal matches,
sign in for the archive or use live capture.

### 📥 What you give it

```json
{
  "searchQuery": "artificial intelligence",
  "searchMode": "auto",
  "authorHandles": ["bsky.app"],
  "maxItems": 100,
  "liveWatchSecs": 60
}
```

| Field | Default | What it is |
|---|---|---|
| `searchQuery` | none | Keywords to match. Quoted phrases stay whole, and several bare words must all appear. The form opens with `artificial intelligence` filled in. |
| `searchMode` | `auto` | `auto`, `live` or `archive`. `archive` refuses to substitute another lane: with no credentials it says so and returns nothing. |
| `authorHandles` | `[]` | Handles to scrape, like `jay.bsky.team`. A leading `@` is fine. The form opens with `bsky.app`. |
| `language` | `any` | Keep only posts tagged with this language, like `es`. Works on author handles and topic feeds; posts in other languages and untagged posts are left out, uncharged. A keyword that would go to the archive or live capture is refused with a free `BAD_INPUT` row. |
| `blueskyIdentifier` | none | Your own handle or email. Only used to reach the archive search. |
| `blueskyAppPassword` | none | An app password from bsky.app, Settings, Privacy and security, App passwords. **Never your account password.** Marked secret. |
| `maxItems` | `100` | Per query and per handle, up to 1,000. Three handles at 100 is up to 300 rows. |
| `liveWatchSecs` | `60` | How long live capture watches the stream, 10 to 900 seconds. It stops early once `maxItems` is reached. |
| `notionConnector`, `notionParentId` | none | Optional. Write every post into your own Notion as well as the dataset. |
| `proxyConfiguration` | off | Optional, and off by default because a normal run does not need it. |

**A rejected app password does not stop the run.** It logs a warning and falls back to the keyless
lanes, so check `source` on the rows if you expected archive results. With a `language` set, each
handle and each feed is read at most 1,000 posts deep, so a language an account rarely uses can
return fewer than `maxItems`.

### 📤 What you get back

A real post row from a keyless run, with the text cut short:

```json
{
  "ok": true,
  "type": "post",
  "uri": "at://did:plc:thtnfvzp2gvyeke24vvrlh4k/app.bsky.feed.post/3mvhejxpvac2g",
  "postUrl": "https://bsky.app/profile/hustlrbase.bsky.social/post/3mvhejxpvac2g",
  "authorHandle": "hustlrbase.bsky.social",
  "authorName": "Hustle with Elisa",
  "authorDid": "did:plc:thtnfvzp2gvyeke24vvrlh4k",
  "text": "“Overwhelmed by Managing Multiple Client Accounts?” ... #ClientManagement #Automation #AI",
  "createdAt": "2026-09-14T05:22:45.479Z",
  "likeCount": 0,
  "repostCount": 0,
  "replyCount": 0,
  "quoteCount": 0,
  "langs": [],
  "feedName": "Artificial Intelligence",
  "feedUri": "at://did:plc:ugbenc43qu4v3hemephm2cry/app.bsky.feed.generator/aaamqhwmov3y4",
  "feedCreator": "raefmeeuwisse.bsky.social",
  "matchesQueryText": false,
  "source": "topic_feed",
  "matchedQuery": "artificial intelligence"
}
```

| Field | How to read it |
|---|---|
| `matchesQueryText` | Whether your keyword appears in the post text itself. On the `topic_feed` lane it is often `false`, as above: the feed matched your keyword, and the post is just on that feed. Filter on it when you need a literal match. |
| `source` | Always check this before drawing a conclusion about coverage. |
| `uri` | Stable, so use it to dedupe across runs. |
| `langs` | Often empty, because many apps set no language tag. |

### 🧾 Reading the output

Five kinds of row can share the dataset.

| Row | How to spot it | Billed |
|---|---|---|
| A post | `type: "post"` | yes |
| A profile | `type: "profile"` | no |
| A notice | `_notice: true` | no |
| A diagnostic | `ok: false` and an `errorCode` | no |
| The sample row | `_sample: true` | no |

**Filter on `type`, not on the presence of `text`.** Notice and profile rows share the dataset with
posts, and the default table view renders them as partly blank lines. With a `language` set, the
topic-feed notice row also counts the posts read, left out, and left out for having no tag.

| Code | What it means |
|---|---|
| `BAD_INPUT` | You asked for `archive` mode with no app password, or set a `language` on a keyword that would go to the archive or live capture. Nothing was read. |
| `NO_RESULTS` | The lane ran and matched nothing, that handle returned neither a profile nor posts, or none of the posts read was in your `language`. The row says how many were read. |
| `NOT_FOUND` | That handle does not resolve. Check the spelling, including the `.bsky.social` part. |
| `RATE_LIMITED` | Bluesky asked for a slower pace. Re-run with a smaller `maxItems`. |
| `SERVER_ERROR` | Bluesky answered with a server error. Usually passes on its own. |
| `NOT_SERVED` | This post was not served this run. Re-run it later. Runs before 13 October 2026 wrote `BLOCKED` here, and runs from 13 to 17 October wrote `NETWORK`. |
| `NETWORK` | Bluesky could not be reached, including when a supplied credential was refused. |
| `TIME_LIMIT` | The run got close to its time limit and stopped on its own. What it delivered before that is complete and charged as usual. Give the run more time, or ask for fewer posts, to get the rest. |

A run that ends on one of these rows still finishes as succeeded, including when the time limit
stopped it before any post or profile came back: the `TIME_LIMIT` row and an amber status message say so.

### 💡 What people use it for

- Watching a handful of accounts daily, deduped on `uri`, to see what they posted and how it landed.
- Catching mentions of a product name as they happen, with `searchMode` on `live` and a longer
  window.
- Pulling a topic feed's recent posts to see who is active in a niche.
- Backfilling a keyword properly with an app password, then keeping it current with short live runs.

### 🚧 What it does not do

- **No archive search without your own app password**, and no setting here changes that.
- **Live capture only sees the window.** It watches the public stream for the seconds you allow and
  returns the matches inside it. A rare keyword can produce zero.
- **Fresh live posts look unpopular.** A post captured seconds after it was written has no likes yet.
- **Author timelines are recent posts only,** capped at 1,000 per handle. It is not a full account
  export.
- **Topic feeds are somebody else's editorial choice.** Those rows reflect the feed, not a search.
- **A language goes by the tag the author's app set.** Plenty of accounts, news outlets among them,
  post with no tag at all, and those posts are left out once you pick a language.
- **No threads, no replies as a tree, no follower lists, no direct messages.**
- **No account is created for you.** The app password stays yours, and archive search runs through
  your own account.
- It is not an official Bluesky tool. Dami's Studio is independent and is not affiliated with or endorsed by Bluesky, or by any other company named on this page.

### 🧭 Which social scraper do you need?

| If you want | Use |
|---|---|
| Posts from Bluesky accounts or a Bluesky search | This one |
| Posts from a Mastodon server by hashtag, account or timeline | [Mastodon Scraper](https://apify.com/dami_studio/mastodon-scraper) |
| Profiles and recent posts from Threads | [Threads Scraper](https://apify.com/dami_studio/threads-scraper) |
| Posts from a Lemmy community or instance | [Lemmy Scraper](https://apify.com/dami_studio/lemmy-scraper) |

### ❓ Questions people ask

#### Do I need a Bluesky account to scrape posts?

Only for archive keyword search. Handles, profiles, timelines, topic feeds and live capture all work
signed out.

#### What is a Bluesky app password?

A separate password Bluesky issues for tools, under Settings, Privacy and security. You can revoke it
any time, and it is not your account password.

#### Why did my keyword return posts that do not mention it?

You were on the topic-feed lane. Check `source` and `matchesQueryText` on the rows, and read the
notice row.

#### Why did live capture return nothing?

Nobody posted a match during the window. Raise `liveWatchSecs`, or sign in and use the archive.

#### Can I call it from code or connect it to an AI assistant?

The [API tab](https://apify.com/dami_studio/bluesky-scraper/api/python) has code for Python, JavaScript and the CLI.
MCP clients like Claude and ChatGPT use
`https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/bluesky-scraper`; coding assistants can load [our skill
file](https://api.apify.com/v2/key-value-stores/BTBHU3DdiuoF4JOsK/records/apify-social-communities-SKILL.md). All
three bill your Apify account at the same price.

#### Is scraping Bluesky legal?

These are public posts on a public network. They are still personal data, which GDPR and similar laws
cover, so have a reason for collecting them. Apify's write-up on
[the legality of web scraping](https://blog.apify.com/is-web-scraping-legal/) is a good starting
point, and we are not lawyers.

### 🆘 If something breaks

Open the **Issues** tab on the actor page. Send the run ID, the handle or keyword, and the `source`
value on the rows you got. The `errorCode` on the diagnostic row usually names the problem on its own.

# Actor input Schema

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

Keyword(s) to match in Bluesky posts (e.g. "artificial intelligence"). Quoted "phrases" stay whole and several words must all appear. With a Bluesky handle + app password below this searches the full post archive; without one it captures matching posts live from Bluesky's public post stream, because Bluesky no longer serves archive search to signed-out clients. Leave empty to scrape specific authors instead.

## `searchMode` (type: `string`):

How to answer a search query. "Auto" uses the full archive when you supply an app password and captures live posts when you do not. "Live capture" always watches the public post stream. "Archive only" never substitutes live capture: without credentials it reports that archive search is unavailable and returns nothing (uncharged).

## `blueskyIdentifier` (type: `string`):

Your Bluesky handle or email, e.g. yourname.bsky.social. Needed ONLY to search the full post archive - Bluesky returns 403 for signed-out archive search. Author handles, profiles and live capture need no login.

## `blueskyAppPassword` (type: `string`):

An app password from bsky.app -> Settings -> Privacy and security -> App passwords. Never your account password. Needed ONLY to search the full post archive.

## `authorHandles` (type: `array`):

Bluesky handles to scrape (e.g. bsky.app, jay.bsky.team). For each handle the actor returns the author's profile plus their recent posts. The leading @ is optional. Leave empty if using a Search query instead.

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

Keep only posts their author tagged with this language. Works on author handles and on the topic-feed lane. Posts with no language tag are left out, since there is nothing to check them against, and some accounts never set one. Left-out posts are not charged. Each handle and each topic feed is read at most 1,000 posts deep, so a language an account rarely uses can return fewer posts than you asked for. Archive search and live capture do not take a language: a keyword that would use either is refused before anything is read.

## `liveWatchSecs` (type: `integer`):

How long live capture watches Bluesky's public post stream before it stops. It also stops early once it has collected "maxItems" matches. Common keywords fill up in seconds; a rare keyword needs a longer window.

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

Maximum number of posts to return per search query or per author handle. Pagination follows the API cursor until this limit is reached.

## `notionConnector` (type: `string`):

Optional. Write each post as a page into your Notion when the run finishes. Authorize a Notion connector once in Settings → API & Integrations → MCP connectors, then pick it here. Leave empty to skip (default), results are always saved to the dataset regardless.

## `notionParentId` (type: `string`):

Optional. The Notion data source ID of the database to write into (only used if a Notion connector is set). Leave empty to create the pages privately in your workspace instead.

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

Optional, and not needed for a normal run.

## Actor input object example

```json
{
  "searchQuery": "artificial intelligence",
  "searchMode": "auto",
  "authorHandles": [
    "bsky.app"
  ],
  "language": "any",
  "liveWatchSecs": 60,
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Scraped rows are stored in the default dataset (one row per result). Blocked/empty/error runs return a single uncharged diagnostic row instead.

# 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 = {
    "searchQuery": "artificial intelligence",
    "authorHandles": [
        "bsky.app"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/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 = {
    "searchQuery": "artificial intelligence",
    "authorHandles": ["bsky.app"],
}

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

```

## MCP server setup

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