# Read KPIs

The KPI API returns metrics about the players linked to you, computed by PlayerPal from your [activity](./sending-activity.mdx), your [facts](./fact-ingest.mdx) and your Links. Every endpoint is a read-only `GET` under `/v1/kpis`. Your organisation is taken from your credential: there is no party parameter, and you can never query another partner's numbers. Metric definitions are in [Metrics](../analytics/metrics.mdx).

## Endpoint map

The catalogue and own-tier KPIs serve operators and game providers. The overview also serves affiliates.

| Group | Endpoint | Use it when |
|---|---|---|
| **Own KPIs** | `GET /v1/kpis/catalogue` | You need the KPI IDs your party type can read |
| | `GET /v1/kpis/own` | Charting one metric by day, week or month |
| | `GET /v1/kpis/breakdown` | Ranking markets, games or counterparts for one metric |
| | `GET /v1/kpis/overview` | Showing every overview KPI with availability and trend in one call |
| **Comparison** | `GET /v1/kpis/compare` | Comparing your players with the rest of the PlayerPal network |
| | `GET /v1/kpis/compare/summary` | Every comparison KPI for a period in one call |
| | `GET /v1/kpis/benchmarks` | Comparing your totals with anonymous peers of your type |
| | `GET /v1/kpis/cohorts/retention` | Monthly first-play cohorts and a pooled network curve |
| | `GET /v1/kpis/funnels/link` | Seeing how Links turn into play |
| **Insights** | `GET /v1/kpis/intelligence/insights`, `/insights/{id}` | Ranked, explained changes and opportunities |
| | `GET /v1/kpis/intelligence/ask`, `/suggestions` | Mapping a short question to a fixed analysis |
| **Explore** | `GET /v1/kpis/explore/*` | Building detailed analytics views with filters |

Every KPI call is recorded in PlayerPal's audit log with your organisation and client. Each endpoint's parameters are in the [API reference](/api/partner).

## List your catalogue

`kpisCatalogue` (see the API reference)

Start here. The catalogue lists the own-tier KPIs your party type can read, with each KPI's `unit`, the `grains` it supports, whether it takes a `market` filter (`markets`) and whether the account floor applies (`account_floor`).

| KPI | Unit | Who |
|---|---|---|
| `own.rounds`, `own.active_accounts` | count | Operator, provider |
| `own.bet_eur`, `own.win_eur`, `own.ggr` | EUR | Operator, provider |
| `own.linked_players`, `own.new_links`, `own.unlinks` | count | Operator, provider |
| `own.link_intents`, `own.link_conversion` | count, ratio | Operator |
| `own.points_issued` | points | Operator, provider |
| `own.ingest_acceptance` | ratio | Operator, provider |
| `own.batch_latency_p95` | seconds | Provider |

(Code sample: kpi-catalogue. See the page for curl, Node.js, Python, PHP, Java and C#.)

Response:

```
{
  "kpis": [
    { "id": "own.rounds", "label": "Rounds", "unit": "count",
      "markets": true, "grains": ["day", "week", "month"],
      "account_floor": true },
    { "id": "own.new_links", "label": "New links", "unit": "count",
      "markets": false, "grains": ["day", "week", "month"],
      "account_floor": false }
  ]
}
```

## Read one KPI

`kpisOwn` (see the API reference)

`own` returns one KPI as a series, with the total, the previous period and the change between them.

- `from` and `to` are `YYYY-MM-DD`, UTC and inclusive, and the range must be under 400 days.
- `grain` is `day` (default), `week` (ISO weeks from Monday) or `month`.
- `market` is ISO 3166-1 alpha-2, for example `MT`, and works only for KPIs with `markets: true`.

Every period in the range is returned. A period with no activity is `0`, or `null` for ratios and latencies. `previous` is the period of equal length just before `from`, and `previous_series` aligns it with `series` for overlays. `delta_pct` is `null` when either total is missing or the previous total is `0`.

(Parameter table: see the API reference.)

You get `200` with one `series` entry for every week in the range. A week backed by too few accounts shows `"suppressed": true`.

(Code sample: read-kpi. See the page for curl, Node.js, Python, PHP, Java and C#.)

Response:

```
{
  "kpi": "own.rounds", "label": "Rounds", "unit": "count",
  "from": "2026-09-01", "to": "2026-09-30", "grain": "week",
  "market": null,
  "series": [
    { "period": "2026-08-31", "value": 1240 },
    { "period": "2026-09-07", "value": null, "suppressed": true },
    { "period": "2026-09-14", "value": 1310 },
    { "period": "2026-09-21", "value": 1290 },
    { "period": "2026-09-28", "value": 1280 }
  ],
  "total": { "value": 5160 },
  "previous": { "from": "2026-08-02", "to": "2026-08-31", "value": 4800 },
  "previous_series": [
    { "value": 610 }, { "value": 1150 }, { "value": 1180 },
    { "value": 1220 }, { "value": 1240 }, { "value": 1200 }
  ],
  "delta_pct": 0.075,
  "generated_at": "2026-10-13T12:00:00.000Z"
}
```

## Break it down

`kpisBreakdown` (see the API reference)

`breakdown` takes the same parameters as `own`, except `grain`, plus `dimension`: `market`, `game` or `partner`. A partner is one of your own integration counterparts: a studio if you are an operator, an operator if you are a studio. Breakdowns support activity KPIs only; others return `400 breakdown_not_supported`.

The response holds the top 10 rows by value. For additive KPIs, each row has its `share` of the visible total and the remaining rows are folded into one `Other (n)` row. Rows under the account floor are never listed; they are counted in `suppressed_rows`.

(Parameter table: see the API reference.)

(Code sample: kpi-breakdown. See the page for curl, Node.js, Python, PHP, Java and C#.)

Response:

```
{
  "kpi": "own.rounds", "label": "Rounds", "unit": "count",
  "dimension": "market",
  "from": "2026-09-01", "to": "2026-09-30", "market": null,
  "rows": [
    { "key": "MT", "label": "MT", "value": 3100, "share": 0.6055 },
    { "key": "DE", "label": "DE", "value": 1400, "share": 0.2734 },
    { "key": "FI", "label": "FI", "value": 620, "share": 0.1211 }
  ],
  "suppressed_rows": 2
}
```

## Compare with the network

`kpisCompare` (see the API reference)

`compare` takes a `cmp.*` KPI and a closed period: `grain=month` with `period=2026-09`, or `grain=week` with `period=2026-W38`. An open or future period returns `400 period_not_closed`.

- `you` is your players' median for per-player KPIs, or your ratio for party KPIs.
- `index` is you ÷ network median × 100, and `band` is the network quartile your value falls in.
- Network size and party counts are given only as bands, never exact numbers.
- With a `market`, a market too thin to publish falls back to the global figure with `fallback: "global"`.

The network side is published only when the rest of the network spans at least 5 parties of your type and 50 accounts, and no single party holds more than 50% of the measure (or two more than 75%).

`kpisBenchmarks` (see the API reference)

`benchmarks` compares your totals with anonymous peers of your type, using the same `period`, `grain` and `market`. It takes `own.rounds`, `own.bet_eur`, `own.ggr` or `own.active_accounts` (global or per market), or `own.linked_players`, `own.ingest_acceptance` or `own.link_conversion` (global only). A segment is published only with at least 6 parties in it, you plus 5 others, and no dominant peer.

GET /v1/kpis/compare?grain=month&kpi=cmp.bet_per_player&period=2026-09:

```
{
  "kpi": "cmp.bet_per_player", "period": "2026-09", "grain": "month",
  "from": "2026-09-01", "to": "2026-09-30",
  "market": null, "fallback": null,
  "you": 45,
  "network": { "baseline": "rest_of_network",
    "p25": 20, "p50": 36, "p75": 52, "mean": 38.4,
    "size_band": "100-999", "parties_band": "5-9" },
  "index": 125, "band": "p50-p75",
  "suppressed": false, "reason": null,
  "computed_at": "2026-10-01T06:00:00.000Z"
}
```

GET /v1/kpis/benchmarks?grain=month&kpi=own.bet_eur&period=2026-09:

```
{
  "kpi": "own.bet_eur", "period": "2026-09", "grain": "month",
  "from": "2026-09-01", "to": "2026-09-30",
  "market": null, "fallback": null,
  "peers": { "party_type": "OPERATOR", "count_band": "5-9",
    "p25": 1200, "p50": 1800, "p75": 2600, "mean": 1950 },
  "band": "p0-p25", "suppressed": false, "reason": null,
  "computed_at": "2026-10-01T06:00:00.000Z"
}
```

## Handle suppression and errors

PlayerPal withholds any cell that describes too few people, and the rule is the same for every caller. See [Privacy thresholds](../analytics/privacy-thresholds.mdx).

- **Own KPIs and breakdowns:** for KPIs with `account_floor: true`, a cell backed by fewer than 50 distinct Accounts comes back as `"value": null, "suppressed": true`. Show it as withheld, never as zero.
- **Comparisons and benchmarks:** a withheld value has `suppressed: true` and a `reason`.

| `reason` | Meaning |
|---|---|
| `peer_floor` | The rest of the network spans fewer than 5 parties of your type |
| `account_floor` | The rest of the network has fewer than 50 accounts |
| `dominance` | One party has more than 50% of the measure, or two have more than 75% |
| `y_floor` | Your own population is under 50 accounts; the network is still shown |
| `elsewhere_floor` | Your players' activity elsewhere spans too few parties, or one dominates it |
| `not_computed` | No data for that period yet |

| Status | Codes |
|---|---|
| `400` | `kpi_unknown`, `kpi_not_available_for_party`, `date_range_invalid`, `date_range_too_long`, `grain_invalid`, `market_invalid`, `market_not_supported`, `dimension_invalid`, `breakdown_not_supported`, `month_range_invalid`, `month_range_too_long`, `period_not_closed`, `filter_invalid` |
| `403` | `partner_scope_missing`: the credential lacks `kpis.read` |
| `404` | Codes ending in `_not_found`, such as `insight_not_found` |
| `502`, `503` | `kpi_backend_error`, `kpi_backend_not_configured`. Retry later |

A comparison withheld:

```
{
  "kpi": "cmp.bet_per_player", "period": "2026-09", "grain": "month",
  "from": "2026-09-01", "to": "2026-09-30",
  "market": null, "fallback": null,
  "you": 45, "network": null,
  "index": null, "band": null,
  "suppressed": true, "reason": "peer_floor",
  "computed_at": "2026-10-01T06:00:00.000Z"
}
```

Range too long:

```
{
  "statusCode": 400,
  "message": "date_range_too_long",
  "error": "Bad Request"
}
```

## Overview, cohorts, insights and Explore

**Overview.** `GET /v1/kpis/overview?from=…&to=…` returns every overview KPI for your audience, each with `value`, `trend`, `source`, `lastUpdated` and a `dataAvailability` that is never faked as zero: `available`, `requires_feed` (with a `requirement` naming the missing feed), `not_applicable` (for example commercial KPIs for studios) or `suppressed`. It also returns chart `panels`, each with its own availability. Operators see their own players, studios see players of their games, and affiliates see only their verified-attributed players.

**Cohorts and funnels.** `/cohorts/retention` takes `from` and `to` as `YYYY-MM`, defaults to the last 12 closed months and allows at most 24. `/funnels/link` takes `YYYY-MM-DD` dates; the whole funnel is suppressed when its first step is under the account floor.

**Insights.** `/intelligence/insights` returns ranked insights and an executive summary from fixed rules and statistical tests. No language model computes or writes them. `/intelligence/ask` takes `q` (up to 300 characters are used) and routes it to one fixed analysis; it never runs free-form queries. See [Insights](../analytics/insights.mdx).

**Explore.** `/v1/kpis/explore/options`, `performance`, `summary`, `dimension` (with `dim=game`, `market` or `partner`), `players`, `retention`, `segments`, `funnel`, `game`, `comparison` and `benchmark-history` back detailed analytics views. Each takes `from` and `to`, plus optional `game`, `market` and `partner` filters. Call `explore/options` first: it lists only filter values with at least 50 players. A malformed filter, or a partner that is not one of your counterparts, returns `400 filter_invalid`.

## Related

  - [Metrics](../analytics/metrics.mdx): 
    How each KPI is defined.
  
  - [Fact ingest](./fact-ingest.mdx): 
    Deliver the feeds the overview needs.
  
  - [API reference](/api/partner): 
    Every KPI endpoint and parameter.
