# Buttondown Newsletter Scraper (`maximedupre/buttondown`) Actor

Read public Buttondown newsletter archives, posts, and profiles into structured dataset rows. Filter posts by date or keyword, fetch a public post by URL, and add HTML and readable content when available.

- **URL**: https://apify.com/maximedupre/buttondown.md
- **Developed by:** [Maxime Dupré](https://apify.com/maximedupre) (community)
- **Categories:** News, Social media, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.70 / 1,000 newsletter 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

### 📰 Turn Buttondown newsletters into usable rows

Researchers, newsletter teams, and developers can read one public Buttondown newsletter per run and get structured `post` or `newsletterProfile` rows in an Apify dataset. Use archive filters, a post URL, or a newsletter slug or URL to bring public content, dates, metadata, and source links into a dataset you can inspect or export.

- Read a public newsletter profile and its links with **[Buttondown Newsletter Profile](https://apify.com/maximedupre/buttondown/examples/buttondown-newsletter-profile)**.
- Fetch one public post from its URL with **[Buttondown Post Scraper](https://apify.com/maximedupre/buttondown/examples/buttondown-post-scraper)**.
- Collect public archive posts from one newsletter with **[Buttondown Newsletter Archive](https://apify.com/maximedupre/buttondown/examples/buttondown-newsletter-archive)**.
- Review post dates and metadata with **[Buttondown Newsletter Scraper](https://apify.com/maximedupre/buttondown/examples/buttondown-newsletter-scraper)**.
- Save public source data as dataset rows with **[Buttondown Scraper](https://apify.com/maximedupre/buttondown/examples/buttondown-scraper)**.

#### 🧾 Buttondown data for research

**Post rows**

Post rows include the public title, summary, publication time, language, and source URL. When the page provides them, rows also include source HTML, readable content, update time, access status, author, section, and social-card data.

**Newsletter profile rows**

Profile rows include the public newsletter name, description, language, icon, website, archive URL, and configured social links when available.

The Actor reads public Buttondown pages for one newsletter or one public post at a time. It does not send or edit newsletters, manage subscriptions, access private content, or combine several independent newsletter searches in one run.

#### ▶️ Run a public Buttondown lookup

Choose one result type per run:

- `archivePosts` reads public posts from the newsletter archive.
- `postByUrl` reads one public post from `postUrl`.
- `newsletterProfile` reads public newsletter details from `newsletter`.

For archive posts, add date or keyword filters and an optional limit. Leaving the limit empty returns all available archive posts until the source is exhausted. Only content the source exposes publicly is included. Private, subscriber-only, and paid-only content that is not public is outside the Actor's scope.

#### ⚙️ Input

**Input fields**

| Field | Type | What it does |
| --- | --- | --- |
| `resultType` | string | Required. Selects `archivePosts`, `postByUrl`, or `newsletterProfile`. |
| `startDate` | date string | For archive posts, keeps posts published on or after this UTC date. |
| `endDate` | date string | For archive posts, keeps posts published on or before this UTC date. |
| `keyword` | string | For archive posts, keeps posts whose title or summary contains this keyword without case sensitivity. |
| `maxItems` | integer | For archive posts, stops after this many posts. Leaving it empty returns all available archive posts until the source is exhausted. |
| `postUrl` | URL string | The full public Buttondown post URL used with `postByUrl`. |
| `newsletter` | string | The Buttondown newsletter slug or full public newsletter URL used with `archivePosts` or `newsletterProfile`. Leave it empty for `postByUrl`. |

**Successful default input**

This example is copied from the public input of a successful current-beta default-input run:

```json
{
  "resultType": "archivePosts",
  "maxItems": 17,
  "newsletter": "buttondown"
}
```

#### 🧾 Output

**Output link**

| Field | Type | What it does |
| --- | --- | --- |
| `dataset` | URL | Opens the Apify dataset with the post and newsletter profile rows. |

Every dataset row has a `resultType` value. Optional fields can be absent when the public source does not provide them.

**Post rows**

| Field | Type | What it does |
| --- | --- | --- |
| `resultType` | string | Always `post` for this shape. |
| `title` | string | The title published for the post. |
| `summary` | string | The summary published for the post. |
| `publishedAt` | date-time string | The date and time when the post was published. |
| `language` | string | The language reported for the post. |
| `url` | URL | The public URL of the post. |
| `bodyHtml` | string | The post body in the source HTML format, when available. |
| `readableContent` | string | A readable version of the post content, when available. |
| `updatedAt` | date-time string | The date and time when the post was last updated, when reported. |
| `accessStatus` | string | The reported access status, `free` or `paid`, when available. |
| `author` | string | The author name reported for the post, when available. |
| `section` | string | The section reported for the post, when available. |
| `socialCard` | object | Social-card information reported for the post, when available. |
| `socialCard.title` | string | The title used by the post's social card, when available. |
| `socialCard.description` | string | The description used by the post's social card, when available. |
| `socialCard.imageUrl` | URL | The image URL used by the post's social card, when available. |

**Genuine post row**

This complete row came from the successful current-beta default-input run. It is not shortened.

```json
{
  "resultType": "post",
  "title": "commenting_mode replaces is_comments_disabled",
  "summary": "Emails get a three-valued commenting mode instead of a boolean",
  "publishedAt": "2024-12-30T00:00:00.000Z",
  "language": "en-US",
  "url": "https://buttondown.com/blog/api-commenting-mode",
  "bodyHtml": "<p><strong>Breaking change</strong>: we've unshipped the <code>is_comments_disabled</code> field from <a href=\"https://docs.buttondown.com/api-emails-introduction\">emails</a>, and replaced it with a more flexible <a href=\"https://docs.buttondown.com/api-emails-commenting-mode\"><code>commenting_mode</code></a> field.</p><p>The <code>commenting_mode</code> field is now a string that can be one of the following values:</p><ul><li><code>\"enabled\"</code>: comments are enabled</li><li><code>\"disabled\"</code>: comments are disabled</li><li><code>\"enabled_for_paid_subscribers\"</code>: comments are enabled, but only for paid subscribers</li></ul>",
  "readableContent": "Breaking change: we've unshipped the is_comments_disabled field from emails, and replaced it with a more flexible commenting_mode field. The commenting_mode field is now a string that can be one of the following values: \"enabled\": comments are enabled \"disabled\": comments are disabled \"enabled_for_paid_subscribers\": comments are enabled, but only for paid subscribers",
  "updatedAt": "2024-12-30T00:00:00.000Z",
  "author": "Buttondown Team",
  "section": "Technology",
  "socialCard": {
    "title": "commenting_mode replaces is_comments_disabled",
    "description": "Emails get a three-valued commenting mode instead of a boolean",
    "imageUrl": "https://marketing.buttondown.com/og/generic?title=commenting_mode+replaces+is_comments_disabled&date=December+30%2C+2024&category=changelog"
  }
}
```

**Newsletter profile rows**

| Field | Type | What it does |
| --- | --- | --- |
| `resultType` | string | Always `newsletterProfile` for this shape. |
| `name` | string | The public name of the newsletter. |
| `description` | string | The public newsletter description, when available. |
| `language` | string | The language reported for the newsletter. |
| `iconUrl` | URL | The public URL of the newsletter icon, when available. |
| `websiteUrl` | URL | The public website URL of the newsletter. |
| `archiveUrl` | URL | The public archive URL of the newsletter. |
| `socialLinks` | array of objects | Configured public social links for the newsletter, when available. |
| `socialLinks[].platform` | string | The social platform named by the newsletter. |
| `socialLinks[].url` | URL | The public URL for the social account. |

**Genuine newsletter profile row**

This complete row came from a successful current-beta newsletter profile run. It is not shortened.

```json
{
  "resultType": "newsletterProfile",
  "name": "Buttondown",
  "description": "Monthly updates about new features, updates, and exciting stuff from the folks who run Buttondown. No spam, no ads, no nonsense. Well, maybe some nonsense. (But fun nonsense, like corgi photos.)",
  "language": "en",
  "iconUrl": "https://assets.buttondown.email/icons/ef795794-c837-4b31-99a9-97e0fd90bd6d.png",
  "websiteUrl": "https://buttondown.com/buttondown",
  "archiveUrl": "https://buttondown.com/buttondown/archive",
  "socialLinks": [
    {
      "platform": "github",
      "url": "https://github.com/buttondown"
    },
    {
      "platform": "bsky",
      "url": "https://bsky.app/profile/buttondown.com"
    },
    {
      "platform": "threads",
      "url": "https://threads.com/@buttondownemail"
    },
    {
      "platform": "x",
      "url": "https://x.com/buttondown"
    },
    {
      "platform": "mastodon",
      "url": "https://mastodon.social/@buttondown"
    },
    {
      "platform": "facebook",
      "url": "https://facebook.com/buttondown.email"
    },
    {
      "platform": "linkedin",
      "url": "https://www.linkedin.com/company/buttondown"
    }
  ]
}
```

#### 💳 Pricing

Pricing is pay per event. A newsletter post charge covers one saved public newsletter post with available metadata and its source link. A newsletter profile charge covers one saved public newsletter profile with available identity and public-link information. Current tier prices are shown on the Store page.

#### 🔌 Integrations

**Dataset access**

Open the dataset in Apify Console, read it through the Apify API, or export the rows in the formats supported by Apify.

https://www.youtube.com/watch?v=bNACk1\_S\_6w\&list=PLObrtcm1Kw6MUrlLNDbK9QRg8VDJg0gOW\&index=4

#### ❓ FAQ

##### Can I fetch one public post by URL?

Yes. Set `resultType` to `postByUrl` and enter one full public Buttondown post URL in `postUrl`.

##### Can I enter a newsletter slug or a full URL?

Yes. For `archivePosts` or `newsletterProfile`, enter the Buttondown slug or full public newsletter URL in `newsletter`.

##### Can I read the newsletter profile instead of posts?

Yes. Set `resultType` to `newsletterProfile`. The profile row can include the name, description, language, icon, website, archive URL, and configured social links.

##### Can I get source HTML and readable content?

Post rows include `bodyHtml` and `readableContent` when the public page provides them. The HTML preserves the source body, while readable content is a plain version when available.

##### How do the date and keyword filters work?

`startDate` keeps archive posts published on or after its UTC date, and `endDate` keeps posts published on or before its UTC date. `keyword` keeps archive posts whose title or summary contains the text without case sensitivity.

##### What does leaving Maximum posts empty do?

It returns all available archive posts until the source is exhausted. A nonempty value stops after that many posts, but the source may have fewer matching posts.

##### Does this include private or paid-only content?

The Actor reads only content the source exposes publicly. It does not bypass private, subscriber-only, or paid-only pages that are not public. A post can include the source's `accessStatus` when that value is reported.

##### Can I combine several newsletter searches in one run?

No. Use one newsletter archive or profile target per run, or one post URL for `postByUrl`. Start another run for a different target.

### 📝 Changelog

**v0.0** (21-09-2026)

- Initial release.

### 🆘 Support

For issues, questions, or feature requests, [file a ticket](https://console.apify.com/actors/maximedupre~buttondown/issues) and I'll fix or implement it in less than 24h 🫡

### 🔗 Related Actors

- [Substack API Scraper: Posts, Authors & Newsletters](https://apify.com/maximedupre/substack-api) reads public Substack posts, publications, and comments for cross-platform newsletter research.
- [RSS Feed Reader](https://apify.com/maximedupre/rss-feed-reader) parses public RSS, Atom, and JSON Feed URLs into feed-item rows for newsletter and blog feeds.
- [Substack Notes Scraper](https://apify.com/maximedupre/substack-notes) collects public Substack Notes for topic and author context around newsletter research.
- [Substack Recommendations](https://apify.com/maximedupre/substack-recommendations) maps public recommendation links between newsletters.
- [Medium Articles Scraper](https://apify.com/maximedupre/medium-articles) collects public Medium article metadata and optional text when research extends beyond Buttondown.

**Made with ❤️ by Maxime Dupré**

# Actor input Schema

## `resultType` (type: `string`):

Choose the kind of public Buttondown data to retrieve.

## `startDate` (type: `string`):

Optional. Keep archive posts published on or after this UTC date.

## `endDate` (type: `string`):

Optional. Keep archive posts published on or before this UTC date.

## `keyword` (type: `string`):

Optional. Keep archive posts whose title or summary contains this keyword, without case sensitivity.

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

Optional. Stop after this many archive posts. If empty, retrieve all available archive posts until the source is exhausted.

## `postUrl` (type: `string`):

Enter one full public Buttondown post URL. This field is used only for Post by URL.

## `newsletter` (type: `string`):

For Archive posts or Newsletter profile, enter the Buttondown newsletter slug or its full public newsletter URL. Leave this empty for Post by URL.

## Actor input object example

```json
{
  "resultType": "archivePosts",
  "startDate": "2025-01-01",
  "endDate": "2025-12-31",
  "keyword": "product updates",
  "maxItems": 17,
  "postUrl": "https://buttondown.com/example/archive/sample-post",
  "newsletter": "https://buttondown.com/example"
}
```

# Actor output Schema

## `dataset` (type: `string`):

Open the post and newsletter results.

# 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 = {
    "resultType": "archivePosts",
    "maxItems": 17,
    "newsletter": "buttondown"
};

// Run the Actor and wait for it to finish
const run = await client.actor("maximedupre/buttondown").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 = {
    "resultType": "archivePosts",
    "maxItems": 17,
    "newsletter": "buttondown",
}

# Run the Actor and wait for it to finish
run = client.actor("maximedupre/buttondown").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 '{
  "resultType": "archivePosts",
  "maxItems": 17,
  "newsletter": "buttondown"
}' |
apify call maximedupre/buttondown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,maximedupre/buttondown"
        }
    }
}
```

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/rniKbFaWwXYQ5GOA7/builds/NpWJZ4brDX4BVrYt7/openapi.json
