# Usage

Two endpoints to see what your store has consumed and project what you'll need next month.

## `GET /v1/usage`

Current calendar month, grouped by endpoint type.

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

### Response 200

```
{

  "data": {

    "period": "2026-04",

    "tier":   "enterprise",

    "usage": {

      "request": { "total": 31, "billable": 0 },

      "signup":  { "total": 3,  "billable": 2 }

    },

    "limits": {

      "requests_per_month":  -1,

      "signups_per_month":   -1,

      "webhooks_per_month":  -1,

      "requests_per_minute": 600

    }

  }

}
```

### Field reference

| Field                    | Meaning                                                                                                                                       |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `period`                 | Always `YYYY-MM` — the current month in **Africa/Algiers time (UTC+01:00, no DST)**. Totals are month-to-date, counted from the 1st at 00:00. |
| `tier`                   | Your current rate-limit tier — always `enterprise` (the API is Enterprise-only).                                                              |
| `usage.<group>.total`    | All requests in that group, including duplicates / rejections.                                                                                |
| `usage.<group>.billable` | Only counted requests (e.g. duplicate signups don't bill).                                                                                    |
| `limits`                 | The effective limits — `tier_limits` overridden by any per-store override. `-1` means unlimited.                                              |

`usage` is **sparse** — only groups with recorded activity this calendar month appear (in practice `request`, plus `signup` / `event` for public-key traffic). If the store has no usage at all, `usage` serialises as an empty JSON **array** `[]`, not an object. Default missing groups to zero client-side and tolerate `[]`; a strictly-typed deserialiser will otherwise fail.

### Endpoint groups

| Group     | What counts                                                                                                                                                                                                                                                                                                                                                                                                 |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `request` | Every call that passed key authentication and the rate limits, counted **before** the request is processed, so 4xx/5xx responses and scope rejections count too; calls refused with `429` or `402` do not. Calls made on your store by Copilot, a connected AI assistant (Claude or ChatGPT) or an installed app count here too. Always `billable: 0`. `GET /v1/ping` is unauthenticated and never counted. |
| `signup`  | Each `/v1/signups` call. Duplicates are counted in `total` but not `billable`.                                                                                                                                                                                                                                                                                                                              |
| `event`   | Each `/v1/events` call. Only newly-recorded events are counted — duplicates (same store + nonce) are dropped and appear in neither `total` nor `billable`. This differs from `signup`.                                                                                                                                                                                                                      |
| `webhook` | Reserved. Outbound deliveries are **not** metered at v1, so this group never appears in the response.                                                                                                                                                                                                                                                                                                       |

## `GET /v1/usage/history`

Hourly rollups over a date range — useful for charts and trend analysis.

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

### Query parameters

| Param  | Type     | Default    | Notes     |
| ------ | -------- | ---------- | --------- |
| `from` | ISO date | 7 days ago | Inclusive |
| `to`   | ISO date | now        | Exclusive |

Range cap: 90 days. `from` is inclusive and `to` is exclusive, and both are floored to the top of the hour for the *query* — the `from` / `to` echoed in the response are your inputs as parsed, unfloored. Common date and date-time formats are accepted (`2026-04-01`, `2026-04-01T12:00:00Z`, `-7 days`, …); an unparseable value or `to` earlier than `from` returns `400 bad_request` ("from/to must be valid date strings, to >= from"), and a range over 90 days returns `400` ("range too large (max 90 days)").

`period_hour` buckets are whole hours, stamped in Africa/Algiers time (UTC+01:00) at the moment each call is counted.

### Errors

| HTTP | Code          | Cause                                            |
| ---- | ------------- | ------------------------------------------------ |
| 400  | `bad_request` | "from/to must be valid date strings, to >= from" |
| 400  | `bad_request` | "range too large (max 90 days)"                  |
| 403  | `forbidden`   | "Missing scope: usage<!-- -->:read<!-- -->"      |

`usage:read` is in the default scope set of keys you generate in the dashboard. A key minted through `POST /v1/keys` carries it only when the calling key does, and an installed app's token can never hold it, so an app gets `403` on these two endpoints and on `/v1/quotas`.

### Request

```
curl 'https://api.dzbuild.app/v1/usage/history?from=2026-04-01&to=2026-05-01' \

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

### Response 200

```
{

  "data": {

    "from": "2026-04-01T00:00:00+01:00",

    "to":   "2026-05-01T00:00:00+01:00",

    "rows": [

      { "period_hour": "2026-04-30 19:00:00", "endpoint_group": "request", "count": 26, "billable_count": 0 },

      { "period_hour": "2026-04-30 20:00:00", "endpoint_group": "request", "count": 5,  "billable_count": 0 },

      { "period_hour": "2026-04-30 20:00:00", "endpoint_group": "signup",  "count": 3,  "billable_count": 2 }

    ]

  }

}
```

Rows are returned in ascending `period_hour` order. Hours with zero usage in any group are omitted (sparse).

## Plotting tips

* **Daily aggregates:** group rows by the first 10 characters of `period_hour` (the date prefix).
* **Stacked area:** group by `endpoint_group`, then by hour for the X-axis.
* **Quota burn rate:** divide `signup.billable_count` cumulative by the elapsed fraction of the month, project to month-end.

A simple Python example:

```
import collections, datetime, requests, os



r = requests.get('https://api.dzbuild.app/v1/usage/history',

    params={'from': '2026-04-01', 'to': '2026-05-01'},

    headers={'Authorization': f"Bearer {os.environ['DZ_KEY']}"})

rows = r.json()['data']['rows']



by_day = collections.defaultdict(lambda: collections.Counter())

for row in rows:

    day = row['period_hour'][:10]

    by_day[day][row['endpoint_group']] += row['count']



for day, counts in sorted(by_day.items()):

    print(day, dict(counts))
```
