Analytics API
Overview
Two read-only endpoints back the widgets on the admin dashboard. They are the only sales aggregates exposed over REST — everything else about orders is read through GraphQL.
Both are declared access: "private" in route.json, so they require an admin credential (a Bearer admin JWT or an admin session cookie), and both aggregate over the entire order table with no store, channel or date scoping beyond what is described below.
data envelopeEvery other EverShop REST endpoint answers {"data": ...}. These two call response.json(...) directly with the aggregate: /api/lifetimesales returns a bare object and /api/salestatistic returns a bare array. Client code that unwraps .data will read undefined.
Endpoints
Get Lifetime Sales
Returns four headline numbers computed over every row in the order table. There are no query parameters and no request body — any query string is ignored.
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/lifetimesales
fetch('https://<your domain>/api/lifetimesales', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
}
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
{
"orders": 184,
"total": "$41,207.55",
"completed_percentage": 62,
"cancelled_percentage": 0
}
Response Properties
| Property | Type | Description |
|---|---|---|
orders | integer | Total number of order rows. No status filter — carts that became orders count even if payment never completed. |
total | string | Sum of grand_total across all orders, already formatted as currency. Not a number. |
completed_percentage | integer | Percentage of orders where payment_status = 'paid' and shipment_status = 'delivered', rounded to the nearest integer. 0 when there are no orders. |
cancelled_percentage | integer | Percentage of orders where payment_status = 'cancelled' and shipment_status = 'cancelled', rounded. 0 when there are no orders. |
total is formatted server-side with Intl.NumberFormat, using the store currency from settings and the locale from the shop.language config key (default en). A store configured for de and EUR gets "41.207,55 €", not "$41,207.55". Parse it at your own risk — if you need a number, sum Order.grandTotal through GraphQL instead.
cancelled_percentage is always 0 on a stock installThe handler compares against the string 'cancelled' (two ls). EverShop's registered status code is canceled (one l) — the spelling used by cancelOrder, the OMS status registry and the Stripe webhook. Nothing ever writes 'cancelled', so the counter never increments. Do not use this field to detect cancellations; derive them from Order.status / Order.paymentStatus through GraphQL.
Get Sales Statistics
Returns six consecutive time buckets of order volume and revenue, oldest first. This backs the dashboard's sales chart.
- cURL
- JavaScript
curl
-H "Accept: application/json"
-H "Authorization: Bearer <admin JWT token>"
https://<your domain>/api/salestatistic?period=monthly
fetch('https://<your domain>/api/salestatistic?period=monthly', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <admin JWT token>'
}
})
.then(response => response.json())
.then(data => {
if(data.error) {
// Handle the error
} else {
// Handle the data
}
})
.catch(error => {
// Handle the error
});
[
{
"total": "3120.5000",
"count": "14",
"time": "Mar 31"
},
{
"total": "4890.0000",
"count": "21",
"time": "Apr 30"
},
{
"total": "5102.2500",
"count": "19",
"time": "May 31"
},
{
"total": 0,
"count": "0",
"time": "Jun 30"
},
{
"total": "6740.7500",
"count": "27",
"time": "Jul 31"
},
{
"total": "2310.0000",
"count": "9",
"time": "Aug 31"
}
]
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
period | string | No | daily, weekly or monthly. Defaults to weekly. |
Bucket Windows
Six buckets are always returned, indexed oldest to newest, with the current period last.
period | Each bucket spans | Range covered |
|---|---|---|
daily | One calendar day, 00:00:00 to 23:59:59 | Today and the five days before it |
weekly | One calendar week, start-of-week to end-of-week | This week and the five weeks before it |
monthly | One calendar month, first day to last day | This month and the five months before it |
Response Properties
| Property | Type | Description |
|---|---|---|
total | string or number | SUM(grand_total) for the bucket. PostgreSQL returns numeric as a string ("3120.5000"); an empty bucket yields the number 0 instead of null. Coerce before doing arithmetic. |
count | string | COUNT(order_id) for the bucket. A bigint, so it is a string even when it is "0". |
time | string | Label for the bucket, formatted MMM DD from the bucket's end date — so a monthly bucket is labelled with the last day of the month, not the first. |
Buckets are computed purely from order.created_at between the window bounds. No status filter is applied: canceled and unpaid orders contribute to both total and count, which is why this endpoint's revenue will not match a paid-orders report.
period is not validatedOnly daily, weekly and monthly build a window. Any other value falls through all three branches and leaves the bucket bounds unset, so the request will not return usable figures. Validate the value on your side before sending it.
Building Your Own Reports
These two endpoints are hardcoded for the dashboard widgets and take no filters beyond period. For anything else — revenue by status, by product, by customer group, over an arbitrary date range — query the orders collection through GraphQL, which supports filtering, sorting and pagination. See the data fetching documentation.