import { ParagraphAPI } from "@paragraph-com/sdk"
const api = new ParagraphAPI({ apiKey: "your-api-key" })
const { rows } = await api.analytics.query({
sql: `SELECT title, total_views
FROM post_analytics_summary
WHERE published_at > now() - interval '30 days'
ORDER BY total_views DESC
LIMIT 10`,
})curl -X POST "https://public.api.paragraph.com/api/v1/analytics/query" \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT COUNT(*) FROM subscribers"}'import requests
url = "https://public.api.paragraph.com/api/v1/analytics/query"
payload = { "sql": "<string>" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({sql: '<string>'})
};
fetch('https://public.api.paragraph.com/api/v1/analytics/query', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://public.api.paragraph.com/api/v1/analytics/query",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sql' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://public.api.paragraph.com/api/v1/analytics/query"
payload := strings.NewReader("{\n \"sql\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://public.api.paragraph.com/api/v1/analytics/query")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sql\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://public.api.paragraph.com/api/v1/analytics/query")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sql\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"rows": [
{}
],
"rowCount": 123,
"fields": [
{
"name": "<string>"
}
],
"truncated": true
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}Run an analytics SQL 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:
SELECTorWITH(CTE) statements only- Reference tables unprefixed (e.g.
FROM posts) — theanalyticsschema is the defaultsearch_path - No semicolons, no writes, no DDL, no superuser functions
- Hard limit of 10,000 rows; anything over is truncated and
truncated: trueis 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.
import { ParagraphAPI } from "@paragraph-com/sdk"
const api = new ParagraphAPI({ apiKey: "your-api-key" })
const { rows } = await api.analytics.query({
sql: `SELECT title, total_views
FROM post_analytics_summary
WHERE published_at > now() - interval '30 days'
ORDER BY total_views DESC
LIMIT 10`,
})curl -X POST "https://public.api.paragraph.com/api/v1/analytics/query" \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT COUNT(*) FROM subscribers"}'import requests
url = "https://public.api.paragraph.com/api/v1/analytics/query"
payload = { "sql": "<string>" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({sql: '<string>'})
};
fetch('https://public.api.paragraph.com/api/v1/analytics/query', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://public.api.paragraph.com/api/v1/analytics/query",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sql' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://public.api.paragraph.com/api/v1/analytics/query"
payload := strings.NewReader("{\n \"sql\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://public.api.paragraph.com/api/v1/analytics/query")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sql\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://public.api.paragraph.com/api/v1/analytics/query")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sql\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"rows": [
{}
],
"rowCount": 123,
"fields": [
{
"name": "<string>"
}
],
"truncated": true
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}{
"success": false,
"msg": "<string>"
}Authorizations
API key for authenticating protected endpoints. Pass as Bearer token in Authorization header.
Body
Body
A SELECT or WITH query against the analytics schema. The analytics.* prefix is implicit.
1Response
Query executed successfully
Result rows. Each row is a column-name → value map.
Show child attributes
Show child attributes
Number of rows returned (after truncation if any)
Column names in row order
Show child attributes
Show child attributes
True if the result was truncated at the 10,000 row limit

