POST
/v1/analytics/query

Execute a read-only SQL query against the analytics schema, scoped to your publication.

Auth: Requires an API key. Queries run as the publication that owns the API key; Row-Level Security guarantees that only that publication's rows are visible even if the SQL is unscoped.

SQL requirements:

  • SELECT or WITH (CTE) statements only
  • Reference tables unprefixed (e.g. FROM posts) — the analytics schema is the default search_path
  • No semicolons, no writes, no DDL, no superuser functions
  • Hard limit of 10,000 rows; anything over is truncated and truncated: true is returned
  • 30-second statement timeout

Discovering the schema: call GET /v1/analytics/schema for column metadata.

Common queries: open rate, subscriber count, top posts by views, engagement over time, click-through rate.

For raw post-scoped tables, join through posts.draft_of to roll draft/version rows up to the canonical published post.

Authorization

apiKey
headerAuthorizationBearer <token>

API key for authenticating protected endpoints. Pass as Bearer token in Authorization header.

Request Body

Body

application/json
  1. body
sql*string

A SELECT or WITH query against the analytics schema. The analytics.* prefix is implicit.

Length1 <= length

Response Body

Query executed successfully

application/json
  1. response
rows*array<>

Result rows. Each row is a column-name → value map.

rowCount*integer

Number of rows returned (after truncation if any)

fields*array<>

Column names in row order

truncated*boolean

True if the result was truncated at the 10,000 row limit

curl -X POST "https://example.com/v1/analytics/query" \  -H "Content-Type: application/json" \  -d '{    "sql": "string"  }'
{  "rows": [    {      "property1": null,      "property2": null    }  ],  "rowCount": 0,  "fields": [    {      "name": "string"    }  ],  "truncated": true}