curl --request GET \
--url https://api.privy.com/v1/reports/top_products \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.privy.com/v1/reports/top_products"
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/top_products', 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/top_products",
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/top_products"
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/top_products")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.privy.com/v1/reports/top_products")
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": [
{
"rank": 1,
"product_id": "812345",
"title": "Blue Tee"
},
{
"rank": 2,
"product_id": "819921",
"title": "Beach Towel"
}
],
"metric": "views",
"window_days": 30,
"refreshed_at": "2026-07-29T06:10:00Z",
"note": "Entries are the connected store platform's product ids (e.g. the numeric Shopify product id) ranked best-first. Titles are resolved live from the connected store and are null when it cannot supply them; full product details are at GET /v1/products/{product_id}.",
"pagination": {
"page": 1,
"per_page": 40,
"total_count": 2,
"total_pages": 1
}
}{
"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 top products
Retrieve your store’s top products ranked by views or sales over a 30, 60, or 90 day window, or all-time where the platform supports it.
Entries are the connected store platform’s product ids (e.g. the
numeric Shopify product id) ranked best-first. Each entry carries the
product title, resolved live from the connected store; it is null
when the store cannot supply it (no connected store, the product was
deleted, or the store API was unreachable). Full product details are
at GET /v1/products/{id}. Every response repeats this in an in-band
note.
Rankings are served from a daily-refreshed cache. refreshed_at is
the cache timestamp; null means this metric/window combination has
never been computed (as opposed to computed and empty) — the request
itself enqueues the background refresh that fills it, so retry after
a short wait.
Platform support differs by metric, and for views by window.
Windowed view rankings (window_days 30, 60, or 90) require a
connected Shopify store: dated product views are only tracked through
Shopify’s Web Pixel, so those windows return an empty ranking on every
other platform. All-time view rankings (window_days=0) come from the
store platform’s own view counter and are available for BigCommerce
only — BigCommerce exposes a per-product view count with no date
dimension, so it can be ranked all-time but never windowed. Shopify
has no such counter, so window_days=0 returns an empty ranking for a
Shopify-only store; a store connected to both platforms is ranked from
its BigCommerce counter.
The all-time ranking is built by scanning the BigCommerce catalog, and that scan is capped at 25,000 products. On a larger catalog it covers the products scanned rather than the whole catalog, so treat it as approximate there.
sales is derived from synced order data and works for any store that
syncs orders to Privy (Shopify, BigCommerce, Wix, and orders submitted
through the API). There is no all-time sales bucket, so
metric=sales with window_days=0 is rejected.
Required scope: reports_read
curl --request GET \
--url https://api.privy.com/v1/reports/top_products \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.privy.com/v1/reports/top_products"
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/top_products', 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/top_products",
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/top_products"
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/top_products")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.privy.com/v1/reports/top_products")
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": [
{
"rank": 1,
"product_id": "812345",
"title": "Blue Tee"
},
{
"rank": 2,
"product_id": "819921",
"title": "Beach Towel"
}
],
"metric": "views",
"window_days": 30,
"refreshed_at": "2026-07-29T06:10:00Z",
"note": "Entries are the connected store platform's product ids (e.g. the numeric Shopify product id) ranked best-first. Titles are resolved live from the connected store and are null when it cannot supply them; full product details are at GET /v1/products/{product_id}.",
"pagination": {
"page": 1,
"per_page": 40,
"total_count": 2,
"total_pages": 1
}
}{
"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
Ranking metric. views availability depends on the store's platform and the requested window (see the operation description); sales works for any store that syncs orders to Privy.
views, sales Ranking window in days. 0 means all-time, which is supported for metric=views on BigCommerce stores only and is rejected for metric=sales.
0, 30, 60, 90 Page number (starts at 1).
x >= 1Number of results per page. Values outside 10–100 are clamped to the nearest bound.
10 <= x <= 100Response
The ranked product ids for the requested metric and window.
Show child attributes
Show child attributes
views, sales The window this ranking covers. 0 means all-time.
0, 30, 60, 90 When this ranking was last computed. null if it has never been computed. For the rolling windows the request enqueues the refresh that fills it. The all-time views ranking is computed only for BigCommerce stores that have configured a most-viewed dynamic product block, so null there means either the store's platform produces no all-time view counts or no such block exists yet.
Fixed caveat that entries are store product ids with live-resolved titles, and full details live at GET /v1/products/{id}.
Show child attributes
Show child attributes
Was this page helpful?