# Bluesky Threads Scraper (`thescrapelab/bluesky-threads-scraper`) Actor

Scrape Bluesky user posts, threads, replies and quotes from handles, profile links or post IDs. Export engagement counts, raw JSON/CSV and conversation graphs. No login.

- **URL**: https://apify.com/thescrapelab/bluesky-threads-scraper.md
- **Developed by:** [Inus Grobler](https://apify.com/thescrapelab) (community)
- **Categories:** Social media, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.75 / 1,000 unique post delivereds

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 Threads Scraper

Collect public Bluesky user posts, reply threads and quote discussions without a Bluesky login. Paste handles, profile links, post links or author/post IDs into one list. Export raw posts with engagement snapshots and the parent/root relationships needed to reconstruct conversations.

### Quick start

1. In **Users or posts**, paste your targets, one per line. Try `@bsky.app` for a user feed or `bsky.app/3l6oveex3ii2l` for a conversation.
2. Set **Maximum posts**. The default 10-post preview costs **$0.00755**, including the start fee.
3. Click **Start**. Use the Output tab to view posts, compare engagement, inspect relationships or download JSON/CSV and graph files.

```json
{
  "postUrls": ["@bsky.app", "bsky.app/3l6oveex3ii2l"],
  "maxPosts": 10
}
```

Only the target list is required. The API key remains `postUrls` and accepts users as well as posts. Maximum posts is **one global limit** across all targets and discussion context. Overlapping post URIs are saved and charged once per run.

### Pricing

**$0.75 per 1,000 unique post rows delivered, plus $0.00005 per run.** Platform usage, conversation graph JSON, relationship CSV and the coverage summary are included. No rental or separate page, request, retry, duplicate or graph fee.

| Delivered posts in one run | Total price |
|---|---:|
| 10 | $0.00755 |
| 100 | $0.07505 |
| 1,000 | $0.75005 |

Authored posts, collected replies, ancestors and quote posts each count as one post when saved. A run with no saved posts has only the start fee. Saved posts remain chargeable if a run later fails or is aborted. The current Console price is authoritative.

Set the spending limit in Apify’s run options. The Actor reduces its output limit to what the remaining budget can buy: **$0.05 permits 66 posts** after the start fee. The summary reports `maxTotalChargeUsd` when this reduces output. Allow at least **$0.0008** for the start plus one post. Requests rejected before creating a run incur no charge.

### What you can collect

- **User posts:** cursor-paginated posts authored by selected users, with their replies included by default. Reposts are excluded.
- **Conversations:** supplied posts, available ancestors, nested replies and known unfinished reply branches.
- **Quote discussions:** one level of available quotes and their replies, with quote edges kept separate from reply relationships.
- **Integration-ready data:** stable post identities, timestamps, source links, public engagement counts, media/rich-text references and deduplicated JSON/CSV graph exports.

Brand/community teams can review responses around accounts and announcements. Researchers can retain discussion context and stable identities. Developers can load the raw posts and typed edges into their own databases and monitoring tools.

### Supported targets

| Target | Result |
|---|---|
| `@bsky.app` or `bsky.app` | The account’s own posts and replies |
| `jay` | User `jay.bsky.social`; use a full handle for other domains |
| A public profile link or user DID | The user’s authored feed |
| `bsky.app/3l6oveex3ii2l` | That post’s available conversation |
| A full Bluesky post link or post AT-URI | That post’s available conversation |

You can mix up to 100 targets. Prefix numeric-looking usernames with `@` to disambiguate them from post IDs.

For a post ID, paste **author/postId** in the same target list, for example `bsky.app/3l6oveex3ii2l`. In JSON/API input you can also supply a bare key with `postAuthor`:

```json
{
  "postUrls": ["3l6oveex3ii2l"],
  "postAuthor": "bsky.app",
  "maxPosts": 10
}
```

Post keys are scoped to an author’s repository. The Actor never guesses an author, and a CID alone is not a supported address. When `postAuthor` is set, unqualified targets are post keys; prefix usernames with `@` to keep them user targets.

### Optional settings

The form has only four optional controls, all for **user targets**. Leave the defaults for a first run. Direct post links automatically collect available conversations, including replies, ancestors and quotes.

| Form control | Default | Purpose |
|---|---|---|
| Include replies written by the user | On | Include the user's authored replies (`includeUserReplies`) |
| Include conversations for user posts | Off | Add available discussions, including other authors (`expandUserThreads`) |
| From date | Unset | First date for authored user posts, inclusive (`since`) |
| Through date | Unset | Last date for authored user posts, inclusive (`until`) |

Dates such as `2026-10-01` use UTC; an end date includes the entire day. ISO timestamps with timezones are also accepted. Date filters select user-feed posts, so direct conversations and optional discussion context may include posts outside the window. For example:

```json
{
  "postUrls": ["@bsky.app"],
  "maxPosts": 100,
  "includeUserReplies": false,
  "since": "2026-10-01",
  "until": "2026-11-04"
}
```

Discussion expansion adds other authors’ posts to the global limit. Hidden quote-list entries count toward the discovery allowance but produce no saved row. Source collection stops at the request limit or after three minutes; already fetched valid records are retained. Repeated pages/cursors and bounded-buffer limits are reported explicitly.

#### JSON/API overrides

Existing integrations keep the same fields, types and defaults. Technical options stay out of the form; use the Input tab's **JSON** editor or the API when you need them.

| API field | Default | Purpose |
|---|---|---|
| `postAuthor` | Unset | Author context for bare post IDs |
| `includeQuotes` | `true` | Discover available quotes for supplied or expanded user posts |
| `includeAncestors` | `true` | Include up to 80 available ancestors for each thread seed |
| `maxQuotesPerInput` | `25` | Source quote-entry allowance per input, shared across a user’s expanded posts |
| `maxReplyDepth` | `1000` | Requested descendant depth; actual source depth can be lower |
| `maxRequests` | `100` | All source HTTP attempts, including lookups and retries |

### Output and downloads

Every row is one unique source post in this run. Missing counters are omitted; real zero counts remain zero.

| Fields | Meaning |
|---|---|
| `uri`, `cid`, `postId` | Full post identity, observed record version and short author-scoped key |
| `url`, `authorDid`, `authorHandle` | Source link and available author identity |
| `text`, `createdAt`, `observedAt` | Source text, posting time and collection time |
| `parentUri`, `rootUri`, `quotedUri` | Actual reply parent, conversation root and quoted post; null when absent |
| `likeCount`, `replyCount`, `repostCount`, `quoteCount` | Public engagement snapshots, where exposed |
| `mediaUrls`, `linkUrls`, `hashtags`, `mentionDids` | Source media references and rich-text metadata |

The table offers **Posts**, **Engagement** and **Relationships** views with readable headings and clickable post links. Raw dataset JSON/CSV retains the full field set. The Output tab also links to:

- **Conversation graph JSON:** collected nodes, unresolved references and zero-based input membership.
- **Reply and quote edges CSV:** `from,to,type` relationships using `replies_to` and `quotes`.
- **Coverage summary:** per-target outcomes, limits, errors and collection counts.

Here is an observed post, with text shortened for display. Counters and timestamps change between snapshots.

```json
{
  "uri": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3l6oveex3ii2l",
  "cid": "bafyreicnt42y6vo6pfpvyro234ac4o6ijug6adwwrh7awflgrqlt4zibxq",
  "postId": "3l6oveex3ii2l",
  "url": "https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur/post/3l6oveex3ii2l",
  "authorDid": "did:plc:z72i7hdynmk6r22z27h6tvur",
  "text": "👋  Bluesky is an open social network…",
  "createdAt": "2024-10-17T07:06:51.491Z",
  "observedAt": "2026-11-03T19:01:44.604Z",
  "parentUri": null,
  "rootUri": null,
  "quotedUri": null,
  "languages": [
    "en"
  ],
  "hashtags": [],
  "mentionDids": [],
  "linkUrls": [],
  "mediaUrls": [],
  "mediaAltTexts": [],
  "authorHandle": "bsky.app",
  "authorDisplayName": "Bluesky",
  "indexedAt": "2024-10-17T07:06:51.491Z",
  "likeCount": 63754,
  "replyCount": 8711,
  "repostCount": 9552,
  "quoteCount": 723,
  "bookmarkCount": 276
}
```

### Python API example

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("thescrapelab/bluesky-threads-scraper").call(run_input={
    "postUrls": ["@bsky.app"],
    "maxPosts": 100,
})
if run is None:
    raise RuntimeError("Actor run failed")

for post in client.dataset(run.default_dataset_id).iterate_items():
    print(post["postId"], post["text"], post["url"])

store = client.key_value_store(run.default_key_value_store_id)
graph = store.get_record("GRAPH")["value"]
coverage = store.get_record("RUN_SUMMARY")["value"]
```

### Coverage, troubleshooting and support

**Fewer posts than requested?** Source visibility, exhausted discovery or a request, time, buffer or spending limit can apply. Check the per-target reasons in the coverage summary. A mixed run preserves valid targets when another fails; zero valid posts produces an explicit failed run.

**Are all replies and quotes guaranteed?** No. The public API can hide branches, omit siblings or return fewer levels than requested. Counters are snapshots, not completeness percentages. Coverage is reported as `partial`, `unknown` or `no_more_discoverable_branches`; the last only means this crawl’s observed frontier ended. Missing records do not establish deletion.

**What is excluded?** Logged-out visibility preferences are honored. Quotes of replies or quotes of quotes need their own target links. Each run is an independent snapshot; keyword search, follower lists, private content and historical change monitoring are outside this Actor’s scope. Media files are referenced rather than downloaded.

**Can I restart an old run?** Start a new run for a fresh snapshot. Restarting a run with saved posts is refused before collection, preserving its output without duplicate post charges.

**Need help?** Open this Actor’s Issues tab with the run ID, a public example target and expected behavior. Do not include access tokens or private information.

Source references: [Bluesky HTTP API](https://endpoints.bsky.app/), [AT URI addressing](https://atproto.com/specs/at-uri-scheme), [thread interface](https://bsky.network/docs/bluesky-api/viewing-threads/) and [visibility preferences](https://bsky.network/docs/about-bluesky-content/visibility-preferences/).

# Actor input Schema

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

One target per line: @handle, username, profile link, post link or author/postId (e.g. bsky.app/3l6oveex3ii2l). Short usernames use .bsky.social. Use @ for numeric-looking usernames.

## `maxPosts` (type: `integer`):

Maximum unique posts saved across all targets. Start with 10. User targets collect their own posts and replies; post targets collect conversations. Your Apify spending limit can stop earlier.

## `includeUserReplies` (type: `boolean`):

For user targets, include replies the selected user wrote. Reposts are always excluded.

## `expandUserThreads` (type: `boolean`):

Also collect available replies, ancestors and quotes around each user post. These may include other authors and count toward Maximum posts.

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

Optional first UTC date for authored user posts, inclusive. Does not filter direct post conversations or added discussion context.

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

Optional last UTC date for authored user posts, inclusive of the whole day. Does not filter direct post conversations or added discussion context.

## `includeQuotes` (type: `boolean`):

Collect available quotes of each supplied post and their reply discussions. Does not recursively discover quotes of quotes.

## `maxRequests` (type: `integer`):

All source requests and retries count. Existing fetched posts are preserved when this limit is reached.

## `maxQuotesPerInput` (type: `integer`):

Maximum source quote-list entries per supplied post, before visibility filtering. Their replies also count toward the global post limit. Set 0 to disable quote discovery.

## `includeAncestors` (type: `boolean`):

Include up to 80 available ancestors above each supplied post and each discovered quote post. Root and parent URIs are preserved even when those posts are unavailable.

## `maxReplyDepth` (type: `integer`):

Maximum descendant distance from each supplied post or quote seed. Default 1000 requests the API maximum; it does not guarantee that depth or complete coverage.

## `postAuthor` (type: `string`):

JSON/API option: author handle, DID or profile link for bare post IDs. In the form, paste author/postId or a full post link instead. With postAuthor set, prefix unqualified usernames with @.

## Actor input object example

```json
{
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"
  ],
  "maxPosts": 10,
  "includeUserReplies": true,
  "expandUserThreads": false,
  "includeQuotes": true,
  "maxRequests": 100,
  "maxQuotesPerInput": 25,
  "includeAncestors": true,
  "maxReplyDepth": 1000
}
```

# Actor output Schema

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

No description

## `csv` (type: `string`):

No description

## `graph` (type: `string`):

No description

## `edges` (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 = {
    "postUrls": [
        "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("thescrapelab/bluesky-threads-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 = { "postUrls": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"] }

# Run the Actor and wait for it to finish
run = client.actor("thescrapelab/bluesky-threads-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 '{
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"
  ]
}' |
apify call thescrapelab/bluesky-threads-scraper --silent --output-dataset

```

## MCP server setup

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