Sales and reports
The sales summary, comparisons and top products, and what each figure counts.
"How were sales this week?" is the first thing a merchant asks an assistant, and the first number a BI tool wants. This guide covers the two report endpoints, what each figure counts, and which plan each needs.
You need a key with reports:read. It is flagged sensitive when you create
the key, because it reveals the store's sales.
| Endpoint | Plan |
|---|---|
GET /reports/sales-summary | Every plan with API access |
GET /reports/sales-summary?compare=previous_period | Advanced Reports |
GET /reports/top-products | Advanced Reports |
A plan without Advanced Reports gets 403 with the code
PLAN_FEATURE_REQUIRED on the last two, and the summary still works. That
code means the plan, not the key, is missing something: a key without
reports:read gets FORBIDDEN_SCOPE instead. See
Errors.
Each call to either endpoint counts as 5 reads against the key's read limit, because a report is several queries. See Rate limits.
Windows
Both endpoints take from and to: the first and last day of the window,
inclusive, as the store's calendar days (2026-09-01). With neither, the
window is the current month so far. A window spans at most 366 days. The
store's timezone is echoed as time_zone, and GET /store returns it as
timezone.
Instead of from and to, you can name the window with period: today,
yesterday, last_7_days (today and the six days before), last_30_days,
this_month (the 1st to today) or last_month. It is read on the store's
calendar, so "yesterday" is the store's yesterday, and the response echoes the
concrete from and to. Sending period with from or to is a 422.
A company-scoped key must name the store with store_id. A report answers for
one store.
Answers are cached for 5 minutes per store and window, so polling faster than that returns the same figures.
The sales summary
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/reports/sales-summary?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/reports/sales-summary");
Object.entries({
from: "2026-09-01",
to: "2026-09-30",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/reports/sales-summary",
params={
"from": "2026-09-01",
"to": "2026-09-30",
},
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
obj = response.json()<?php
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/reports/sales-summary?from=2026-09-01&to=2026-09-30');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}The figures are the same numbers the dashboard's Reports pages show for the
same window. They run on three different clocks, and basis says which:
| Field | Counts | basis |
|---|---|---|
orders | Orders delivered in the window | sales: delivered |
gross_sales | Their line value after markdowns, before coupons and delivery | sales: delivered |
net_sales | What those customers were billed for goods and delivery, excluding tax | sales: delivered |
net_revenue | net_sales less returned goods, refunds, whole-order returns and courier concessions | sales: delivered |
average_order_value | net_sales ÷ orders | sales: delivered |
money_received | Payments settled in the window, whichever order they belong to | money_received: settled |
cod_outstanding | What open orders still owe as of today: not shipped, with the courier, delivered but unpaid | cod_outstanding: as_of_today |
An order placed on the 30th and delivered on the 2nd counts in the next
month's sales, which is when the sale became final. Don't add
money_received to net_sales: they measure different things.
money_received leaves out payments on orders that are cancelled or returned
now. If an order paid in a past window is cancelled or returned later,
that window's money_received goes down, exactly as the dashboard's Payments
report does. Store a closed month's figure yourself if you need it frozen.
By channel. Pass channel (a slug from GET /channels) to count only
that channel's sales. money_received and cod_outstanding are then null,
because payments are not split by channel.
Comparing with the previous period
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/reports/sales-summary?from=2026-09-22&to=2026-09-28&compare=previous_period" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/reports/sales-summary");
Object.entries({
from: "2026-09-22",
to: "2026-09-28",
compare: "previous_period",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/reports/sales-summary",
params={
"from": "2026-09-22",
"to": "2026-09-28",
"compare": "previous_period",
},
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
obj = response.json()<?php
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/reports/sales-summary?from=2026-09-22&to=2026-09-28&compare=previous_period');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}compare=previous_period adds previous_period: the same figures for the
window of equal length that ends the day before from. A week compares with
the week before it. A 30-day month compares with the 30 days before it, not
with the previous calendar month.
Top products
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"
curl -sS "https://api.shopdeskbd.com/ext/v1/reports/top-products?from=2026-09-01&to=2026-09-30&limit=10" \
-H "Authorization: Bearer $SHOPDESK_API_KEY"const url = new URL("https://api.shopdeskbd.com/ext/v1/reports/top-products");
Object.entries({
from: "2026-09-01",
to: "2026-09-30",
limit: "10",
}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.SHOPDESK_API_KEY}`,
},
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}
const object = await response.json();import os
import requests
response = requests.get(
"https://api.shopdeskbd.com/ext/v1/reports/top-products",
params={
"from": "2026-09-01",
"to": "2026-09-30",
"limit": "10",
},
headers={
"Authorization": f"Bearer {os.environ['SHOPDESK_API_KEY']}",
},
timeout=30,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
obj = response.json()<?php
$key = getenv('SHOPDESK_API_KEY');
$ch = curl_init('https://api.shopdeskbd.com/ext/v1/reports/top-products?from=2026-09-01&to=2026-09-30&limit=10');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$response = json_decode($body, true);
if ($status >= 400) {
throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}Product variants ranked by the revenue of the units delivered in the window
that customers kept. Returned units are netted out and shown as
units_returned. limit is 1–50, default 10.
With the store MCP server
An assistant connected to the store MCP server
answers "how were sales this week?" with one tool call:
get_sales_summary takes period (or from/to), compare_to: "previous_period" and channel, and returns the same figures as
GET /reports/sales-summary with the same basis. It answers concisely by
default — the headline figures and the COD total — and with
response_format: "detailed" it returns the full REST body, stage split
included. The same plan rules apply: comparisons need Advanced Reports.
A call counts as 5 reads against the key's read limit, the same as the REST
endpoint. A comparison on a plan without Advanced Reports comes back as an
error with the code PLAN_FEATURE_REQUIRED. The ranking has no tool: a BI
sync reads GET /reports/top-products over REST.