# GET /v1/metrics/{metric}

The read API: the same numbers the dashboard shows, for your own pricing
pages, admin panels, and investor updates.

```sh
curl "https://api.datagauge.dev/v1/metrics/mau?date_from=2026-06-19&date_to=2026-07-18" \
  -H "Authorization: Bearer $DATAGAUGE_KEY"
```

Two credentials can read. An `sk_live_` key is scoped to one app, so the
project is implied. A `dg_live_` [control token](/docs/api/apps) with the
`metrics:read` scope is org-scoped and reads any app in the org, so it must
name one: add `?project=proj_...`. That is the app's `public_id` from
[`GET /v1/apps`](/docs/api/apps#one-identifier-three-names), the same value the
billing webhook path calls `{project_id}`, and not the app's `app_id`.
Public `pk_live_` keys can never read.

Both credentials are plane-confined. An `sk_test_` key reads its app's test
plane; a `dg_test_` token reads only test projects (name one with
`?project=<test_public_id>`). A `dg_live_` token reads only live projects; a
project in the other plane is `404 unknown project`, the same as one outside
your org.

| Metric | Returns | Window |
|---|---|---|
| `mau` | distinct active users | inclusive dates, default last 30 days |
| `dau` | daily series `[{day, dau}]` | same |
| `signups` | distinct users who signed up | same |
| `revenue` | `[{day, currency, revenue}]`, minor units, **earned only** | same |
| `volume` | `[{day, currency, volume}]`, marketplace gross processed | same |
| `mrr` | `[{currency, mrr}]`, current | windowless |

Distinct counts (`mau`, `dau`, `signups`) are HyperLogLog-precise (~0.5% at
scale) and immune to duplicate events by construction. Money endpoints are
exact.

## How far behind ingest these numbers are

**A write is not readable here for about five minutes.** Two delays stack, and
neither is a fault:

| Stage | Delay |
|---|---|
| Ingest batches before it stores. A trickle sender's batch waits out the timeout rather than filling. | up to **30 s** |
| Metric reads are cached at the edge (`x-cache: hit\|miss`). | **300 s** |

The cache is the one that surprises people, because it latches: read a metric
one second after writing and you pin the pre-write answer for the next five
minutes, however many times you ask.

This matters most when you are learning the [reserved
names](/docs/api/events): someone checking whether `payment` really needs
`props.amount` sends one, reads back zero revenue, and cannot tell a rejected
event from a queued one. Read the ingest response instead: a rejected event is
`422`, or a `202` carrying a non-empty `errors[]`, and it says so at the moment
you send it rather than five minutes later.

If the upstream is rate-limited or unavailable we serve the last known value
rather than fail: `x-cache: stale`, with `x-cache-age` giving its age in
seconds.

## When a read is refused: `429`

Two different ceilings can refuse a read, and both answer `429`. They are not the
same event and the right response to them is opposite, so branch on
`x-datagauge-throttle` rather than on the status code.

| `x-datagauge-throttle` | What happened | What to do |
|---|---|---|
| `rate_limit` | Your plan's per-minute read ceiling. | Retry. `Retry-After` gives the exact seconds to wait, computed from the oldest request still inside the window, not a fixed backoff. |
| `allowance:queries` | The org's agent-query allowance for the billing period is spent. | **Do not retry**: it cannot succeed until the period rolls over. `x-datagauge-reset` is the ISO 8601 instant it returns. |
| `allowance:raw_scans` | The sub-limit on the three metrics that scan raw events (`event_names`, `event_count`, `event_series`) is spent. | Stop asking for those three; `x-datagauge-reset` again says when they return. Every other metric is still served. |

`Retry-After` is present on `rate_limit` and deliberately absent from an
allowance refusal, where the honest value would be days. The body carries the
same facts for a client that parses bodies:

```json
{ "ok": false, "reason": "allowance", "scope": "queries",
  "reset_at": "2026-08-01T00:00:00.000Z",
  "error": "agent query allowance exhausted for this billing period" }
```

The per-minute ceiling is **per organization**: an `sk_live_` key's traffic and a
`dg_live_` token's traffic add up against the same figure, and issuing more
credentials of either kind does not raise it. A cache hit (`x-cache: hit`) never
consults either limit.

Only `dg_live_` reads spend the period allowance; that is the agent surface, and
the [MCP server](/docs/api/mcp) is one of its clients, so a tool call and a direct
token read cost the same thing. `sk_live_` reads and your own dashboard never
spend it.

Treat an `x-datagauge-throttle` value you do not recognize as a bare `429`. The
three above are the whole vocabulary; matching a new label by prefix is how a
client ends up retrying something it should have stopped on.

A machine-readable description of the whole API (this read plane plus the
ingest endpoints) lives at
[`https://datagauge.dev/openapi.json`](https://datagauge.dev/openapi.json)
(OpenAPI 3.1), on the docs site rather than on `api.datagauge.dev`. The `429` is
described there too, with the header vocabulary as an `enum`.
