# Bluesky Profile Scraper (`axiomworks/bluesky-profile-scraper`) Actor

Collect Bluesky profiles, posts, followers, follows and lists, or search for accounts by keyword. Get handles, DIDs, bios, profile counts, post text and engagement metrics in structured rows. Uses the public Bluesky API without login or a browser, with cursor-based pagination.

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

## Pricing

from $1.05 / 1,000 results

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

**Returns:** one row per Bluesky profile, post, follower, follow or list with `id`, `recordType`, `did`, `handle`, `displayName`, `createdAt`, `sourceUrl`; profile counts and post text depend on the mode.
**Input:** `{"mode": "profiles", "handles": ["bsky.app", "jay.bsky.team"], "maxItems": 10}`
**Best for:** lead lists of Bluesky accounts, follower and audience analysis, social listening on an account's posts.

```json
{
    "id": "did:plc:z72i7hdynmk6r22z27h6tvur",
    "recordType": "profile",
    "handle": "bsky.app",
    "displayName": "Bluesky",
    "followersCount": 35250704,
    "followsCount": 19,
    "postsCount": 870,
    "sourceUrl": "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles?actors=bsky.app&actors=jay.bsky.team&actors=atproto.com"
}
```

### What does Bluesky Profile Scraper do?

Bluesky Profile Scraper is a Bluesky scraper and Bluesky API wrapper in one Actor. It reads the open, keyless public Bluesky API and returns clean rows you can export as JSON, CSV or Excel. You give it handles, DIDs or profile links, choose a mode, and it collects profile details, an account's posts, its followers, the accounts it follows, its lists, or the results of a keyword search for accounts.

There is no login and no browser involved, so runs are fast and cheap. The Actor sends about two to three requests per second, follows the cursor pagination the API provides, and retries politely when the API asks it to slow down. Every row carries the `sourceUrl` of the API call it came from and a stable `id`, so you can de-duplicate across runs.

Typical jobs are building lead lists of accounts in a niche, measuring how big an account is and who follows it, tracking what a brand or creator posts and how many likes and reposts it gets, and mapping which accounts an organisation follows.

### What data can you get from Bluesky?

Each row has a `recordType` that tells you which fields to expect. Fields that do not apply to a type are empty.

| Field | Description | Example |
|---|---|---|
| `recordType` | profile, post, follower, follow or list | `profile` |
| `id` | Stable unique ID of the row | `did:plc:z72i7hdynmk6r22z27h6tvur` |
| `did` | Decentralized ID of the account (the author for posts, the creator for lists) | `did:plc:z72i7hdynmk6r22z27h6tvur` |
| `handle` | Account handle | `bsky.app` |
| `displayName` | Public display name | `Bluesky` |
| `description` | Profile bio, or list description | `official Bluesky account` |
| `avatar` | Avatar image URL | `https://cdn.bsky.app/img/avatar/...` |
| `banner` | Banner image URL (profiles mode) | `https://cdn.bsky.app/img/banner/...` |
| `pinnedPostUri` | AT URI of the pinned post (profiles mode) | `at://did:plc:.../app.bsky.feed.post/3l6oveex3ii2l` |
| `labels` | Moderation label values on the account (profiles mode) | `[]` |
| `listsCount` | Lists the account created (profiles mode) | `18` |
| `feedGeneratorsCount` | Custom feeds the account created (profiles mode) | `7` |
| `starterPacksCount` | Starter packs the account created (profiles mode) | `15` |
| `isLabeler` | Whether the account is a moderation labeler (profiles mode) | `false` |
| `profileUrl` | Public bsky.app profile page (profiles mode) | `https://bsky.app/profile/bsky.app` |
| `createdAt` | Account creation time, or post creation time | `2023-04-12T04:53:57.057Z` |
| `verified` | Verification status, null when absent from the response | `none` |
| `followersCount` | Number of followers (profiles mode) | `35133356` |
| `followsCount` | Number of accounts followed (profiles mode) | `15` |
| `postsCount` | Number of posts (profiles mode) | `865` |
| `subjectDid` | Account whose followers or follows were listed | `did:plc:z72i7hdynmk6r22z27h6tvur` |
| `subjectHandle` | Handle of that account | `bsky.app` |
| `searchQuery` | Query that matched the profile (searchProfiles mode) | `journalist` |
| `uri` | at:// URI of the post or list | `at://did:plc:.../app.bsky.feed.post/3mwolmfws5k2r` |
| `cid` | Content ID of the post | `bafyrei...` |
| `text` | Post text | `Happy opening day of hockey season` |
| `likeCount` | Likes on the post | `952` |
| `repostCount` | Reposts of the post | `135` |
| `replyCount` | Replies to the post | `70` |
| `quoteCount` | Quote posts | `16` |
| `langs` | Language codes declared on the post | `["en"]` |
| `indexedAt` | Post or list indexing time; list `createdAt` retains its existing indexing-time value | `2025-08-06T03:43:31.609Z` |
| `trustedVerifierStatus` | Account trusted verifier status, null when unavailable | `valid` |
| `images` | Post image URLs and original alternative text, including media attached to quotes | `[{"fullsize":"https://cdn.bsky.app/...","thumb":"https://cdn.bsky.app/...","alt":"A blue sky"}]` |
| `videoPlaylist` | Post video playback playlist URL | `https://video.bsky.app/.../playlist.m3u8` |
| `externalUrl` | Post link card destination URL | `https://bsky.app` |
| `quotedPostUri` | Available quoted post AT URI | `at://did:plc:.../app.bsky.feed.post/...` |
| `hasMedia` | True when the post has an embed such as an image, video, link card or quote | `true` |
| `isRepost` | True when the feed entry is a repost by the target account | `false` |
| `repostedAt` | When the account reposted it (reposts only) | `2026-09-30T13:30:00.000Z` |
| `postUrl` | bsky.app link to the post | `https://bsky.app/profile/.../post/3mwolmfws5k2r` |
| `name` | List name (lists mode) | `Hockey writers` |
| `purpose` | List purpose, curation or moderation | `app.bsky.graph.defs#curatelist` |
| `listItemCount` | Number of accounts on the list | `120` |
| `accountType` | Optional AI classification: person, brand, media, bot or other | `brand` |
| `sourceUrl` | API endpoint the row came from | `https://public.api.bsky.app/xrpc/...` |

Follower and follow rows come from list endpoints that do not include follower, follow or post counts. Use profiles mode when you need those numbers. Search rows are also list items and do not include the counts either.

### How to use Bluesky Profile Scraper

1. Open the Actor in Apify Console and pick a **Mode**.
2. Add one or more **Handles**. You can paste a handle such as `bsky.app`, a DID, or a full `bsky.app/profile/...` link. In searchProfiles mode, fill **Search query** instead.
3. Set **Maximum results**. This is the total across all handles, and it is the most important setting for followers and posts of large accounts.
4. Click Start. When the run finishes, open the Output tab and export the dataset as JSON, CSV, Excel, XML or RSS.

Some tips. The profiles mode takes up to 25 handles per API call, so checking hundreds of accounts is quick. In posts mode, the default filter returns posts and reposts but not replies. The API only exposes a limited depth of an account's feed, so a very prolific account may return fewer posts than it has published. Follower lists of very large accounts run into the tens of millions of rows, so always set a sensible limit.

### Input

For API and MCP calls, prefills are examples rather than automatic targets. Supply `handles` for account modes, or `searchQuery` with `mode: "searchProfiles"`. The default mode is profiles; `maxItems` is one shared budget across every target, not a separate allowance for each handle. Three profile targets can therefore return three rows even when `maxItems` is 10. Search ignores handles, and postFilter affects only posts.

An empty handle list returns zero rows. Unknown accounts are skipped so another supplied account can still return data. If no rows are found, the final status identifies what to change: supply existing handles or profile URLs, broaden searchQuery, try another account or mode for an empty feed, or retry later and configure Apify Proxy for persistent blocks. A missing searchQuery in searchProfiles mode produces a validation message.

| Field | Type | Required | Description |
|---|---|---|---|
| `handles` | array of strings | Yes (except searchProfiles) | Handles, DIDs, AT profile URIs or bsky.app profile URLs, e.g. `@bsky.app`, one per line. |
| `mode` | string | No | profiles, posts, followers, follows, lists or searchProfiles. Default profiles. |
| `searchQuery` | string | Only for searchProfiles | Keyword to find accounts by name, handle or bio. |
| `postFilter` | string | No | posts_no_replies, posts_with_replies or posts_with_media. Default posts_no_replies. |
| `maxItems` | integer | No | Total rows to return, 1-10000, default 50. |
| `enrichAccountType` | boolean | No | Add an AI account type classification. Default false. |
| `proxyConfiguration` | object | No | Proxy settings. Default: no proxy, because the public API is open. |

```json
{
    "mode": "followers",
    "handles": ["bsky.app"],
    "maxItems": 120,
    "enrichAccountType": false,
    "proxyConfiguration": { "useApifyProxy": false }
}
```

List records also include the creator’s `displayName` when available. Post embed fields are nullable: a text-only post has no image, video, external link, or quoted-post value. Image alternative text is copied verbatim and can be an empty string. An unavailable or blocked quoted post has no `quotedPostUri`.

### Output

Every row is a flat JSON object. Here are real rows from a run: a profile and a post.

```json
[
    {
        "id": "did:plc:oky5czdrnfjpqslsw2a5iclo",
        "recordType": "profile",
        "did": "did:plc:oky5czdrnfjpqslsw2a5iclo",
        "handle": "jay.bsky.team",
        "displayName": "Jay 🦋",
        "description": "Founder & Chief Innovation Officer @ Bluesky\n\nWorking on @attie.ai\n\n🌱 🪴 🌳",
        "createdAt": "2022-11-17T06:31:40.296Z",
        "verified": "valid",
        "sourceUrl": "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles?actors=bsky.app&actors=jay.bsky.team&actors=atproto.com"
    },
    {
        "id": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3mwolmfws5k2r",
        "recordType": "post",
        "handle": "bsky.app",
        "text": "Happy opening day of hockey season, NHL fans!",
        "createdAt": "2026-09-29T19:43:14.855Z",
        "likeCount": 952,
        "repostCount": 135,
        "replyCount": 70,
        "isRepost": false
    }
]
```

Results can be downloaded in JSON, CSV, Excel, XML or RSS from the dataset, or read through the API.

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

Bluesky Profile Scraper is charged per result returned. You pay only for the rows saved, and `maxItems` caps how many that can be. When you turn on the optional AI account classification, an additional charge applies only for rows where the classification succeeded. See the Pricing tab for the current prices.

### Use with AI agents (MCP)

Connect Bluesky Profile Scraper to Claude, Cursor or any MCP client through the Apify MCP server:

```json
{
    "mcpServers": {
        "apify": {
            "url": "https://mcp.apify.com/?tools=axiomworks/bluesky-profile-scraper"
        }
    }
}
```

Example prompts:

1. "Get the follower count and bio of the Bluesky accounts bsky.app and atproto.com."
2. "Fetch the latest 50 posts from jay.bsky.team on Bluesky with their like and repost counts."
3. "Find Bluesky accounts matching 'data journalist' and list their handles and bios."

### Integrations and API

Run Bluesky Profile Scraper from your own code with the Apify API. Set `APIFY_TOKEN` to your Apify API token (send it in the `Authorization` header, never in the URL).

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")
run = client.actor("axiomworks/bluesky-profile-scraper").call(run_input={"mode": "profiles", "handles": ["bsky.app"], "maxItems": 10})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

**JavaScript**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });
const run = await client.actor('axiomworks/bluesky-profile-scraper').call({ mode: 'posts', handles: ['bsky.app'], maxItems: 60 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**cURL**

```bash
curl -X POST "https://api.apify.com/v2/acts/axiomworks~bluesky-profile-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode": "profiles", "handles": ["bsky.app"], "maxItems": 10}'
```

The Integrations tab connects the dataset to Make, Zapier, n8n, Google Sheets and webhooks, so you can schedule runs and send results on.

### FAQ

**Do I need a Bluesky account or API key?**
No. The Actor reads the open public API and needs no login.

**Why did a run return 0 results?**
The handle may not exist, may be deactivated, or the account may have no data of the requested type. Check the spelling and try the profiles mode first. The log lists handles that were not found.

**Why are some follower rows missing counts?**
The follower, follow and search endpoints return a short profile view without counts. Run profiles mode on the handles you care about to get `followersCount`, `followsCount` and `postsCount`.

**Are reposts included in posts mode?**
Yes, with the default filter. They have `isRepost` set to true, and the `handle` and `did` fields are those of the original author, not the account you scraped.

**Can I search post text by keyword?**
No. Keyword search of posts is not available on the public API. Account search by keyword is available through searchProfiles mode.

**How does the AI account classification work?**
When enabled, the bio and display name are sent to a third-party AI processor (Jev by TypeSafe), which picks one of person, brand, media, bot or other. If the processor fails, the row is still returned without `accountType` and no extra charge applies.

**How fresh is the data?**
Every run reads live from the API, so counts and posts are current at run time.

**Can I get the data on a schedule?**
Yes. Use Apify schedules and compare runs by `id`.

### Is it legal to scrape Bluesky?

The Actor collects only publicly available data from the open Bluesky API, with no login and no private content. Profile data can be personal data, so you are responsible for using it lawfully, for example under GDPR, and for respecting the people behind the accounts. This is not legal advice. This Actor is an independent tool and is not affiliated with or endorsed by Bluesky.

### Feedback

Found a bug or need a field that isn't here? Open an issue on the Issues tab and we'll look at it.

# Actor input Schema

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

Accounts to read: handles (bsky.app or @jay.bsky.team), DIDs (did:plc:z72i7hdynmk6r22z27h6tvur), AT URIs (at://did:plc:z72i7hdynmk6r22z27h6tvur), or profile URLs (https://bsky.app/profile/bsky.app). Used except in searchProfiles, which ignores handles. Omitted or empty handles return 0 rows; unknown accounts are skipped. Prefill supplies three examples only in the editor; API calls must supply targets.

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

Choose profiles, posts, followers, follows, lists or searchProfiles, e.g. "posts". Default profiles returns one detailed profile per supplied account. Other account modes use handles; searchProfiles uses searchQuery and ignores handles. maxItems is shared across all targets.

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

Keywords to find accounts by name, handle or bio, e.g. "journalist" or "日本". Required only with mode="searchProfiles"; ignored in other modes. Supply 1-200 characters. No default search runs automatically; no matches return 0 rows.

## `postFilter` (type: `string`):

Choose posts_no_replies, posts_with_replies or posts_with_media, e.g. "posts_with_media". Used only with mode="posts". Default posts_no_replies includes original posts and reposts but excludes replies; post authors can differ from handles when isRepost=true.

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

Maximum rows saved across all handles or search pages, e.g. 7. Accepts 1-10000; default 50. Profiles return at most one row per unique account, so three handles may return only three rows. Stops at this shared limit or the run spending limit; only saved rows are charged.

## `enrichAccountType` (type: `boolean`):

Set true to classify profile, follower, follow and search rows as person, brand, media, bot or other, e.g. true. Default false leaves accountType unset. Jev by TypeSafe processes the name and bio; an extra event is charged only on successful classification. Ignored for posts and lists.

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

Proxy settings for Bluesky API requests, e.g. {"useApifyProxy":false}. Default disables the proxy. Set {"useApifyProxy":true} on Apify if requests are blocked; retries use fresh sessions. Persistently blocked targets return 0 rows with advice to retry or change proxy settings.

## Actor input object example

```json
{
  "handles": [
    "bsky.app",
    "jay.bsky.team",
    "atproto.com"
  ],
  "mode": "profiles",
  "searchQuery": "",
  "postFilter": "posts_no_replies",
  "maxItems": 10,
  "enrichAccountType": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset with one item per result, in the order found.

# 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 = {
    "handles": [
        "bsky.app",
        "jay.bsky.team",
        "atproto.com"
    ],
    "mode": "profiles",
    "maxItems": 10,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("axiomworks/bluesky-profile-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 = {
    "handles": [
        "bsky.app",
        "jay.bsky.team",
        "atproto.com",
    ],
    "mode": "profiles",
    "maxItems": 10,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("axiomworks/bluesky-profile-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 '{
  "handles": [
    "bsky.app",
    "jay.bsky.team",
    "atproto.com"
  ],
  "mode": "profiles",
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call axiomworks/bluesky-profile-scraper --silent --output-dataset

```

## MCP server setup

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