# Reports

Part of the [Shopdesk Commerce API reference](https://www.shopdeskbd.com/developers/reference.md). Paths below are shown exactly as the server routes them — prefix them with `https://api.shopdeskbd.com`. Authenticate every request with `Authorization: Bearer shopdesk_sk_YOUR_API_KEY`; each operation names the scope its key must hold. Every failure uses the one error envelope — see [Errors](https://www.shopdeskbd.com/developers/errors.md).

## Get a sales summary for a window

`GET /ext/v1/reports/sales-summary` · Requires scope `reports:read`

Orders, gross and net sales, net revenue and average order value for the orders DELIVERED in the window, the money received (payments settled in the window) and the cash on delivery still to collect (as of today) — each figure the same number the Reports pages in the dashboard show, with its basis stated in `basis`. Available on every plan with API access; `compare=previous_period` needs Advanced Reports. The window spans at most 366 days; answers are cached for 5 minutes. A call counts as 5 reads against the key's read limit. Company-scoped keys must pass `store_id`.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `period` | `string` | No | A named window instead of `from`/`to`, resolved on the store's own calendar: `today`, `yesterday`, `last_7_days` (today and the six days before), `last_30_days`, `this_month` (the 1st to today) or `last_month`. The response echoes the concrete `from` and `to`. Not combinable with `from` or `to`. One of: `today`, `yesterday`, `last_7_days`, `last_30_days`, `this_month`, `last_month`. |
| `from` | `string` | No | First day of the window (inclusive, a store-local calendar day). Defaults to the first day of the current month. |
| `to` | `string` | No | Last day of the window (inclusive). Defaults to today. The window may span at most 366 days. |
| `store_id` | `uuid` | No | The store to report on. REQUIRED with a company-scoped key (a report answers for one store); with a store-scoped key it may only repeat the key's own store. |
| `channel` | `string` | No | Only this sales channel (its SLUG, from `GET /channels`). With a channel filter, `money_received` and `cod_outstanding` are null: payments are not split by channel. |
| `compare` | `string` | No | `previous_period` adds `previous_period`: the same figures for the window of equal length just before. Needs a plan with Advanced Reports. One of: `previous_period`. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS "https://api.shopdeskbd.com/ext/v1/reports/sales-summary" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/reports/sales-summary");

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();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.shopdeskbd.com/ext/v1/reports/sales-summary",
    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**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/reports/sales-summary');
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']}");
}
```

### Responses

- `200` — The summary.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — `FORBIDDEN_SCOPE`: the key's scopes do not cover this endpoint. `PLAN_FEATURE_REQUIRED`: `compare` was asked for on a plan without Advanced Reports.
- `404` — Store not found (a `store_id` outside this key).
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "from": "2026-09-01",
  "to": "2026-09-30",
  "orders": 42,
  "gross_sales": "1250.00",
  "net_sales": "1250.00",
  "net_revenue": "1250.00",
  "average_order_value": "1250.00",
  "money_received": "1250.00",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "time_zone": "Asia/Dhaka",
  "channel": null,
  "basis": {
    "sales": "delivered",
    "money_received": "settled",
    "cod_outstanding": "as_of_today"
  },
  "cod_outstanding": {
    "as_of": "2026-09-29",
    "amount": "1250.00",
    "orders": 11,
    "not_shipped": {
      "orders": 3,
      "amount": "1250.00"
    },
    "with_courier": {
      "orders": 3,
      "amount": "1250.00"
    },
    "delivered_unpaid": {
      "orders": 3,
      "amount": "1250.00"
    }
  },
  "previous_period": {
    "from": "2026-09-01",
    "to": "2026-09-30",
    "orders": 42,
    "gross_sales": "1250.00",
    "net_sales": "1250.00",
    "net_revenue": "1250.00",
    "average_order_value": "1250.00",
    "money_received": "1250.00"
  }
}
```

## List the top products for a window

`GET /ext/v1/reports/top-products` · Requires scope `reports:read`

Product variants ranked by the revenue of units delivered in the window and KEPT (returns netted) — the Products report ranking in the dashboard, with revenue and units only: no cost, profit or margin is ever returned. Needs a plan with Advanced Reports. The window spans at most 366 days; answers are cached for 5 minutes. A call counts as 5 reads against the key's read limit. Company-scoped keys must pass `store_id`.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `period` | `string` | No | A named window instead of `from`/`to`, resolved on the store's own calendar: `today`, `yesterday`, `last_7_days` (today and the six days before), `last_30_days`, `this_month` (the 1st to today) or `last_month`. The response echoes the concrete `from` and `to`. Not combinable with `from` or `to`. One of: `today`, `yesterday`, `last_7_days`, `last_30_days`, `this_month`, `last_month`. |
| `from` | `string` | No | First day of the window (inclusive, a store-local calendar day). Defaults to the first day of the current month. |
| `to` | `string` | No | Last day of the window (inclusive). Defaults to today. The window may span at most 366 days. |
| `store_id` | `uuid` | No | The store to report on. REQUIRED with a company-scoped key (a report answers for one store); with a store-scoped key it may only repeat the key's own store. |
| `limit` | `number` (default `10`, max 50) | No | How many products to rank. |

### Request

**cURL**

```bash
export SHOPDESK_API_KEY="shopdesk_sk_YOUR_API_KEY"

curl -sS "https://api.shopdeskbd.com/ext/v1/reports/top-products?limit=25" \
  -H "Authorization: Bearer $SHOPDESK_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.shopdeskbd.com/ext/v1/reports/top-products");
Object.entries({
  limit: "25",
}).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 { data } = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.shopdeskbd.com/ext/v1/reports/top-products",
    params={
        "limit": "25",
    },
    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']}")

page = response.json()
rows = page["data"]
```

**PHP**

```php
<?php

$key = getenv('SHOPDESK_API_KEY');

$ch = curl_init('https://api.shopdeskbd.com/ext/v1/reports/top-products?limit=25');
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']}");
}

$rows = $response['data'];
```

### Responses

- `200` — The ranking.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — `FORBIDDEN_SCOPE`: the key's scopes do not cover this endpoint. `PLAN_FEATURE_REQUIRED`: the plan does not include Advanced Reports.
- `404` — Store not found (a `store_id` outside this key).
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "from": "2026-09-01",
  "to": "2026-09-30",
  "time_zone": "Asia/Dhaka",
  "basis": {
    "revenue": "delivered_kept"
  },
  "data": [
    {
      "product_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "variant_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "product_name": "Classic Cotton T-Shirt",
      "variant_name": "Red / M",
      "sku": "TSHIRT-RED-M",
      "units_sold": 18,
      "units_returned": 2,
      "revenue": "1250.00"
    }
  ]
}
```
