curl --request GET \
--url https://api.privy.com/v1/reports/campaigns \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.privy.com/v1/reports/campaigns"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.privy.com/v1/reports/campaigns', 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://api.privy.com/v1/reports/campaigns",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.privy.com/v1/reports/campaigns"
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(string(body))
}HttpResponse<String> response = Unirest.get("https://api.privy.com/v1/reports/campaigns")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.privy.com/v1/reports/campaigns")
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{
"data": [
{
"campaign_id": "kD4qN8wZr1",
"type": "newsletter",
"title": "June Restock Announcement",
"channel": "email",
"sent_date": "2026-06-18",
"audience": {
"included_segments": [
"Engaged 90d"
],
"excluded_segments": [
"Recent purchasers"
]
},
"stats": {
"sent": {
"value": 8200,
"unit": "count"
},
"delivered": {
"value": 8020,
"unit": "count"
},
"delivery_rate": {
"value": 97.8,
"unit": "percent"
},
"opened_unique": {
"value": 3450,
"unit": "count"
},
"open_rate": {
"value": 43,
"unit": "percent"
},
"clicked_unique": {
"value": 610,
"unit": "count"
},
"click_rate": {
"value": 7.6,
"unit": "percent"
},
"bounced": {
"value": 74,
"unit": "count"
},
"bounce_rate": {
"value": 0.9,
"unit": "percent"
},
"spam_reports": {
"value": 3,
"unit": "count"
},
"unsubscribed": {
"value": 21,
"unit": "count"
},
"unsubscribe_rate": {
"value": 0.3,
"unit": "percent"
},
"attributed_orders": {
"value": 45,
"unit": "count"
},
"order_rate": {
"value": 0.6,
"unit": "percent"
},
"attributed_revenue": {
"value": 2810,
"unit": "usd"
},
"average_order_value": {
"value": 62.44,
"unit": "usd"
}
}
}
],
"period": {
"label": "last_30_days",
"start_date": "2026-06-21",
"end_date": "2026-07-20"
},
"pagination": {
"page": 1,
"per_page": 10,
"total_count": 12,
"total_pages": 2
}
}{
"error": {
"code": "unauthorized",
"message": "Bearer token is missing or invalid"
}
}{
"error": {
"code": "insufficient_scope",
"message": "Token does not have the required scope"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid",
"details": [
{
"field": "email",
"message": "is required"
}
]
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded"
}
}List campaign performance
Every message campaign sent in the period, ranked by attributed revenue, each row carrying the full channel stat block (delivery, engagement, unsubscribes, attribution) scoped to the same period — so period comparisons need no per-campaign detail calls. Zero-revenue sends are listed too, ranked after the revenue rows; at most 100 campaigns are ranked, and a send with no recorded activity at all in the period does not appear.
By default the list serves your newsletters — email and SMS
one-time sends. Pass type=flows to instead rank the flow
campaigns: the email and SMS messages your automation flows send
(flow rows carry no sent_date or audience, and instead name the
flow that sends them in flow, so attributing a message to its flow
needs no follow-up request). Use
/v1/reports/campaigns/{campaign_id} for one campaign’s
most-clicked links or its lifetime funnel.
Required scope: reports_read
curl --request GET \
--url https://api.privy.com/v1/reports/campaigns \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.privy.com/v1/reports/campaigns"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.privy.com/v1/reports/campaigns', 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://api.privy.com/v1/reports/campaigns",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.privy.com/v1/reports/campaigns"
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(string(body))
}HttpResponse<String> response = Unirest.get("https://api.privy.com/v1/reports/campaigns")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.privy.com/v1/reports/campaigns")
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{
"data": [
{
"campaign_id": "kD4qN8wZr1",
"type": "newsletter",
"title": "June Restock Announcement",
"channel": "email",
"sent_date": "2026-06-18",
"audience": {
"included_segments": [
"Engaged 90d"
],
"excluded_segments": [
"Recent purchasers"
]
},
"stats": {
"sent": {
"value": 8200,
"unit": "count"
},
"delivered": {
"value": 8020,
"unit": "count"
},
"delivery_rate": {
"value": 97.8,
"unit": "percent"
},
"opened_unique": {
"value": 3450,
"unit": "count"
},
"open_rate": {
"value": 43,
"unit": "percent"
},
"clicked_unique": {
"value": 610,
"unit": "count"
},
"click_rate": {
"value": 7.6,
"unit": "percent"
},
"bounced": {
"value": 74,
"unit": "count"
},
"bounce_rate": {
"value": 0.9,
"unit": "percent"
},
"spam_reports": {
"value": 3,
"unit": "count"
},
"unsubscribed": {
"value": 21,
"unit": "count"
},
"unsubscribe_rate": {
"value": 0.3,
"unit": "percent"
},
"attributed_orders": {
"value": 45,
"unit": "count"
},
"order_rate": {
"value": 0.6,
"unit": "percent"
},
"attributed_revenue": {
"value": 2810,
"unit": "usd"
},
"average_order_value": {
"value": 62.44,
"unit": "usd"
}
}
}
],
"period": {
"label": "last_30_days",
"start_date": "2026-06-21",
"end_date": "2026-07-20"
},
"pagination": {
"page": 1,
"per_page": 10,
"total_count": 12,
"total_pages": 2
}
}{
"error": {
"code": "unauthorized",
"message": "Bearer token is missing or invalid"
}
}{
"error": {
"code": "insufficient_scope",
"message": "Token does not have the required scope"
}
}{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid",
"details": [
{
"field": "email",
"message": "is required"
}
]
}
}{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded"
}
}Authorizations
Send either an API token or an OAuth access token as
Authorization: Bearer <token>. See the Authentication page for details.
Headers
Target a subaccount instead of the main connected account.
Pass a subaccount id from get_account's subaccounts list. Only direct subaccounts
of the connected account are reachable — omit this for the main account itself.
Query Parameters
The date range to report over, resolved in the account's time zone. Pass custom together with start_date and end_date.
last_7_days, last_30_days, last_90_days, this_month, last_month, custom Inclusive start of a custom period (ISO 8601 date). Required when period=custom, ignored otherwise.
Inclusive end of a custom period (ISO 8601 date). Required when period=custom, ignored otherwise. Custom periods are capped at 366 days.
Pass flows to rank the messages your flows send instead of newsletters.
flows Filter by message channel. Works for both newsletters and flow campaigns.
email, sms Page number (starts at 1).
x >= 1Campaigns per page. Values above 50 are clamped to 50; omitted or invalid values default to 10.
1 <= x <= 50Response
Campaigns ranked by attributed revenue over the resolved period.
Show child attributes
Show child attributes
The resolved date range the report covers, inclusive, in the account's
time zone. Every label except last_month ends today, so the last day
of the range is still in progress: last_7_days is the six days before
today plus today, this_month runs from the 1st through today, and
last_month is the whole previous calendar month. Custom ranges cannot
exceed 366 days.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Was this page helpful?