> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.beehiiv.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.beehiiv.com/_mcp/server.

# List posts <Badge intent="info" minimal outlined>OAuth Scope: posts:read</Badge>

GET https://api.beehiiv.com/v2/publications/{publicationId}/posts

Retrieve all posts belonging to a specific publication

Reference: https://developers.beehiiv.com/api-reference/posts/index

## Authentication

- `Authorization` header (bearer token, required) — Bearer authentication of the form `Bearer <token>`, where token is your auth token.

## Request

### Path parameters

- `publicationId` (string, required) — The prefixed ID of the publication object

### Query parameters

- `expand` (list of enum, optional) — Optionally expand the results by adding additional information. `stats` - Adds statistics about the post(s). `free_web_content` - Adds the web HTML rendered to a free reader. `free_email_content` - Adds the email HTML rendered to a free reader. `free_rss_content` - Adds the RSS feed HTML. `premium_web_content` - Adds the web HTML rendered to a premium reader. `premium_email_content` - Adds the email HTML rendered to a premium reader.
  - Allowed values: `stats`, `free_web_content`, `free_email_content`, `free_rss_content`, `premium_web_content`, `premium_email_content`, `recipients`
- `audience` (enum, optional, default: all) — Optionally filter the results by audience
  - Allowed values: `free`, `premium`, `all`
- `platform` (enum, optional, default: all) — Optionally filter the results by platform.`web` - Posts only published to web.`email` - Posts only published to email.`both` - Posts published to email and web.`all` - Does not restrict results by platform.
  - Allowed values: `web`, `email`, `both`, `all`
- `status` (enum, optional, default: all) — Optionally filter the results by the status of the post.`draft` - not been scheduled.`confirmed` - The post will be active after the `scheduled_at`.`archived` - The post is no longer active.`all` - Does not restrict results by status.
  - Allowed values: `draft`, `confirmed`, `archived`, `all`
- `content_tags[]` (list of string, optional) — Optionally filter posts by content\_tags. Adding a content tag will return any post with that content tag associated to it.Example: Filtering for `content_tags: ["sales","closing"]` will return results of posts that have *either* `sales` or `closing` content\_tags.
- `slugs[]` (list of string, optional) — Optionally filter posts by their slugs. Adding a slug will return any post with that exact slug associated to it.Example: Filtering for `slugs: ["my-first-post","another-post"]` will return results of posts that have *either* `my-first-post` or `another-post` as their slug.
- `authors[]` (list of string, optional) — Optionally filter posts by their authors. Adding an author name will return any post with that author associated to it (case-insensitive).Example: Filtering for `authors: ["John Doe","Jane Smith"]` will return results of posts that have *either* John Doe or Jane Smith as authors.
- `premium_tiers` (list of string, optional) — Optionally filter posts by audience based on premium tiers. This takes in an array of Display Names of the premium tiers. It will also scope any expanded content output to the specified premium tiers. Note: This is case insensitive.
- `limit` (integer, optional) — A limit on the number of objects to be returned. The limit can range between 1 and 100, and the default is 10.
- `page` (integer, optional) — Pagination returns the results in pages. Each page contains the number of results specified by the `limit` (default: 10).If not specified, results 1-10 from page 1 will be returned.
- `order_by` (enum, optional, default: created) — The field that the results are sorted by. Defaults to created `created` - The time in which the post was first created. `publish_date` - The time the post was set to be published. `displayed_date` - The time displayed in place of the `publish_date`. If no `displayed_date` was set, it will default to the `publish_date`
  - Allowed values: `created`, `publish_date`, `displayed_date`
- `direction` (enum, optional, default: asc) — The direction that the results are sorted in. Defaults to asc `asc` - Ascending, sorts from smallest to largest. `desc` - Descending, sorts from largest to smallest.
  - Allowed values: `asc`, `desc`
- `hidden_from_feed` (enum, optional, default: all) — Optionally filter the results by the `hidden_from_feed` attribute of the post.`all` - Does not restrict results by `hidden_from_feed`.`true` - Only return posts hidden from the feed.`false` - Only return posts that are visible on the feed.
  - Allowed values: `all`, `true`, `false`

## Response

### 200

OK

- `data` (list of Post, required)
- `limit` (integer, required) — The limit placed on the results. If no limit was specified in the request,this defaults to 10.
- `page` (integer, required, default: 1) — The page number the results are from. If no page was specified in the request, this defaults to page 1.
- `total_results` (integer, required) — The total number of results from all pages.
- `total_pages` (integer, required) — The total number of pages.

## Errors

### 400 Bad Request Error

Bad Request

- `status` (integer, required)
- `statusText` (string, required)
- `errors` (list of ErrorDetail, required)

### 401 Unauthorized Error

Unauthorized. The API key or OAuth access token is missing, invalid, or expired.

- `status` (integer, required)
- `statusText` (string, required)
- `errors` (list of ErrorDetail, required)

### 404 Not Found Error

Resource Not Found

- `status` (integer, required)
- `statusText` (string, required)
- `errors` (list of ErrorDetail, required)

### 429 Too Many Requests Error

Rate Limit Exceeded

- `status` (integer, required)
- `statusText` (string, required)
- `errors` (list of ErrorDetail, required)

### 500 Internal Server Error

Internal Server Error

- `status` (integer, required)
- `statusText` (string, required)
- `errors` (list of ErrorDetail, required)

## Types

### Post

- `id` (string, required) — The prefixed post id
- `subtitle` (string, required) — The subtitle displayed in web views
- `title` (string, required) — The title displayed in web views
- `authors` (list of string, required) — An array of author names
- `created` (integer, required) — The time the post was created. Measured in seconds since the Unix epoch
- `status` (enum, required, default: draft) — The status of the post.`draft` - not been scheduled.`confirmed` - The post will be active after the `scheduled_at`.`archived` - The post is no longer active.
  - Allowed values: `draft`, `confirmed`, `archived`
- `split_tested` (boolean, required) — A flag to indicate if a split test was done. Only applicable to email posts.
- `subject_line` (string, required) — The email subject line. In cases of A/B Testing, this will be adjusted to the winning subject line.
- `preview_text` (string, required) — The email preview text
- `slug` (string, required) — The web slug where this post can be accessed.
- `thumbnail_url` (string, required) — The URL of the thumbnail. Defaults to the Publication logo if not set.
- `web_url` (string, required) — The full URL where this post can be accessed on the web. Only applicable if the platform is `web` or `both`.
- `audience` (enum, required) — The audience that the post is available to on the web. Only applicable if the platform is `web` or `both`.
  - Allowed values: `free`, `premium`, `both`
- `platform` (enum, required) — The platform that the post is or will be published to.
  - Allowed values: `web`, `email`, `both`
- `content_tags` (list of string, required) — All content tags that were associated with the post.
- `hidden_from_feed` (boolean, required, default: false) — A flag to indicate if the post is hidden from the website feed.
- `enforce_gated_content` (boolean, required, default: false) — A flag to indicate if the post enforces gated content for non-subscribers.
- `email_capture_popup` (boolean, required, default: false) — A flag to indicate if popup email capture is enabled for this post.
- `publish_date` (integer, optional) — The time the post was set to be published. Measured in seconds since the Unix epoch
- `displayed_date` (integer, optional) — The time displayed in place of the `publish_date`. Measured in seconds since the Unix epoch
- `preview_url` (string, optional) — A stable URL to the post's email preview in the beehiiv app. Renders the current post content on every visit. Requires a beehiiv session with access to the publication. The same URL is returned on every create, update, or show. Supports optional query parameters to preview as a specific audience: `platform` (`email` default, or `web`), `subscriber_type` (`free` default, or `premium` - requires `tier_ids[]`), `tier_ids[]` (one or more tier ids, only used when `subscriber_type=premium`), and `subscriber_id` (renders using that subscriber's real tier memberships and takes precedence over `subscriber_type`/`tier_ids[]`).
- `meta_default_description` (string, optional) — Meta tag description for the post, called SEO Description in the admin UI
- `meta_default_title` (string, optional) — meta tag title for the post, called SEO Title in the admin UI
- `newsletter_list_id` (string, optional) — The prefixed ID of the newsletter list this post is associated with. When set, the post targets only subscribers of this newsletter list.
- `recipients` (PostRecipientsResponse, optional) — The targeting configuration for this post.
- `recipient_count` (integer, optional) — Estimated (draft/confirmed) or actual (sent) number of email subscribers targeted by this post.
- `headers` (map from string to string, optional) — Custom email headers set on this post. A `null` value is a suppression marker for a header inherited from the newsletter list or publication.
- `from_address` (string, optional) — The from address for this post. If not set, falls back to the publication default.
- `reply_to_address` (string, optional) — The reply-to address replies to this post will go to. If not set on the post, falls back to the newsletter list it sends to, then the publication default, then the workspace owner.
- `sender_name` (string, optional) — The sender name preset composition key set on this post. If not set, falls back to the newsletter list or publication.
- `utm_source` (string, optional) — The utm_source set on this post. If not set, falls back to the newsletter list or publication.
- `utm_medium` (string, optional) — The utm_medium set on this post. If not set, falls back to the newsletter list or publication.
- `utm_campaign` (string, optional) — The utm_campaign set on this post. If not set, falls back to the newsletter list or publication.
- `utm_params_enabled` (boolean, optional) — Whether UTM parameters are appended to links in this post. If not set, falls back to the newsletter list or publication.
- `content` (PostContent, optional) — Optional html content for a post. Retrievable by including any of `expand: [free_web_content, free_email_content, free_rss_content, premium_web_content, premium_email_content]` in the post request body. **Note:** Generating HTML is slow. We recommend only requesting the HTML versions you need at the time.
- `stats` (PostStats, optional) — Optional list of stats for a post. Retrievable by including `expand: [stats]` in the post request body. **Note:** If a timeout occurs while aggregating stats, subsequent requests may return consolidated click metrics rather than individual raw click metrics.

### ErrorDetail

- `message` (string, required)
- `code` (string, required)

### PostRecipientsResponse

The targeting configuration currently set on the post.

- `web` (PostWebRecipientsResponse, optional) — Web targeting configuration. Absent if the post has no web targets.
- `email` (PostEmailRecipientsResponse, optional) — Email targeting configuration. Absent if the post has no email targets.

### PostContent

Optional html content for a post. Retrievable by including any of `expand: [free_web_content, free_email_content, free_rss_content, premium_web_content, premium_email_content]` in the post request body. **Note:** Generating HTML is slow. We recommend only requesting the HTML versions you need at the time.

- `free` (FreePostContent, optional) — The requested free post HTML. This HTML has paywalls enforced.
- `premium` (PremiumPostContent, optional) — The requested premium post HTML. This HTML does not have paywalls enforced.

### PostStats

Optional list of stats for a post. Retrievable by including `expand: [stats]` in the post request body. **Note:** If a timeout occurs while aggregating stats, subsequent requests may return consolidated click metrics rather than individual raw click metrics.

- `email` (PostStatsEmail, optional) — Stats scoped only to email recipients. Not relevant for posts published only to web
- `web` (PostStatsWeb, optional) — Stats scoped only to web views. Not relevant for posts published only to email
- `clicks` (list of ClickStats, optional) — An array of click statistics for each URL in the post
- `upgrades` (integer, optional, default: 0) — Total number of free-to-premium upgrades attributed to this post. Only populated when the publication has a paid subscription tier.

### PostWebRecipientsResponse

- `tier_ids` (list of string, optional) — The tier IDs targeted for web. Values are `"free"`, `"premium"`, or a specific tier's prefixed ID.
- `include_segment_ids` (list of string, optional)
- `exclude_segment_ids` (list of string, optional)

### PostEmailRecipientsResponse

- `tier_ids` (list of string, optional) — The tier IDs targeted for email. Values are `"free"`, `"premium"`, or a specific tier's prefixed ID.
- `include_segment_ids` (list of string, optional)
- `exclude_segment_ids` (list of string, optional)

### FreePostContent

The requested free post HTML. This HTML has paywalls enforced.

- `web` (string, optional) — The web HTML rendered to a free or annonomous reader.
- `email` (string, optional) — The email HTML rendered to a free reader.
- `rss` (string, optional) — The HTML that is rendered in RSS feeds.

### PremiumPostContent

The requested premium post HTML. This HTML does not have paywalls enforced.

- `web` (string, optional) — The website HTML rendered to a free reader
- `email` (string, optional) — The email HTML rendered to a premium reader

### PostStatsEmail

Stats scoped only to email recipients. Not relevant for posts published only to web

- `recipients` (integer, optional, default: 0) — Total number of email recipients
- `delivered` (integer, optional, default: 0) — Total number of emails delivered
- `opens` (integer, optional, default: 0) — Total number of email opens
- `unique_opens` (integer, optional, default: 0) — Total number of unique email opens
- `open_rate` (double, optional) — The percentage of emails that have been opened
- `clicks` (integer, optional, default: 0) — Total number of email clicks
- `unique_clicks` (integer, optional, default: 0) — Unique number of email clicks
- `verified_clicks` (integer, optional, default: 0) — Total number of verified human email clicks across all URLs in this post. Verified clicks have passed bot detection and are confirmed to be from real subscribers.
- `unique_verified_clicks` (integer, optional, default: 0) — Unique number of verified human email clicks across all URLs in this post. Only counts the first verified click per subscriber.
- `click_rate` (double, optional) — The percentage of emails that have been clicked
- `unsubscribes` (integer, optional, default: 0) — Total number of email unsubscribes
- `spam_reports` (integer, optional, default: 0) — The number of subscribers that reported this post email as spam

### PostStatsWeb

Stats scoped only to web views. Not relevant for posts published only to email

- `views` (integer, optional, default: 0) — Total number of web views
- `clicks` (integer, optional, default: 0) — Total number of web clicks

### ClickStats

Details about specific URL's click stats from a post.

- `url` (string, optional) — The URL the stats are for
- `base_url` (string, optional) — The canonical URL with all query parameters and fragments removed. Derived by stripping everything after the '?' and '#' characters. Preserves the protocol, host (including casing), port, and path exactly as they appear in the original URL. Guaranteed to be present whenever url is present, enabling grouping of clicks by destination regardless of tracking parameters.
- `email` (PostClickStatsEmail, optional) — URL stats scoped only to email recipients. Not relevant for posts published only to web
- `web` (PostClickStatsWeb, optional) — Stats scoped only to web views. Not relevant for posts published only to email
- `total_clicks` (integer, optional)
- `total_unique_clicks` (integer, optional)
- `total_click_through_rate` (double, optional) — The percentage of clicks on the URL compared to the total number of recipients and web views

### PostClickStatsEmail

URL stats scoped only to email recipients. Not relevant for posts published only to web

- `clicks` (integer, optional)
- `unique_clicks` (integer, optional)
- `verified_clicks` (integer, optional) — Total number of verified human email clicks on this URL. Verified clicks have passed bot detection and are confirmed to be from real subscribers.
- `unique_verified_clicks` (integer, optional) — Unique number of verified human email clicks on this URL. Only counts the first verified click per subscriber.
- `click_through_rate` (double, optional) — The percentage of email clicks on the URL compared to the total number of recipients

### PostClickStatsWeb

Stats scoped only to web views. Not relevant for posts published only to email

- `clicks` (integer, optional)
- `unique_clicks` (integer, optional)
- `click_through_rate` (double, optional) — The percentage of clicks on the URL compared to the total number of web views

## Examples

**Response**

```json
{
  "data": [
    {
      "id": "post_00000000-0000-0000-0000-000000000000",
      "subtitle": "New post subtitle",
      "title": "New Post Title",
      "authors": [
        "Clark Kent"
      ],
      "created": 1666800076,
      "status": "draft",
      "split_tested": true,
      "subject_line": "Check this out",
      "preview_text": "More news on the horizon",
      "slug": "slug",
      "thumbnail_url": "thumbnail_url",
      "web_url": "web_url",
      "audience": "free",
      "platform": "web",
      "content_tags": [
        "content_tags"
      ],
      "hidden_from_feed": true,
      "enforce_gated_content": false,
      "email_capture_popup": false,
      "publish_date": 1666800076,
      "displayed_date": 1666800076,
      "preview_url": "https://app.beehiiv.com/posts/post_00000000-0000-0000-0000-000000000000/preview",
      "meta_default_description": "A post with great content",
      "meta_default_title": "My great post",
      "newsletter_list_id": "nl_list_00000000-0000-0000-0000-000000000000",
      "content": {
        "free": {
          "web": "<HTML><body><h1>New Post</h1></body></HTML>",
          "email": "<HTML><body><h1>New Post</h1></body></HTML>",
          "rss": "<HTML><body><h1>New Post</h1></body></HTML>"
        },
        "premium": {
          "web": "<HTML><body><h1>New Post</h1></body></HTML>",
          "email": "<HTML><body><h1>New Post</h1></body></HTML>"
        }
      },
      "stats": {
        "email": {
          "recipients": 100,
          "delivered": 100,
          "opens": 50,
          "unique_opens": 45,
          "open_rate": 45,
          "clicks": 10,
          "unique_clicks": 8,
          "verified_clicks": 9,
          "unique_verified_clicks": 7,
          "click_rate": 8,
          "unsubscribes": 1,
          "spam_reports": 1
        },
        "web": {
          "views": 200,
          "clicks": 40
        },
        "clicks": [
          {
            "url": "https://www.google.com",
            "base_url": "https://www.google.com",
            "email": {
              "clicks": 10,
              "unique_clicks": 8,
              "verified_clicks": 9,
              "unique_verified_clicks": 7,
              "click_through_rate": 80
            },
            "web": {
              "clicks": 40,
              "unique_clicks": 40,
              "click_through_rate": 20
            },
            "total_clicks": 50,
            "total_unique_clicks": 48,
            "total_click_through_rate": 40
          }
        ],
        "upgrades": 5
      }
    }
  ],
  "limit": 1,
  "page": 1,
  "total_results": 1,
  "total_pages": 1
}
```

**SDK Code**

```typescript
import { BeehiivClient } from "beehiiv";

const client = new BeehiivClient({ token: "YOUR_TOKEN" });
await client.posts.list("pub_00000000-0000-0000-0000-000000000000");

```

```python
import requests

url = "https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts"

headers = {"Authorization": "Bearer <token>"}

response = requests.get(url, headers=headers)

print(response.json())
```

```go
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts"

	req, _ := http.NewRequest("GET", url, nil)

	req.Header.Add("Authorization", "Bearer <token>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.get("https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts")
  .header("Authorization", "Bearer <token>")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts', [
  'headers' => [
    'Authorization' => 'Bearer <token>',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts");
var request = new RestRequest(Method.GET);
request.AddHeader("Authorization", "Bearer <token>");
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Authorization": "Bearer <token>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```