GET
/
api
/
v1
/
admin
/
analytics
/
cohorts
Admin analytics cohorts
curl --request GET \
  --url https://api.example.com/api/v1/admin/analytics/cohorts
import requests

url = "https://api.example.com/api/v1/admin/analytics/cohorts"

response = requests.get(url)

print(response.text)
const options = {method: 'GET'};

fetch('https://api.example.com/api/v1/admin/analytics/cohorts', 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.example.com/api/v1/admin/analytics/cohorts",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);

$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.example.com/api/v1/admin/analytics/cohorts"

req, _ := http.NewRequest("GET", url, nil)

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.example.com/api/v1/admin/analytics/cohorts")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/api/v1/admin/analytics/cohorts")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)

response = http.request(request)
puts response.read_body

Overview

Per-day product analytics for the admin dashboard’s “trend” charts — DAU, MAU, new users, trial→paid conversions, and 30-day churn. Unlike the overview, these metrics are not computed live; they are read straight from the analytics_daily collection populated by the nightly rollup worker at 02:00 UTC. Pass from/to (both YYYY-MM-DD, inclusive bounds) to bound the range. When neither is supplied the server returns the trailing 30 days (today inclusive). See Analytics and rollups for the rollup pipeline + staleness contract.
Available to any admin role — SUPER, MODERATOR, and FINANCE all read this surface.

Authentication

Bearer <accessToken> with scope: 'admin' required (requireAdmin).

Path parameters

None.

Query parameters

NameTypeRequiredNotesExample
fromstringnoInclusive start date, YYYY-MM-DD. Defaults to 29 days before to (so the response is exactly 30 days).2026-04-24
tostringnoInclusive end date, YYYY-MM-DD. Defaults to today (UTC).2026-05-23
Days for which the rollup has not yet produced a row are simply absent from items — there is no zero-fill. The most recent day in items is generally yesterday (the rollup runs 02:00 UTC and computes the previous day).

Request body

None.

Response — 200 OK

FieldTypeNotes
itemsarrayOne entry per analytics_daily row in the range. Order matches the underlying repo (chronological).

Cohort row

FieldTypeNotesExample
datestringUTC date the row covers, YYYY-MM-DD.2026-05-22
dauintegerDaily Active Users — users with lastSeenAt inside the 24h window ending at midnight UTC the next day.247
mauintegerMonthly Active Users — users with lastSeenAt inside the trailing 30-day window.1432
newUsersintegerusers whose createdAt falls inside the day.18
trialToPaidintegerSubscriptions whose status is ACTIVE AND whose trialEndsAt falls in the trailing 30 days from the row’s date. Captures the trial-end conversion.9
churn30dintegerSubscriptions in CANCELLED/EXPIRED whose updatedAt falls in the trailing 30 days. Proxy metric — see limitations.4
{
  "items": [
    {
      "date": "2026-05-21",
      "dau": 241,
      "mau": 1418,
      "newUsers": 22,
      "trialToPaid": 8,
      "churn30d": 4
    },
    {
      "date": "2026-05-22",
      "dau": 247,
      "mau": 1432,
      "newUsers": 18,
      "trialToPaid": 9,
      "churn30d": 4
    }
  ]
}

MVP limitations

  • churn30d uses updatedAt as a proxy for the cancellation timestamp because the subscriptions collection has no dedicated statusChangedAt field. False positives are possible if a CANCELLED/EXPIRED row was touched for an unrelated reason (e.g. a tax metadata refresh). Acceptable for the dashboard; not acceptable for accounting.
  • trialToPaid captures TRIALING → ACTIVE transitions but does not count trial drops (TRIALING → CANCELLED). That is correct by definition — those users did not convert — but flagged here so the metric is not misread as “trial outcomes.”

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDfrom or to not in YYYY-MM-DD form.
401UNAUTHENTICATEDMissing, malformed, expired, or non-admin-scope token.

See also

curl

curl "https://api.swappr.co.uk/api/v1/admin/analytics/cohorts?from=2026-04-24&to=2026-05-23" \
  -H "Authorization: Bearer $ADMIN_ACCESS_TOKEN"

Postman

See docs/postman/swappr.postman_collection.jsonAdmin Analytics → Cohorts.