Statistics
Methods for reports: the same statistics as on the "Dashboard" and the "Statistics" page, only in JSON - for your own dashboards, exports to spreadsheets and automated checks. What the metrics themselves mean is covered in detail in the "Dashboard" and "Statistics" sections.
| Method | Address | What it does |
|---|---|---|
| GET | /stats/dashboard | Summary for a period: totals, charts, GEO, devices, top PWAs |
| GET | /stats/funnel | Daily funnel and split results |
| GET | /stats/subs | Breakdown by one sub tag (sub_1 … sub_20) |
Replace YOUR_KEY and panel.example.com with your own.
Period
All three methods require a period - the date_from and date_to parameters:
| Parameter | Required | What it is |
|---|---|---|
date_from | yes | First day of the period, YYYY-MM-DD |
date_to | yes | Last day of the period inclusive, not earlier than date_from |
- Days are taken whole:
date_from=2026-10-01&date_to=2026-10-01is the whole of October 1. - Time is UTC: a day starts at 00:00 UTC, not in your time zone.
- The period is no longer than a month:
date_tocan't be later thandate_fromplus one month. Need statistics for a quarter - request it month by month and add up (exceptuniqand percentages - those can't be added).
Without dates or with a period longer than a month you get 422:
{
"data": null,
"message": "The date to field must be a date before or equal to 2026-02-01.",
"errors": { "date_to": ["The date to field must be a date before or equal to 2026-02-01."] }
}Visits by visit date, events by event date
Visits, uniques and whites are counted by the day of the visit. Installs, opens, registrations and deposits - by the day the event arrived. That's why on a short period a funnel step can be more than 100% of the previous one: the events belong to visits that happened earlier.
The numbers are refreshed every few minutes: a fresh visit or deposit shows up in reports with a delay of up to 5 minutes. Statistics cover only the PWAs of your teams, for admin - all of them.
Summary
GET/stats/dashboard
Everything the "Dashboard" shows, in one request. The only parameter is the period.
curl "https://panel.example.com/api/stats/dashboard?date_from=2026-09-29&date_to=2026-10-05" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$response = $api->get('stats/dashboard', ['query' => [
'date_from' => '2026-09-29',
'date_to' => '2026-10-05',
]]);
$metrics = json_decode($response->getBody(), true)['data']['metrics'];
echo "Installs: {$metrics['total_installs']['value']} ({$metrics['total_installs']['trend']}%)\n";
echo "Deposits: {$metrics['total_deposits']['value']}, CR {$metrics['avg_cr']['value']}%\n";# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
response = requests.get(f"{API}/stats/dashboard", headers=HEADERS, params={
"date_from": "2026-09-29",
"date_to": "2026-10-05",
}, timeout=30)
metrics = response.json()["data"]["metrics"]
print("Installs:", metrics["total_installs"]["value"], f'({metrics["total_installs"]["trend"]}%)')
print("Deposits:", metrics["total_deposits"]["value"], f'CR {metrics["avg_cr"]["value"]}%')// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
const params = new URLSearchParams({ date_from: '2026-09-29', date_to: '2026-10-05' });
const response = await fetch(`${API}/stats/dashboard?${params}`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();
const { metrics } = data;
console.log('Installs:', metrics.total_installs.value, `(${metrics.total_installs.trend}%)`);
console.log('Deposits:', metrics.total_deposits.value, `CR ${metrics.avg_cr.value}%`);Response (shortened)
{
"data": {
"metrics": {
"previous_period": { "from": "2026-09-22", "to": "2026-09-29" },
"total_visits": { "value": 12480, "trend": 14 },
"total_uniques": { "value": 9870, "trend": 12.2 },
"total_repeats": { "value": 2610, "trend": 21.4 },
"total_whites": { "value": 1310, "trend": -7.7 },
"total_installs": { "value": 2140, "trend": 8.1 },
"total_opens": { "value": 1890, "trend": 11 },
"total_registrations": { "value": 412, "trend": 8.4 },
"total_deposits": {
"value": 96,
"trend": 18.5,
"amounts": [
{ "currency": "USD", "amount": 4820.5 },
{ "currency": "EUR", "amount": 310 }
]
},
"avg_cr": { "value": 4.49, "trend": 6.1 }
},
"chart": [
{ "date": "2026-10-04", "visits": 610, "whites": 60, "installs": 110, "deposits": 4 },
{ "date": "2026-10-05", "visits": 640, "whites": 70, "installs": 120, "deposits": 6 }
],
"geo": [
{ "code": "US", "installs": 1200, "cr": 5.1, "pct": 100 },
{ "code": "DE", "installs": 400, "cr": 3.5, "pct": 33 }
],
"devices": [
{ "name": "Android", "installs": 1900, "pct": 89 },
{ "name": "iOS", "installs": 240, "pct": 11 }
],
"pwas": [
{
"id": "WZAX", "name": "Promo EN", "icon_url": null,
"domains": [{ "id": "K3LP", "domain": "promo-en.example.com" }],
"visits": 12480, "installs": 2140, "cr": 4.5, "status": "active"
}
],
"activity": [
{ "id": "WZAX", "type": "deploy", "msg": "Promo EN is running", "icon_url": null, "time": "2 hours ago" }
]
}
}metrics - totals for the period. Each metric has value - the value, and trend - the change in percent against the equally long period before it (its dates are in previous_period). If the previous period was zero and now there is something, trend is 100.
| Field | What it is |
|---|---|
total_visits | All visits |
total_uniques | Uniques - a visitor's first visits per flow |
total_repeats | Repeats: visits minus uniques |
total_whites | Visits the filter sent to the white page |
total_installs | Installs |
total_opens | Offer opens from an installed PWA |
total_registrations | Registrations from postbacks |
total_deposits | Deposits; amounts - payout totals for each currency separately |
avg_cr | Conversion: deposits ÷ installs × 100%. trend - the change against the previous period, same rules as the other trends |
chart - the same visits, whites, installs and deposits by day. A day that had only installs or deposits and no visits is there too - with zeros in visits and whites.
geo - top 10 countries by installs: code - country code (XX - not detected), cr - deposits ÷ installs, pct - installs as a percentage of the first country in the list.
devices - installs by device type (Android, iOS, Desktop), pct - share of all installs.
pwas - top 10 PWAs by installs for the period. status: active - enabled, paused - disabled, archived - archived. domains - the PWA's domains (an empty list when it has none).
activity - the ten most recently changed PWAs, regardless of the period. It's for the feed in the panel, scripts rarely need it.
Errors
| Code | message | When |
|---|---|---|
422 | The date from field is required. (and 1 more error) | No period passed |
422 | The date from field must be a valid date. | The date isn't recognised |
422 | The date to field must be a date after or equal to date from. | date_to is earlier than date_from |
422 | The date to field must be a date before or equal to 2026-02-01. | The period is longer than a month (the text gives the last allowed day) |
Funnel and splits
GET/stats/funnel
The Visits → Uniq → Installs → Opens → Reg → Dep funnel by day and in total, plus split results.
| Parameter | Required | What it is |
|---|---|---|
date_from, date_to | yes | Period, see above |
pwa_id | no | Only one PWA. Someone else's or a non-existent one - 404 |
curl "https://panel.example.com/api/stats/funnel?date_from=2026-09-29&date_to=2026-10-05" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$response = $api->get('stats/funnel', ['query' => [
'date_from' => '2026-09-29',
'date_to' => '2026-10-05',
]]);
$funnel = json_decode($response->getBody(), true)['data']['funnel'];
foreach ($funnel['rows'] as $day) {
echo "{$day['date']}: {$day['installs']} installs, {$day['deps']} deposits, CR {$day['cr']}%\n";
}# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
response = requests.get(f"{API}/stats/funnel", headers=HEADERS, params={
"date_from": "2026-09-29",
"date_to": "2026-10-05",
}, timeout=30)
funnel = response.json()["data"]["funnel"]
for day in funnel["rows"]:
print(day["date"], day["installs"], "installs,", day["deps"], "deposits, CR", day["cr"])// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
const params = new URLSearchParams({ date_from: '2026-09-29', date_to: '2026-10-05' });
const response = await fetch(`${API}/stats/funnel?${params}`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();
for (const day of data.funnel.rows) {
console.log(day.date, day.installs, 'installs,', day.deps, 'deposits, CR', day.cr);
}Response (shortened)
{
"data": {
"funnel": {
"rows": [
{
"date": "2026-10-05",
"visits": 640, "uniq": 590, "uniques": 520, "repeats": 120, "whites": 70,
"installs": 120, "opens": 105, "regs": 24, "deps": 6,
"inst_pct": 20.3, "open_pct": 87.5, "reg_pct": 20, "dep_pct": 25, "cr": 5
}
],
"totals": {
"date": null,
"visits": 1250, "uniq": 1100, "uniques": 1020, "repeats": 230, "whites": 130,
"installs": 230, "opens": 203, "regs": 44, "deps": 10,
"inst_pct": 20.9, "open_pct": 88.3, "reg_pct": 19.1, "dep_pct": 22.7, "cr": 4.3
}
},
"splits": [
{
"id": "Lm3T",
"name": "Split US",
"status": "active",
"visits": 6100, "installs": 1030, "conversions": 44, "cr": 4.3,
"flows": [
{
"flow_id": "Qp8R", "pwa_id": "WZAX", "pwa_name": "Promo EN", "weight": 50,
"visits": 6100, "installs": 1030, "conversions": 44, "cr": 4.3
}
]
}
],
"pwa": null
}
}funnel.rows - one row per day, newest first. funnel.totals - the total for the whole period (date is null).
| Field | What it is |
|---|---|
visits | All visits |
uniq | Unique people - how many different visitors came |
uniques | Unique visits - a visitor's first visits per flow. One person who went from the main PWA to a similar one is two uniques but one uniq |
repeats | Repeats: visits − uniques |
whites | Went to the white page |
installs, opens, regs, deps | Installs, offer opens, registrations, deposits |
inst_pct | Installs ÷ uniq |
open_pct | Opens ÷ installs |
reg_pct | Registrations ÷ installs |
dep_pct | Deposits ÷ registrations |
cr | Deposits ÷ installs - the main conversion |
All percentages are numbers from 0 to 100 (on a short period they can be higher, see above).
uniq can't be added up
totals.uniq is recounted over the whole period: a person who came three days in a row is in it once, but three times in the sum of days. So totals.uniq is less than the sum of daily uniq, and that's correct. The other counters add up as usual.
splits - all splits of your teams with results for the period: the total per split and per flow inside it (flows) - which PWA the flow leads to (pwa_id, pwa_name) and its weight (weight). conversions are deposits, cr - deposits ÷ installs. With the pwa_id parameter the split list is empty: a split combines several PWAs and isn't counted for a single app.
pwa - the selected PWA (id and name) if pwa_id was passed, otherwise null.
Errors
| Code | message | When |
|---|---|---|
422 | The date from field is required. (and 1 more error) | No period passed |
422 | The date from field must be a valid date. | The date isn't recognised |
422 | The date to field must be a date after or equal to date from. | date_to is earlier than date_from |
422 | The date to field must be a date before or equal to 2026-02-01. | The period is longer than a month (the text gives the last allowed day) |
404 | Not found | pwa_id of someone else's or a non-existent PWA |
Sub tags
GET/stats/subs
Which values of one sub tag bring installs and deposits: for example, which campaigns, creatives or ad sets in sub_1 … sub_20 really convert.
| Parameter | Required | What it is |
|---|---|---|
date_from, date_to | yes | Period, see above |
slot | yes | Sub tag number, from 1 to 20 |
limit | no | How many values to return, from 1 to 100, 20 by default |
search | no | Only values containing this substring (case-insensitive) |
filters[sub_N] | no | Narrow by other tags: filters[sub_1]=fb-ua-creo1 - only visits with this sub_1. Several are allowed |
pwa_id | no | Only one PWA: its visits and its conversions. Someone else's or a non-existent one - 404 |
Example: which ad sets (sub_2) work inside the creative fb-ua-creo1 (sub_1).
curl -G "https://panel.example.com/api/stats/subs" \
-H "Authorization: Bearer YOUR_KEY" \
-H "Accept: application/json" \
--data-urlencode "date_from=2026-09-29" \
--data-urlencode "date_to=2026-10-05" \
--data-urlencode "slot=2" \
--data-urlencode "filters[sub_1]=fb-ua-creo1"<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer YOUR_KEY', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$response = $api->get('stats/subs', ['query' => [
'date_from' => '2026-09-29',
'date_to' => '2026-10-05',
'slot' => 2,
'filters' => ['sub_1' => 'fb-ua-creo1'],
]]);
$subs = json_decode($response->getBody(), true)['data']['subs'];
foreach ($subs as $sub) {
$value = $sub['value'] ?? '(empty)';
echo "{$value}: {$sub['installs']} installs, CR {$sub['cr']}%\n";
}# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer YOUR_KEY", "Accept": "application/json"}
response = requests.get(f"{API}/stats/subs", headers=HEADERS, params={
"date_from": "2026-09-29",
"date_to": "2026-10-05",
"slot": 2,
"filters[sub_1]": "fb-ua-creo1",
}, timeout=30)
subs = response.json()["data"]["subs"]
for sub in subs:
print(sub["value"] or "(empty)", sub["installs"], "installs, CR", sub["cr"])// Node.js 18+, no dependencies; save as script.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer YOUR_KEY', Accept: 'application/json', 'Content-Type': 'application/json' };
const params = new URLSearchParams({
date_from: '2026-09-29',
date_to: '2026-10-05',
slot: 2,
'filters[sub_1]': 'fb-ua-creo1',
});
const response = await fetch(`${API}/stats/subs?${params}`, {
method: 'GET',
headers,
signal: AbortSignal.timeout(30000),
});
const { data } = await response.json();
for (const sub of data.subs) {
console.log(sub.value || '(empty)', sub.installs, 'installs, CR', sub.cr);
}Response
{
"data": {
"slot": 2,
"pwa": null,
"subs": [
{ "value": "adset-7", "visits": 5210, "installs": 980, "conversions": 51, "cr": 5.2 },
{ "value": null, "visits": 1200, "installs": 140, "conversions": 3, "cr": 2.1 }
]
}
}Values are sorted by installs, best first. value: null - visits where this tag is empty. conversions are deposits, cr - deposits ÷ installs.
A key in filters can only be sub_1 … sub_20.
Errors
| Code | message | When |
|---|---|---|
422 | The date from field is required. (and 1 more error) | No period passed |
422 | The date from field must be a valid date. | The date isn't recognised |
422 | The date to field must be a date after or equal to date from. | date_to is earlier than date_from |
422 | The date to field must be a date before or equal to 2026-02-01. | The period is longer than a month (the text gives the last allowed day) |
422 | The slot field is required. | slot not passed |
422 | The slot field must not be greater than 20. | slot is greater than 20 |
422 | The limit field must not be greater than 100. | limit is greater than 100 |
422 | Filter key must be sub_1..sub_20. | A key in filters is not sub_1 … sub_20 |
404 | Not found | pwa_id of someone else's or a non-existent PWA |