# Bluesky Scraper: Posts, Profiles, Followers and Feeds (`angadseth/bluesky-scraper`) Actor

Scrape Bluesky without a login: user posts, keyword search, profiles, followers and follows, full reply threads, likers and reposters, and custom or list feeds. Clean typed records with engagement counts, hashtags, mentions, links and image alt text, from the official public AT Protocol API.

- **URL**: https://apify.com/angadseth/bluesky-scraper.md
- **Developed by:** [Angad Jangir](https://apify.com/angadseth) (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

$1.00 / 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.

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: posts, profiles, followers, threads, likers and feeds

Scrape **Bluesky** without a login or an account. Get a user's posts, search posts by keyword or hashtag, read profiles, followers and follows, download a full reply thread, list who liked or reposted a post, and read any custom feed or list. Every record comes back in one clean, typed shape with engagement counts, hashtags, mentions, links, image URLs with alt text, reply and quote links, and ISO dates.

The data comes from Bluesky's own **public AT Protocol API** (the AppView at `public.api.bsky.app`), the same open API that Bluesky apps use, so it is complete, fast and stable. No cookies, no browser, no app password.

### Who uses it

- **Social listening and brand monitoring**: track mentions of a brand, product or hashtag on Bluesky every hour.
- **Researchers and journalists**: collect posts, threads and follower graphs for studies of public discourse.
- **Marketers and growth teams**: find influencers by topic, read their follower counts, and see who engages with competitors' posts.
- **Data and AI teams**: build training and evaluation sets from public posts with language tags and image alt text.
- **AI agents**: call the Actor as a tool through the Apify MCP server ("what is bsky.app posting this week?").

### Modes

| Mode | Input | What you get |
|---|---|---|
| **User posts** | handles, DIDs or profile URLs | The user's posts, newest first. Options: replies, reposts, pinned post, only media, date range, keyword filter. The user's follower counts are added to every post for free. |
| **Search posts** | search terms | Posts matching each term (Bluesky search syntax: `#tag`, `from:handle`, `"exact phrase"`, `domain:`, `mentions:`), newest or top, optional language and date range. |
| **Profiles** | handles, DIDs or profile URLs | Full profile: bio, follower, following and post counts, avatar, banner, labels, verification, pinned post. |
| **Search profiles** | search terms | Accounts matching a name or bio keyword, with full counts. |
| **Followers** / **Follows** | handles, DIDs or profile URLs | Everyone who follows the user, or everyone the user follows, with full profile counts. |
| **Post thread** | post URLs | The post, its parents up to the root, and the whole reply tree, flattened. `threadDepth` is 0 for the post, 1 for direct replies, 2 for replies to those, and negative for parents. |
| **Post engagement** | post URLs | Accounts that **liked** the post (with the time of the like), accounts that **reposted** it, and posts that **quote** it. |
| **Custom feed** | feed URLs, list URLs or `at://` URIs | Posts from any custom feed (for example Discover) or any list feed. |

### Why this one

- **9 modes in one Actor**, including threads, likers, reposters, quotes, follows and list feeds.
- **Typed, flat records**: counts are numbers, dates are ISO, missing values are `null`, and every post has the same columns, so CSV and Excel exports just work.
- **Rich post fields**: hashtags, mentions (handle and DID), every link, images (full size, thumbnail, alt text, size), video playlist, external link card, quoted post, reply parent and thread root.
- **Reliable**: official API only, polite pacing, automatic back-off on HTTP 429 and server errors, cursor pagination, and a per-input SUMMARY so you see exactly what each input returned.
- **Fair limits**: **Max records** is shared evenly across your inputs, so ten users get ten fair slices instead of the first user taking everything. Share that an input cannot use (an account with few posts, a bad handle) goes to the inputs that have more.
- **Respects user choices** (see below).

### Accounts that opted out are skipped

Bluesky posts are public by the protocol's design, but every user can turn on **"Discourage apps from showing my account to logged-out users"**. That sets the `!no-unauthenticated` label on their account. This Actor honours it everywhere: profiles and posts from accounts with this label are **never saved** (in search results, feeds, threads, likers, followers, everything). The SUMMARY record counts how many were skipped (`skippedOptOut`). You are not charged for skipped records.

### Example inputs

Latest 100 posts from two accounts, with replies, from the last 30 days:

```json
{
  "mode": "userPosts",
  "users": ["bsky.app", "https://bsky.app/profile/nytimes.com"],
  "includeReplies": true,
  "since": "30 days",
  "maxItems": 100
}
```

Search two hashtags in English, top results:

```json
{
  "mode": "searchPosts",
  "searchQueries": ["#ai", "#climate"],
  "searchSort": "top",
  "searchLanguage": "en",
  "maxItems": 200
}
```

Full reply tree of a post:

```json
{
  "mode": "postThread",
  "postUrls": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"],
  "maxItems": 1000
}
```

Who liked and reposted a post, with follower counts:

```json
{
  "mode": "postEngagement",
  "postUrls": ["https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l"],
  "engagementTypes": ["likes", "reposts"],
  "maxItems": 500
}
```

First 1,000 followers of an account:

```json
{ "mode": "followers", "users": ["jay.bsky.team"], "maxItems": 1000 }
```

Posts about "election" from the Discover feed and a list:

```json
{
  "mode": "customFeed",
  "feeds": ["https://bsky.app/profile/bsky.app/feed/whats-hot", "https://bsky.app/profile/bsky.app/lists/3lvpca43j5z26"],
  "keywords": ["election"],
  "maxItems": 100
}
```

### Output

Each record has `recordType` `"post"` or `"profile"`. A real post record (search for `#astronomy`, top):

```json
{
  "recordType": "post",
  "uri": "at://did:plc:btzjjptgouva43ko4ip5pl67/app.bsky.feed.post/3mzkg6k7w5kyn",
  "cid": "bafyreicmjujpigfficrt5jcscyikg2nyo3hkrlnqxotc5s7fdkjn4zpome",
  "rkey": "3mzkg6k7w5kyn",
  "url": "https://bsky.app/profile/apod.shinyakato.dev/post/3mzkg6k7w5kyn",
  "text": "🔭 Enceladus in Silhouette\n\nCredit: Cassini Imaging Team, SSI, JPL, ESA, NASA\n\nap261105",
  "createdAt": "2026-11-05T08:10:56.806Z",
  "indexedAt": "2026-11-05T08:10:58.381Z",
  "authorDid": "did:plc:btzjjptgouva43ko4ip5pl67",
  "authorHandle": "apod.shinyakato.dev",
  "authorDisplayName": "Astronomy Picture of the Day 🪐",
  "authorAvatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:btzjjptgouva43ko4ip5pl67/bafkreidjotrof5t65mz3ba7ayzfsqgjri76cyvbzh2iowspte6ppcej3pa",
  "authorFollowersCount": null,
  "authorFollowsCount": null,
  "authorPostsCount": null,
  "authorVerified": false,
  "authorLabels": [],
  "replyCount": 0,
  "repostCount": 27,
  "likeCount": 73,
  "quoteCount": 0,
  "bookmarkCount": 5,
  "langs": [],
  "hashtags": ["nasa", "apod", "science", "astronomy", "APOD-261105"],
  "mentions": [],
  "links": ["https://ciclops.org/", "https://www.spacescience.org/", "https://www.jpl.nasa.gov/", "https://www.esa.int/", "https://www.nasa.gov/", "https://science.nasa.gov/apod/?date=261105"],
  "images": [
    {
      "url": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:btzjjptgouva43ko4ip5pl67/bafkreigreamyuydczuxsppvv2phkz3iprae7rntc6n4444asx6rhkjo6ii",
      "thumb": "https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:btzjjptgouva43ko4ip5pl67/bafkreigreamyuydczuxsppvv2phkz3iprae7rntc6n4444asx6rhkjo6ii",
      "alt": "Saturn moon Enceladus in silhouette",
      "width": null,
      "height": null
    }
  ],
  "video": null,
  "externalLink": null,
  "embedType": "images",
  "quotedPostUri": null,
  "quotedPostUrl": null,
  "quotedPostText": null,
  "quotedPostAuthorHandle": null,
  "isReply": false,
  "replyTo": null,
  "replyToUrl": null,
  "rootUri": null,
  "isRepost": false,
  "repostedByHandle": null,
  "repostedAt": null,
  "isPinned": false,
  "labels": [],
  "threadDepth": null,
  "source": "searchPosts",
  "input": "#astronomy",
  "scrapedAt": "2026-11-05T15:49:14.920Z"
}
```

A real profile record (a liker from Post engagement):

```json
{
  "recordType": "profile",
  "did": "did:plc:rhqbtzvxl3yt7dbhgy77jqws",
  "handle": "nb1shadowfan.bsky.social",
  "url": "https://bsky.app/profile/nb1shadowfan.bsky.social",
  "displayName": "Ash",
  "description": null,
  "pronouns": null,
  "website": null,
  "followersCount": 7,
  "followsCount": 46,
  "postsCount": 0,
  "createdAt": "2025-01-30T19:49:41.344Z",
  "indexedAt": "2026-03-05T18:53:53.551Z",
  "avatar": "https://cdn.bsky.app/img/avatar/plain/did:plc:rhqbtzvxl3yt7dbhgy77jqws/bafkreid6eh4azz7h6u7gufgwke6rusnddrrgdtu7xj2johgeff6jsw66zy",
  "banner": "https://cdn.bsky.app/img/banner/plain/did:plc:rhqbtzvxl3yt7dbhgy77jqws/bafkreih5bbni6e5nxt7iworirqkibp2sautq7ojfea47kzeaynrcc7fe6u",
  "labels": [],
  "isVerified": false,
  "isLabeler": false,
  "listsCount": 0,
  "feedsCount": 0,
  "starterPacksCount": 0,
  "pinnedPostUri": null,
  "relation": "like",
  "relatedTo": "at://did:plc:qjeavhlw222ppsre4rscd3n2/app.bsky.feed.post/3mzj2fyriss2n",
  "relatedAt": "2026-11-05T15:33:16.452Z",
  "source": "postEngagement:likes",
  "input": "https://bsky.app/profile/rose.bsky.team/post/3mzj2fyriss2n",
  "scrapedAt": "2026-11-05T15:46:40.484Z"
}
```

| Field | What it is |
|---|---|
| `uri`, `cid`, `rkey`, `url` | AT Protocol identifiers and the bsky.app link |
| `text`, `langs`, `createdAt`, `indexedAt` | Post text, language tags set by the author, ISO times |
| `author*` | DID, handle, display name, avatar, verification, labels; follower, following and post counts in User posts mode (always) or with **Add author follower counts** (any mode) |
| `replyCount`, `repostCount`, `likeCount`, `quoteCount`, `bookmarkCount` | Engagement counts at scrape time |
| `hashtags`, `mentions`, `links` | From the post's rich-text facets and tags; links also include the link-card URL |
| `images`, `video`, `externalLink`, `embedType` | Media and embeds (`images`, `video`, `external`, `quote`, `quoteWithMedia`) |
| `quotedPost*` | The quoted post's URI, URL, text and author |
| `isReply`, `replyTo`, `replyToUrl`, `rootUri` | Reply parent and thread root |
| `isRepost`, `repostedByHandle`, `repostedAt`, `isPinned` | Feed context in User posts and feeds |
| `threadDepth` | Position in a Post thread |
| Profile `relation`, `relatedTo`, `relatedAt` | `follower`, `follow`, `like`, `repost`, `search` or `profile`; the account or post it relates to; the time of the like |

The run's **SUMMARY** record (key-value store) lists every input with records saved, items scanned, accounts skipped for opting out, and a clear error for any input that failed (for example a handle that does not exist).

### Pricing

You pay per record saved to the dataset. Records removed by your filters and accounts skipped for opting out cost nothing. **Max records** is a hard cap, so you always know the most a run can cost.

### Limits

- **Search returns up to 100 posts per search term.** Bluesky serves only the first page of search results to logged-out clients and refuses further pages without a login. This Actor never logs in, so for more posts add more specific terms (per hashtag, per language, `from:` a user, or narrower date ranges with **Posted since/until**). If Bluesky ever closes logged-out search completely, the Actor automatically falls back to matching your term against the public Discover feed and says so in the run log and SUMMARY.
- Custom feeds are ranked by their creator, not by date, so **Posted since** filters them but cannot stop paging early. A feed is read until Max records or a safety cap of scanned posts.
- **Very large threads are capped by Bluesky.** For a post with thousands of replies, the public API serves a few hundred of them (for example about 760 posts for a post with 8,700 replies), always including the reply chains below each reply it serves. Threads of normal size come back complete.
- Counts are a snapshot at scrape time.
- Deleted posts, blocked threads and accounts that opted out of logged-out viewing are not returned.

### Schedules, webhooks and integrations

- **Monitoring**: save your input as a Task and add a Schedule (for example every hour, with **Posted since** `2 hours`) to track a hashtag, a brand or a set of accounts.
- **Google Sheets, Slack, email, Zapier, Make, n8n**: connect them in the Integrations tab to send each run's records where you need them.
- **Webhooks**: get a call when a run finishes and fetch the dataset from the API.
- **MCP / AI agents**: the Actor works as a tool in the Apify MCP server, so an agent can read posts, threads and profiles as structured data.

### FAQ

**Is this legal and clean?** The Actor reads only public data from Bluesky's official public API, which Bluesky documents for unauthenticated use; its robots.txt says "Crawling the public parts of the API is allowed". It does not log in, use anyone's account or app password, solve captchas or get around any protection. It paces requests well under Bluesky's published rate limits and backs off when asked (HTTP 429). It honours each user's `!no-unauthenticated` choice. You are responsible for how you use the data, including privacy laws such as GDPR when you store personal data.

**Do I need a Bluesky account or app password?** No.

**What can I paste as a user?** A handle (`bsky.app`, `@jay.bsky.team`), a custom-domain handle (`nytimes.com`), a DID (`did:plc:...`) or any bsky.app profile or post URL. A bare name like `alice` is read as `alice.bsky.social`.

**Why did a user return nothing?** The handle may not exist (see SUMMARY), the account may have opted out of logged-out viewing, or your date and keyword filters removed everything.

**Why are there fewer search results than Max records?** See Limits: logged-out search gives up to 100 posts per term. Add more terms.

**Something broke or a field is missing?** Open an issue on the Actor's Issues tab and it will be fixed quickly.

### More Actors by angadseth

- [GeM Tenders Scraper (India Government e-Marketplace Bids)](https://apify.com/angadseth/gem-tenders-scraper): Get live GeM bids and reverse auctions from India's Government e-Marketplace
- [India eProcurement Tenders Scraper (CPPP + 30 State Portals)](https://apify.com/angadseth/india-eprocurement-tenders): Live government tenders from India's central eProcure (CPPP), central PSU, defence and 30+ state NIC eProcurement portals in one run.
- [Apple App Store Scraper: apps, ratings and reviews](https://apify.com/angadseth/app-store-scraper): Get Apple App Store apps, ratings and customer reviews from many countries in one run
- [ATS Jobs Feed: Greenhouse, Lever, Ashby, Workday + 5 more](https://apify.com/angadseth/ats-jobs-feed): Get every open job from a company's own careers board
- [Website Contact Enricher: Emails, Phones, Socials per Domain](https://apify.com/angadseth/website-contact-enricher): One clean lead row per company domain
- [Website Screenshot & PDF: full page, mobile, element, WebP](https://apify.com/angadseth/website-screenshot): Screenshots (PNG, JPEG, WebP) and PDFs of any list of URLs.
- [Remote Jobs Aggregator: RemoteOK, Jobicy, Arbeitnow + 2 more](https://apify.com/angadseth/remote-jobs-aggregator): Remote jobs from RemoteOK, Jobicy, Arbeitnow, Working Nomads and 4 Day Week in one clean feed.
- [PDF & Document Text Extractor: PDF, DOCX, PPTX, XLSX to Text](https://apify.com/angadseth/pdf-text-extractor): Turn public document URLs (PDF, DOCX, PPTX, XLSX, CSV, HTML, TXT) into clean text

# Actor input Schema

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

User posts: the posts of the users below. Search posts: posts matching the search terms. Profiles: profile details of the users below. Search profiles: accounts matching the search terms. Followers / Follows: who follows the users below, or who they follow. Post thread: a post with its parents and its full reply tree, flattened. Post engagement: who liked or reposted a post (and its quote posts). Custom feed: posts from a feed or list.

## `users` (type: `array`):

Handles, DIDs or profile URLs, one per line, e.g. bsky.app, @jay.bsky.team, did:plc:z72i7hdynmk6r22z27h6tvur or https://bsky.app/profile/nytimes.com. A bare name like "alice" means alice.bsky.social.

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

One search per line. Post search supports Bluesky's search syntax, e.g. "climate", "#ai", "from:nytimes.com", ""exact phrase"", "domain:github.com", "mentions:bsky.app". Logged-out search returns up to 100 posts per term.

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

Post links like https://bsky.app/profile/bsky.app/post/3l6oveex3ii2l, or at:// URIs, one per line.

## `feeds` (type: `array`):

Custom feed links (https://bsky.app/profile/<creator>/feed/<id>), list links (https://bsky.app/profile/<creator>/lists/<id>) or at:// URIs, one per line. "discover" is a shortcut for Bluesky's Discover feed.

## `includeReplies` (type: `boolean`):

User posts and feeds: also keep posts that are replies to someone. Off = top-level posts only. Search always keeps replies (isReply marks them).

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

User posts and feeds: also keep posts the user reposted (marked isRepost, with repostedAt).

## `includePinned` (type: `boolean`):

User posts: keep the user's pinned post (marked isPinned) even if it is old. Date filters still apply.

## `onlyWithMedia` (type: `boolean`):

User posts and feeds: keep only posts that have images or a video.

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

Keep posts created on or after this date. A date like 2026-10-01, an ISO time, or a relative time like "7 days" or "24 hours". For user posts the Actor stops paging once it reaches older posts.

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

Keep posts created on or before this date (same formats as Posted since).

## `keywords` (type: `array`):

User posts, feeds and search: keep only posts whose text contains any of these words. Case does not matter. Leave empty for all posts.

## `searchSort` (type: `string`):

Search posts: newest first, or Bluesky's top (most engaging) results.

## `searchLanguage` (type: `string`):

Search posts: only posts in this language, as a two-letter code like en, ja, pt or de. Leave empty for all languages.

## `engagementTypes` (type: `array`):

Post engagement: which lists to read. likes = accounts that liked the post, reposts = accounts that reposted it, quotes = posts that quote it. Default: likes and reposts.

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

Post thread: also save the posts above the given post, up to the thread root (threadDepth below 0).

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

Post thread: how many levels of replies to read below the post (1 = direct replies only, up to 1000).

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

Followers, follows, likers, reposters and profile search: add follower, following and post counts and the banner (one extra request per 25 accounts).

## `authorDetails` (type: `boolean`):

Fill authorFollowersCount, authorFollowsCount and authorPostsCount on every post (one extra request per 25 new authors). In user posts mode the user's own counts are always filled for free.

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

Stop after this many records in total. It is shared fairly across your inputs. You pay per record saved.

## `maxItemsPerInput` (type: `integer`):

Optional cap for each user, search term, post or feed. 0 = share Max records evenly; share an input cannot use goes to the inputs that have more.

## Actor input object example

```json
{
  "mode": "userPosts",
  "users": [
    "bsky.app"
  ],
  "includeReplies": false,
  "includeReposts": false,
  "includePinned": true,
  "onlyWithMedia": false,
  "searchSort": "latest",
  "engagementTypes": [
    "likes",
    "reposts"
  ],
  "includeParents": true,
  "threadDepth": 1000,
  "fullProfiles": true,
  "authorDetails": false,
  "maxItems": 20,
  "maxItemsPerInput": 0
}
```

# Actor output Schema

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

All post and profile records from this run, as a dataset you can download in JSON, CSV or Excel.

## `summary` (type: `string`):

Records saved per input, accounts skipped because they opted out of logged-out viewing, and any warnings.

# 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": "userPosts",
    "users": [
        "bsky.app"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("angadseth/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 = {
    "mode": "userPosts",
    "users": ["bsky.app"],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("angadseth/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 '{
  "mode": "userPosts",
  "users": [
    "bsky.app"
  ],
  "maxItems": 20
}' |
apify call angadseth/bluesky-scraper --silent --output-dataset

```

## MCP server setup

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