# Analytics

`GET /v1/analytics` returns the figures the dashboard home page shows for a period: ten KPIs, each with its value for a comparison period and the change, and eight chart series. Use it for client reports or a BI export instead of rebuilding the numbers from `GET /v1/orders`. The API and the dashboard share the same definitions and the same cache, so for the same period they show the same numbers. What each figure means for the merchant is explained on the [Analytics](https://dzbuild.com/docs/marketing/analytics.md) page of the merchant docs.

The API always counts all traffic: store pages and landing pages together. The dashboard's traffic filter (landing pages only, or the store only) has no equivalent here.

## Before you start

* The key needs `analytics:read`. Keys created from the dashboard (**Settings → API**, `/dashboard/api`) carry it since v1.7. **Scopes are frozen when a key is created**, so an older key answers `403 forbidden` with "Missing scope: analytics
  <!-- -->
  :read
  <!-- -->
  ": create a new key from the dashboard. A key created through `POST /v1/keys` only gets the scopes held by the key that created it.
* Personal keys need a store on an active Enterprise plan. An installed app's token is not bound to Enterprise, but it needs `analytics:read` among the scopes the merchant approved. See [Introduction](https://dzbuild.com/api-docs/intro.md).
* Revenue, profit and average order value, and the `revenue` values inside the charts, are sent only when the key belongs to the store owner's account. Otherwise those fields are missing from the answer: they are not set to `0` or `null`.

| Scope            | Description                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `analytics:read` | Read store analytics and KPIs. Included in merchant keys created from v1.7 on; an older key needs a new key. |

## `GET /v1/analytics`

The report for one period.

**Auth:** platform key with `analytics:read`.

### Query parameters

| Param   | Type         | Default      | Notes                                                                                                                              |
| ------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `range` | string       | `this_month` | `today`, `yesterday`, `7d`, `30d`, `this_month`, `last_month`, `this_year` or `custom`. Any other value returns `400 bad_request`. |
| `from`  | `YYYY-MM-DD` | none         | First day. Required with `range=custom`, ignored otherwise.                                                                        |
| `to`    | `YYYY-MM-DD` | none         | Last day. Required with `range=custom`, ignored otherwise. Never before `from`, at most 365 days after it.                         |

### Periods

| `range`      | Period                                       | Compared with                              | `group_by`               |
| ------------ | -------------------------------------------- | ------------------------------------------ | ------------------------ |
| `today`      | Today                                        | Yesterday                                  | `hour`                   |
| `yesterday`  | Yesterday                                    | The day before                             | `hour`                   |
| `7d`         | Today and the 6 days before                  | The 7 days before those                    | `day`                    |
| `30d`        | Today and the 29 days before                 | The 30 days before those                   | `day`                    |
| `this_month` | The 1st to the last day of the current month | The whole previous month                   | `day`                    |
| `last_month` | The whole previous month                     | The month before it                        | `day`                    |
| `this_year`  | 1 January to 31 December of the current year | The whole previous year                    | `month`                  |
| `custom`     | `from` to `to`, both days included           | The same number of days just before `from` | `hour`, `day` or `month` |

For `custom`, `group_by` is `hour` when `to` is `from` or the next day, `day` when `to` is at most 90 days after `from`, and `month` beyond that. `this_month` and `this_year` run to the end of the month or the year, so they compare the days so far with a whole previous month or year.

The report is cached for 120 seconds per store and period, and the dashboard reads the same cache. A call can return figures up to two minutes old, and calls inside that window get the same numbers.

### Request

```
curl 'https://api.dzbuild.app/v1/analytics?range=7d' \

  -H "Authorization: Bearer $DZ_KEY"
```

A custom period:

```
curl 'https://api.dzbuild.app/v1/analytics?range=custom&from=2026-09-01&to=2026-09-30' \

  -H "Authorization: Bearer $DZ_KEY"
```

### Response 200

The answer for `range=7d` on 6 October 2026, to a key created by the store owner. The time series and the lists are cut to their first entries.

```
{

  "data": {

    "period": {

      "from": "2026-09-30 00:00:00",

      "to": "2026-10-06 23:59:59",

      "prev_from": "2026-09-23 00:00:00",

      "prev_to": "2026-09-29 23:59:59",

      "label": "7d",

      "group_by": "day"

    },

    "kpis": {

      "total_orders": { "value": 48, "change": 20, "previous": 40 },

      "delivered_orders": { "value": 31, "change": 7, "previous": 29 },

      "cancelled_orders": { "value": 6, "change": -25, "previous": 8 },

      "total_revenue": { "value": 186000, "change": 8, "previous": 172000 },

      "total_profit": { "value": 61500, "change": 6, "previous": 58000 },

      "avg_order_value": { "value": 4350, "change": -1, "previous": 4400 },

      "total_visitors": { "value": 1520, "change": 17, "previous": 1300 },

      "page_views": { "value": 4810, "change": 17, "previous": 4100 },

      "conversion_rate": { "value": 3.2, "change": 3, "previous": 3.1 },

      "new_customers": { "value": 41, "change": 17, "previous": 35 }

    },

    "charts": {

      "revenue_over_time": [

        { "label": "09/30", "orders": 7, "revenue": 24500 },

        { "label": "10/01", "orders": 5, "revenue": 18000 }

      ],

      "orders_by_hour": [

        { "hour": "00:00", "orders": 0, "impressions": 35 },

        { "hour": "01:00", "orders": 1, "impressions": 22 }

      ],

      "orders_by_status": {

        "pending": 5,

        "confirmed": 4,

        "processing": 2,

        "shipped": 9,

        "delivered": 20,

        "cancelled": 6,

        "returned": 2

      },

      "top_products": [

        {

          "id": 12,

          "name": "Classic watch",

          "image": "https://cdn.dzbuild.app/uploads/products/123/123_1700000000_example.webp",

          "qty_sold": 14,

          "revenue": 49000,

          "views": 380

        }

      ],

      "visitors_over_time": [

        { "label": "09/30", "page_views": 690, "unique_visitors": 215, "orders": 7 },

        { "label": "10/01", "page_views": 702, "unique_visitors": 230, "orders": 5 }

      ],

      "devices": {

        "desktop": { "count": 510, "percent": 11 },

        "mobile": { "count": 4180, "percent": 87 },

        "tablet": { "count": 120, "percent": 2 }

      },

      "traffic_sources": [

        { "source": "facebook", "count": 2900, "percent": 60 },

        { "source": "direct", "count": 1210, "percent": 25 },

        { "source": "tiktok", "count": 700, "percent": 15 }

      ],

      "top_wilayas": [

        { "wilaya": "الجزائر", "orders": 9, "revenue": 41000 },

        { "wilaya": "وهران", "orders": 6, "revenue": 27500 }

      ]

    }

  }

}
```

### The period

| Field                  | Meaning                                                                                |
| ---------------------- | -------------------------------------------------------------------------------------- |
| `from`, `to`           | Start and end of the period, `YYYY-MM-DD HH:MM:SS`, Algiers time.                      |
| `prev_from`, `prev_to` | Start and end of the comparison period.                                                |
| `label`                | The `range` you asked for.                                                             |
| `group_by`             | Bucket size of `revenue_over_time` and `visitors_over_time`: `hour`, `day` or `month`. |

### KPIs

Each KPI has three numbers: `value` for the period, `previous` for the comparison period, and `change`, the percent change rounded to a whole number. When `previous` is `0` or below, `change` is `100` if `value` is above `0`, and `0` otherwise. Amounts are rounded to whole numbers.

| KPI                | What it counts                                                                                                                          |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `total_orders`     | Orders created in the period, whatever their status.                                                                                    |
| `delivered_orders` | Orders delivered in the period, counted on the day they were delivered.                                                                 |
| `cancelled_orders` | Orders created in the period that are now cancelled.                                                                                    |
| `total_revenue`    | Owner only. Subtotal minus discount of the orders delivered in the period. Shipping and payment fees are not included.                  |
| `total_profit`     | Owner only. `total_revenue` minus quantity × cost price of the items in those orders. A product with no cost price counts as zero cost. |
| `avg_order_value`  | Owner only. Average order total of the orders created in the period, cancelled and returned orders left out.                            |
| `total_visitors`   | Distinct visitors in the period, store pages and landing pages together.                                                                |
| `page_views`       | Page views in the period, store pages and landing pages together.                                                                       |
| `conversion_rate`  | `total_orders` ÷ `total_visitors` × 100, with one decimal. `0` when there were no visitors.                                             |
| `new_customers`    | Customer records created in the period.                                                                                                 |

These KPIs come from different sets of orders, so they do not add up: `total_revenue` ÷ `total_orders` is not `avg_order_value`.

### Charts

| Chart                | Content                                                                                                                                                                                                                                                                                                                 |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `revenue_over_time`  | One entry per bucket: `label`, the `orders` created in the bucket and, for the owner only, `revenue`, the order totals of those orders that are now delivered. It is not `total_revenue`, which counts by delivery day and leaves shipping out.                                                                         |
| `orders_by_hour`     | Always 24 entries, `00:00` to `23:00`: the orders created and the page views (`impressions`) at that hour of the day, over the whole period.                                                                                                                                                                            |
| `orders_by_status`   | Orders created in the period by current status: `pending`, `confirmed`, `processing`, `shipped`, `delivered`, `cancelled` and `returned`.                                                                                                                                                                               |
| `top_products`       | Up to 10 products by quantity sold in the orders created in the period, cancelled and returned orders left out. Each has `id`, `name`, `image` (main image URL or `null`), `qty_sold`, `views` (distinct visitors on the product in the period) and, for the owner only, `revenue` (quantity × the price on the order). |
| `visitors_over_time` | One entry per bucket: `label`, `page_views`, `unique_visitors` and `orders`. A visitor who returns on another day counts in both buckets, so the `unique_visitors` values can add up to more than `total_visitors`.                                                                                                     |
| `devices`            | Page views by device: `desktop`, `mobile` and `tablet` keys, each with `count` and `percent`. A device with no views has no key, and the field is an empty array `[]` when the period had no visits.                                                                                                                    |
| `traffic_sources`    | Up to 6 sources by page views, busiest first, each with `source`, `count` and `percent`. `percent` is the share among the listed sources.                                                                                                                                                                               |
| `top_wilayas`        | Up to 10 wilayas by number of orders created in the period, all statuses: `wilaya` (the Arabic name, `غير محدد` for orders with no wilaya), `orders` and, for the owner only, `revenue`, the order totals of those orders whatever their status.                                                                        |

Labels follow `group_by`: `00:00` to `23:00` for `hour` (24 entries; a two-day `custom` period adds both days into the same hours), `MM/DD` for `day`, and the English month and year for `month`, such as `Sep 2026`. Days after today and months after the current one are left out.

The platform records `source` as `direct`, `facebook`, `instagram`, `tiktok`, `google`, `youtube`, `twitter`, `snapchat`, `telegram` or `other`. A visit whose link carries `utm_source` is classed by that tag (`fb` counts as `facebook`, `ig` as `instagram`, `x` as `twitter`, any tag not in the list as `other`); a visit without it is classed by the site it came from.

### Errors

| HTTP | Code             | Cause                                                                                                                                                                                                                                       |
| ---- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `bad_request`    | `range` is not one of the eight values ("Invalid range. Allowed: ..."), or a `custom` period whose `from` or `to` is missing or not `YYYY-MM-DD`, or whose `to` is before `from` or more than 365 days after it ("Invalid date range ..."). |
| 401  | `unauthorized`   | Bad or missing key.                                                                                                                                                                                                                         |
| 402  | `quota_exceeded` | The store's monthly request quota is used up. See [Rate limits](https://dzbuild.com/api-docs/rate-limits.md).                                                                                                                               |
| 403  | `forbidden`      | "Missing scope: analytics<!-- -->:read<!-- -->", or a personal key whose store is not on an active Enterprise plan ("API access requires an active Enterprise plan").                                                                       |
| 429  | `rate_limited`   | The store's per-minute cap, shared by all of its keys, or the per-install cap of an installed app's token. Wait for `Retry-After`. See [Rate limits](https://dzbuild.com/api-docs/rate-limits.md).                                          |

## Known limits

* **Month ends.** When today's day number does not exist in the previous month, as on 31 October, `range=last_month` returns the current month and `range=this_month` is compared with the current month itself. `last_month` is also compared with itself when the day does not exist two months back, as on 30 April. On those days, ask for the dates you need with `range=custom`.
