# Bluesky Scraper – Posts, Profiles & Search (`sfoln/bluesky-scraper`) Actor

Bluesky posts and profiles without login: keyword search (latest or top, date and language filters), the posts of any account, full reply threads, and profiles with follower, following and post counts. Text, links, images, likes, reposts, replies. $0.80 per 1,000 posts, $2 per 1,000 profiles.

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

## Pricing

from $0.80 / 1,000 bluesky posts

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

Scrape **Bluesky posts and profiles without logging in**: keyword search (latest or top, with date and language filters), the posts of any account, full reply threads, and profiles with follower, following and post counts. One clean record per post or profile, for social listening, brand and competitor monitoring, research datasets, lead lists and AI agents. **$0.80 per 1,000 posts, $2 per 1,000 profiles**, no start fee; unknown accounts, deleted posts and errors are free.

- **What you get:** post text, author handle, DID and name, created and indexed dates, languages, reply/repost/like/quote counts, links, mentions, hashtags, images, video, link card, quoted post, reply parent and root, labels; profiles with bio, avatar, follower, following and post counts, creation date and verification.
- **Price:** `post` $0.0008 per post, `profile` $0.002 per profile.
- **Works everywhere:** API, MCP for AI agents, Make, Zapier, n8n, schedules and webhooks.

### Input

Choose a `mode` and fill its list. Each query, account or post URL is one **item** with its own summary record.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` (posts matching keywords), `author-feed` (posts of accounts), `profiles` (profile records) or `threads` (a post with its parents and replies). |
| `searchQueries` | string\[] | – | Search mode: keywords, phrases or hashtags. Bluesky's search syntax works (`"exact phrase"`, `from:handle`, `domain:example.com`). |
| `handles` | string\[] | – | Author-feed and profiles modes: handles (`jay.bsky.team`, `@bsky.app`), DIDs or profile URLs. A bare name means `name.bsky.social`. |
| `postUrls` | string\[] | – | Threads mode: post URLs or `at://` URIs. |
| `maxPostsPerInput` | integer | 100 | Posts per query, account or thread (1–10,000). |
| `sort` | string | `latest` | Search mode: `latest` (newest first) or `top` (Bluesky's ranking, first 100 results). |
| `since` / `until` | string | – | Search mode: date (`2026-01-31`) or ISO date-time limits. |
| `lang` | string | – | Search mode: only posts in this language (`en`, `de`, `pt-BR`…). |
| `feedFilter` | string | `posts` | Author-feed mode: `posts` (no replies), `replies` (posts and replies), `media`, `threads` (posts and own threads) or `videos`. |
| `includeReposts` | boolean | false | Author-feed mode: also return what the account reposted (marked `isRepost`). |
| `threadDepth` | integer | 6 | Threads mode: reply levels to follow (0 = the post only). |
| `includeParents` | boolean | true | Threads mode: also return the posts above, up to the thread's root. |
| `maxItems` | integer | 1000 | Safety cap on items per run; the rest is reported in `OUTPUT` as `inputItemsDropped`. |
| `concurrency` | integer | by mode | Items in parallel (1–10): 2 for search, 3 for posts and threads, 10 for profiles. |
| `includeRaw` | boolean | false | Attach Bluesky's unmodified JSON under `raw`. |

```json
{ "mode": "search", "searchQueries": ["apify", "\"web scraping\""], "sort": "latest", "since": "2026-10-01", "lang": "en", "maxPostsPerInput": 200 }
```

```json
{ "mode": "profiles", "handles": ["bsky.app", "jay.bsky.team"] }
```

### Output

Every record has `recordType`: `post`, `profile`, or `summary` (one per item in the search, author-feed and threads modes, free). In the profiles mode the `profile` record is itself the item's record.

Post:

```json
{
  "input": { "mode": "search", "query": "launch" },
  "recordType": "post",
  "position": 1,
  "uri": "at://did:plc:synthetic1aaaaaaaaaaaaaa/app.bsky.feed.post/3mxulfla27c2e",
  "cid": "bafyrei3mxulfla27c2ecid",
  "url": "https://bsky.app/profile/alex-example.bsky.social/post/3mxulfla27c2e",
  "postId": "3mxulfla27c2e",
  "authorDid": "did:plc:synthetic1aaaaaaaaaaaaaa",
  "authorHandle": "alex-example.bsky.social",
  "authorDisplayName": "Alex Example",
  "authorAvatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:synthetic1aaaaaaaaaaaaaa/bafkreiavatar1@jpeg",
  "text": "Launch day 🚀 Notes at example.com/launch, thanks @sam-example.bsky.social #webdev",
  "createdAt": "2026-10-14T21:56:03.120Z",
  "indexedAt": "2026-10-14T21:56:03.120Z",
  "langs": ["en"],
  "replyCount": 2,
  "repostCount": 3,
  "likeCount": 10,
  "quoteCount": 1,
  "isReply": false,
  "replyParentUri": null,
  "replyRootUri": null,
  "isQuote": false,
  "quotedPostUri": null,
  "embedType": "external",
  "links": ["https://example.com/launch"],
  "mentions": ["sam-example.bsky.social"],
  "hashtags": ["webdev"],
  "images": [],
  "videoUrl": null,
  "videoThumbnailUrl": null,
  "externalLink": { "url": "https://example.com/launch", "title": "Launch notes", "description": "What shipped today.", "thumbUrl": "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:synthetic1aaaaaaaaaaaaaa/bafkreithumb1@jpeg" },
  "labels": [],
  "status": "ok",
  "charged": true,
  "chargedEvent": "post",
  "source": "bluesky",
  "fetchedAt": "2026-10-14T22:00:00.000Z"
}
```

Profile and a summary record:

```json
[
  {
    "input": { "mode": "profiles", "handle": "bsky.app" },
    "recordType": "profile",
    "did": "did:plc:z72i7hdynmk6r22z27h6tvur",
    "handle": "bsky.app",
    "displayName": "Bluesky",
    "description": "official Bluesky account",
    "url": "https://bsky.app/profile/bsky.app",
    "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihwihm6kpd6zuwhhlro75p5qks5qtrcu55jp3gddbfjsieiv7wuka",
    "banner": null,
    "followersCount": 35258853,
    "followingCount": 19,
    "postsCount": 876,
    "createdAt": "2023-04-12T04:53:57.057Z",
    "indexedAt": "2025-10-27T21:05:26.152Z",
    "labels": [],
    "pinnedPostUri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l",
    "listsCount": 18,
    "feedsCount": 7,
    "starterPacksCount": 15,
    "isLabeler": false,
    "verified": false,
    "status": "ok",
    "charged": true,
    "chargedEvent": "profile",
    "source": "bluesky",
    "fetchedAt": "2026-10-14T22:00:00.000Z"
  },
  { "input": { "mode": "search", "query": "launch" }, "recordType": "summary", "query": "launch", "sort": "latest", "hitsTotal": 1130, "resultsDelivered": 200, "stopReason": "limit", "status": "ok", "charged": false, "source": "bluesky", "fetchedAt": "2026-10-14T22:00:05.000Z" }
]
```

| Field | Meaning |
| --- | --- |
| `uri`, `url`, `postId`, `cid` | The post: its permanent `at://` URI and its bsky.app link. |
| `authorDid`, `authorHandle`, `authorDisplayName`, `authorAvatar` | The author (the DID never changes; handles can). |
| `text`, `createdAt`, `indexedAt`, `langs` | Post text, when it was written and indexed, and its language tags. |
| `replyCount`, `repostCount`, `likeCount`, `quoteCount` | Engagement at fetch time. |
| `isReply`, `replyParentUri`, `replyRootUri`, `isQuote`, `quotedPostUri` | Reply and quote relations. |
| `embedType`, `images`, `videoUrl`, `externalLink`, `links`, `mentions`, `hashtags`, `labels` | Media, link card, entities and labels. |
| `isRepost`, `repostedByHandle`, `repostedAt` | Author-feed mode with `includeReposts`. |
| `threadRole`, `threadDepth` | Threads mode, in delivery order: `anchor` (the requested post, 0), its `reply` rows (1, 2…), then `parent` rows from the nearest (-1) up to the root. |
| `followersCount`, `followingCount`, `postsCount`, `verified` | Profile counts and verification. |
| `resultsDelivered`, `stopReason`, `duplicatesSkipped`, `hitsTotal` | Summary records: posts delivered, why the item stopped (`limit`, `end`, `free_plan`, `budget`, `deadline`, `empty_pages`, `error`), posts already delivered for an earlier input, matches reported by search. |

`status` is one of:

- `ok`: delivered (post rows and profile records charged; summaries free);
- `not_found`: unknown account, or deleted or blocked post (not charged);
- `error`: Bluesky failed or refused the request (not charged);
- `skipped_invalid`: not a handle, DID, profile URL or post URL (not charged);
- `skipped_budget`: your **Maximum cost per run** was reached (not charged);
- `skipped_free_plan`: beyond the free-plan limit below (not charged);
- `skipped_unmetered`: the run is not pay-per-event (not charged).

The run's key-value store `OUTPUT` record contains totals (counts, charged amount).

### Pricing

Pay-per-event, charged only for delivered records:

| Event | Price | When |
| --- | --- | --- |
| `post` | $0.0008 | per post row (search, author feed or thread) |
| `profile` | $0.002 | per profile record |

Examples: 1,000 posts cost $0.80; 500 profiles cost $1. A post found by several inputs of one run is delivered and charged once. Set **Maximum cost per run** to cap spend: posts are sized to the remaining budget before each page is fetched, and the Actor stops cleanly when the cap is reached. Accounts on the Apify free plan can process up to 10 items per run (an item is one query, account or post) and 20 posts per item; further items are returned with `status: "skipped_free_plan"` and are not charged. Upgrade to any paid Apify plan for full batches.

### Use with AI agents (MCP)

Use this Actor from Claude, ChatGPT, Cursor, VS Code or any other MCP client through the [Apify MCP server](https://mcp.apify.com). Add this server URL to your client and sign in with your Apify account when asked:

```text
https://mcp.apify.com?tools=sfoln/bluesky-scraper
```

To run the MCP server locally instead, start `npx -y @apify/actors-mcp-server --tools sfoln/bluesky-scraper` with your `APIFY_TOKEN` in the environment.

Example prompt:

> Find the 50 latest Bluesky posts that mention "apify" and summarise what people say about it.

Runs started by an agent are billed like any other run (see Pricing). Agents can cap what a run may spend with `maxTotalChargeUsd`.

### API usage

Run the Actor and get its dataset items in one call (the call waits up to 5 minutes for the run to finish):

```bash
curl -X POST "https://api.apify.com/v2/acts/sfoln~bluesky-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer <APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"searchQueries": ["bluesky"], "maxPostsPerInput": 10}'
```

For large inputs, start the run asynchronously and download the dataset when it has finished:

```bash
## 1. Start a run: the response holds the run ID (data.id) and its dataset ID (data.defaultDatasetId)
curl -X POST "https://api.apify.com/v2/acts/sfoln~bluesky-scraper/runs" \
  -H "Authorization: Bearer <APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"searchQueries": ["bluesky"], "maxPostsPerInput": 10}'
## 2. Repeat until data.status is SUCCEEDED (each call waits up to 60 s)
curl "https://api.apify.com/v2/actor-runs/<runId>?waitForFinish=60" -H "Authorization: Bearer <APIFY_TOKEN>"
## 3. Download the results (format=json, csv or xlsx)
curl "https://api.apify.com/v2/datasets/<defaultDatasetId>/items?format=json" -H "Authorization: Bearer <APIFY_TOKEN>"
```

Python ([apify-client](https://pypi.org/project/apify-client/)):

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("sfoln/bluesky-scraper").call(run_input={"searchQueries": ["bluesky"], "maxPostsPerInput": 10})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

Instead of polling, you can attach a webhook to the run (see Integrations). The API tab of this page shows the same calls for JavaScript and the Apify CLI.

### Integrations

- **Make, Zapier and n8n:** run `sfoln/bluesky-scraper` with the Apify app (in n8n, the Apify node) and pass the dataset items to the next step of your workflow.
- **Google Sheets:** add the items to a sheet with the Google Sheets step of Make, Zapier or n8n, or download them as CSV or Excel from the run's Storage tab or the API (`format=csv` or `format=xlsx`).
- **Webhooks:** get a POST request when a run succeeds or fails: add a webhook in the Actor's Integrations tab, or pass `webhooks` when you start a run through the API.
- **Schedules:** run it hourly, daily or weekly with the same input from Apify Schedules; every run keeps its results in its own dataset.
- **More:** Slack, Google Drive, GitHub, LangChain, LlamaIndex and other [Apify integrations](https://docs.apify.com/platform/integrations).

### Related Actors

- [Brand Monitoring – X, Reddit, News & Bluesky](https://apify.com/sfoln/brand-mentions-monitor)
- [Reddit Posts Scraper (Subreddits)](https://apify.com/sfoln/reddit-subreddit-posts)
- [Threads Profile Scraper](https://apify.com/sfoln/threads-profile-lookup)
- [Instagram Hashtag Scraper (No Login)](https://apify.com/sfoln/instagram-hashtag-posts)
- [Facebook Posts Scraper – Pages & Profiles](https://apify.com/sfoln/facebook-page-posts)

See [all our Actors](https://apify.com/sfoln) on Apify Store.

### FAQ

#### How much does it cost, and am I charged for failed items?

You pay per event at the prices under Pricing above, plus a run start fee of $0.00005 per GB of memory, with no subscription; platform usage is included. Errors and skipped items (`skipped_…` statuses) are never charged; a lookup that finds nothing is charged only where Pricing says so, and every record's `charged` field shows whether it was billed. Set **Maximum cost per run** to cap what a single run can spend.

#### How fresh is the data?

Each run queries its source while it runs, so you get what the source returns at that moment. Source-specific details, such as how often a provider refreshes its own database, are under Limits & notes.

#### What are the limits?

Per-run caps (for example the maximum number of items) are listed under Input, source limits under Limits & notes, and the free-plan limit under Pricing. For big batches, start the run asynchronously (see API usage) and read the dataset when it has finished.

#### Can I schedule runs or get results by webhook?

Yes. Create a schedule in Apify Console to run it hourly, daily or weekly with the same input, and add a webhook to get a POST request when a run finishes (see Integrations).

#### What does this Actor record about my runs?

This Actor records operational metrics about each run (item counts, duration, charges, error codes and a pseudonymous hash of your Apify user ID) to monitor reliability and costs. It never stores your input or results.

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

No. The Actor reads Bluesky's public API, the same data the Bluesky web app shows to logged-out visitors.

#### How far back does keyword search go?

With sort "latest", the Actor pages back in time until it has maxPostsPerInput posts or reaches since. Bluesky serves only the first page (up to 100 posts) of a "top" search to logged-out clients.

#### Why does a post appear only once when two of my queries match it?

Within one run a post is delivered and charged once, under the first input that found it; the summary record of the other input counts it in duplicatesSkipped.

#### How do I get help?

Open an issue in the Actor's Issues tab and include the link to the run, so we can reproduce the problem. Feature requests are welcome there too.

### Limits & notes

- **Data source:** Bluesky's public AppView API (`public.api.bsky.app`, and `api.bsky.app` for search), live at run time. No login, cookies or app password.
- **Search depth:** Bluesky serves logged-out clients only the first page of a search, so `latest` search pages back in time instead (each page starts before the oldest post of the previous one) until `maxPostsPerInput` or `since`; `top` search returns at most 100 posts per query.
- **Pace:** a few requests per second per run (Bluesky asks for a handful of concurrent requests at most). 1,000 posts take about 10 requests; profiles come 25 per request.
- **Threads:** the requested post first, then its replies (up to `threadDepth` levels), then the chain of parents, so a `maxPostsPerInput` cap never drops the post you asked for. Deleted and blocked replies are skipped.
- **Posts found by several inputs** are delivered and charged once per run. The run keeps that list in memory: if Apify moves a run to another server part-way (a migration), a post delivered before the move can be delivered and charged once more under a later input.
- Not returned: follower and following lists, likes of a post, private or deleted posts, direct messages.

# Actor input Schema

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

search: posts matching keywords. author-feed: the posts of accounts. profiles: one profile record per account. threads: a post with its parents and replies.

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

For the search mode: keywords, phrases or hashtags, one per line. Bluesky's search syntax works too ("exact phrase", from:handle, domain:example.com). Accounts on the Apify free plan can process up to 10 items per run (an item is one query, account or post).

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

For the author-feed and profiles modes: handles (jay.bsky.team, @bsky.app), DIDs (did:plc:…) or profile URLs, one per line. A bare name means name.bsky.social.

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

For the threads mode: post URLs (https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l) or at:// URIs, one per line.

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

Posts returned per query, account or thread (1–10,000).

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

Search mode: latest (newest first, pages back in time) or top (Bluesky's ranking; first 100 results only).

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

Search mode: only posts from this date or date-time on (2026-01-31 or 2026-01-31T12:00:00Z).

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

Search mode: only posts before this date or date-time.

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

Search mode: only posts in this language (en, de, ja, pt-BR…).

## `feedFilter` (type: `string`):

author-feed mode: posts (no replies), replies (posts and replies), media (posts with images or video), threads (posts and the account's own threads) or videos.

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

author-feed mode: also return the posts the account reposted (marked isRepost).

## `threadDepth` (type: `integer`):

threads mode: levels of replies to follow (0 = the post only).

## `includeParents` (type: `boolean`):

threads mode: also return the posts above the given one, up to the thread's root.

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

Safety cap on queries, accounts or posts processed in this run; the rest is reported as dropped.

## `concurrency` (type: `integer`):

Items processed in parallel (1–10). Default: 2 for search, 3 for posts and threads, 10 for profiles (fetched 25 per call).

## `includeRaw` (type: `boolean`):

Attach Bluesky's unmodified JSON to each record under raw (larger output).

## Actor input object example

```json
{
  "mode": "search",
  "searchQueries": [
    "bluesky"
  ],
  "maxPostsPerInput": 10,
  "sort": "latest",
  "feedFilter": "posts",
  "includeReposts": false,
  "threadDepth": 6,
  "includeParents": true,
  "maxItems": 1000,
  "includeRaw": false
}
```

# Actor output Schema

## `results` (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 = {
    "searchQueries": [
        "bluesky"
    ],
    "maxPostsPerInput": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("sfoln/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 = {
    "searchQueries": ["bluesky"],
    "maxPostsPerInput": 10,
}

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

```

## MCP server setup

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