> 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.

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

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

Retrieve aggregate stats for all posts.The `clicks` breakdown returns the top 1,000 URLs by total clicks across the matched posts, and URLs that differ only by a per-subscriber UUID (polls, preference pages, etc.) are grouped into a single row\.Stats for large publications are served from a cache that is refreshed in the background about once an hour; `computed_at` is when the returned numbers were calculated.

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

## 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

- `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.
- `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.
- `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` (PostsAggregateStatsResponseStats, required)

## 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

### PostsAggregateStatsResponseStats

- `stats` (PostStats, required) — 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.
- `computed_at` (integer, required) — When the returned stats were calculated. Measured in seconds since the Unix epoch.

### ErrorDetail

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

### 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.

### 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": {
    "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
    },
    "computed_at": 1700000000
  }
}
```

**SDK Code**

```python
import requests

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

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

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

print(response.json())
```

```javascript
const url = 'https://api.beehiiv.com/v2/publications/pub_00000000-0000-0000-0000-000000000000/posts/aggregate_stats';
const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

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

func main() {

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

	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/aggregate_stats")

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/aggregate_stats")
  .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/aggregate_stats', [
  '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/aggregate_stats");
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/aggregate_stats")! 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()
```