# Get board analytics

`GET https://evohub.io/api/v1/board/analytics`

Part of the [Board API](https://docs.evohub.io/board.md) reference · operationId `getBoardAnalytics`.

Throughput, cycle and lead time, cumulative flow, work in progress,
overdue cards, review figures and a per-assignee breakdown, counted
over the boards the caller can see.

With neither `from` nor `to` the range is the last 30 UTC days, today
included. A date (`YYYY-MM-DD`) is a UTC day and `to` names the last
day included; an RFC 3339 timestamp is an exact instant and `to` is
exclusive. A range may be at most 366 days. Ranges up to 48 hours are
bucketed by hour, longer ones by day.

Completed, cycle time and lead time are over the range; work in
progress, backlog, overdue and pending review are the boards as they
stand now. `compare` is `null` unless a comparison is asked for.

Requires the `board:board:read` scope.

## Authorization

Any one of:

- `bearerKey`
- `headerKey`

Where:

- `bearerKey`: HTTP Bearer — An EvoHub API key (`evohub_…`) as a bearer token.
- `headerKey`: API key in the header `X-API-Key` — An EvoHub API key (`evohub_…`).

## Query parameters

- `from` (string, example `2026-10-01`): Start of the range, `YYYY-MM-DD` or RFC 3339. Sent together with `to`.
- `to` (string, example `2026-10-31`): End of the range, `YYYY-MM-DD` (included) or RFC 3339 (excluded). Sent together with `from`.
- `compare` (string, one of `previous`, `year`): Also report the headline numbers of the previous period of the same length (`previous`) or the same range a year earlier (`year`).
- `board_id` (string, example `brd_5b0c8e1a2f3d4c6b9a7e0d1f2c3b4a59`): Count only these boards, comma-separated, at most 100. Every one must be a board the caller can see, or the answer is **404**.
- `team_id` (string): Count only one team's boards — its own and the ones given to it.

## Responses

### 200 — The analytics.

Content type: `application/json`

Type: `object`

- `data` (BoardAnalytics)
  - `range` (object)
    - `from` (string (date-time))
    - `to` (string (date-time)): Exclusive.
    - `granularity` (string, one of `hour`, `day`)
    - `tz` (string, value `UTC`)
  - `boards` (array of object): The boards counted.
    - `id` (string)
    - `name` (string)
    - `card_prefix` (string)
    - `team_id` (string)
  - `totals` (object)
    - `completed` (integer)
    - `cycle_time` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
    - `lead_time` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
    - `wip` (integer)
    - `backlog` (integer)
    - `overdue` (integer)
    - `pending_review` (integer)
  - `throughput` (object)
    - `series` (array of object): Cards finished per bucket.
      - `bucket` (string (date-time))
      - `completed` (integer)
    - `weekly` (array of object): Cards finished per ISO week (Monday, UTC). `partial` is a week the range only partly covers.
      - `week_start` (string (date-time))
      - `completed` (integer)
      - `partial` (boolean)
  - `scatter` (array of object): Finished cards with their cycle and lead time, the most recent 2,000.
    - `card_id` (string)
    - `board_id` (string)
    - `reference` (string)
    - `title` (string)
    - `completed_at` (string (date-time))
    - `cycle_seconds` (number)
    - `lead_seconds` (number)
  - `scatter_truncated` (boolean)
  - `cumulative_flow` (object): How many cards were in each list at the end of each bucket; `counts` align with `buckets`.
    - `buckets` (array of string (date-time))
    - `boards` (array of object)
      - `board_id` (string)
      - `lists` (array of object)
        - `list_id` (string)
        - `name` (string)
        - `is_backlog` (boolean): The board's first ordinary list, where cycle time starts.
        - `is_review_list` (boolean)
        - `is_done_list` (boolean)
        - `archived` (boolean)
        - `counts` (array of integer)
  - `wip` (array of object): What sits in each live list now.
    - `board_id` (string)
    - `list_id` (string)
    - `name` (string)
    - `is_backlog` (boolean)
    - `is_review_list` (boolean)
    - `is_done_list` (boolean)
    - `cards` (integer)
    - `overdue` (integer)
    - `pending_review` (integer)
  - `overdue` (object): Open cards past their due date, most overdue first, at most 100.
    - `total` (integer)
    - `cards` (array of object)
      - `card_id` (string)
      - `board_id` (string)
      - `list_id` (string)
      - `reference` (string)
      - `title` (string)
      - `due_at` (string (date-time))
      - `assignees` (array of string)
    - `truncated` (boolean)
  - `review` (object): The review gate over the range.
    - `submitted` (integer)
    - `approved` (integer)
    - `rejected` (integer)
    - `approval_rate` (number | null)
    - `rework_cards` (integer): Cards rejected in the range that came back to review afterwards.
    - `rework_rate` (number | null)
    - `time_in_review` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
    - `time_to_approval` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
    - `time_to_rejection` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
    - `pending_now` (integer)
    - `oldest_pending_since` (string (date-time) | null)
  - `by_assignee` (array of object): Each person's share. `user_id` `""` is the cards with nobody on them.
    - `user_id` (string)
    - `completed` (integer)
    - `cycle_time` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
    - `wip` (integer)
    - `overdue` (integer)
  - `compare` (object | null): The comparison period's headline numbers, or `null`.
    - `mode` (string, one of `previous`, `year`)
    - `from` (string (date-time))
    - `to` (string (date-time))
    - `completed` (integer)
    - `cycle_time` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
    - `lead_time` (DurationStats)
      - `count` (integer)
      - `p50_seconds` (number | null)
      - `p85_seconds` (number | null)
      - `p95_seconds` (number | null)
      - `avg_seconds` (number | null)
- `success` (boolean, value `true`)

### 400 — A parameter is wrong (`VALIDATION_FAILED`; `details` names each one): only one of `from` and `to`, a bound that is neither a date nor RFC 3339, `to` not after `from`, a range over 366 days, an unknown `compare`, or more than 100 boards.

Content type: `application/json`

Type: `ValidationFailed`

- `error` (object, required)
  - `code` (string, required, value `VALIDATION_FAILED`)
  - `message` (string, required)
  - `details` (array of object, required)
    - `field` (string)
    - `message` (string)

### 401 — No API key was sent, or it is unknown, revoked or expired (`UNAUTHORIZED`).

Content type: `application/json`

Type: `Error`

- `error` (object, required)
  - `code` (string, required, example `NOT_FOUND`)
  - `message` (string, required, example `not found`)
  - `request_id` (string)

### 403 — The key lacks the scope this endpoint needs (`FORBIDDEN`).

Content type: `application/json`

Type: `Error`

- `error` (object, required)
  - `code` (string, required, example `NOT_FOUND`)
  - `message` (string, required, example `not found`)
  - `request_id` (string)

### 404 — A `board_id` names a board the caller cannot see (`NOT_FOUND`).

Content type: `application/json`

Type: `Error`

- `error` (object, required)
  - `code` (string, required, example `NOT_FOUND`)
  - `message` (string, required, example `not found`)
  - `request_id` (string)

### 429 — Too many requests (`RATE_LIMITED`). Wait for `Retry-After` seconds.

Headers:

- `Retry-After` (integer): Seconds to wait before retrying.

Content type: `application/json`

Type: `Error`

- `error` (object, required)
  - `code` (string, required, example `NOT_FOUND`)
  - `message` (string, required, example `not found`)
  - `request_id` (string)

### 500 — Something went wrong on EvoHub's side (`INTERNAL_ERROR`). Retry later.

Content type: `application/json`

Type: `Error`

- `error` (object, required)
  - `code` (string, required, example `NOT_FOUND`)
  - `message` (string, required, example `not found`)
  - `request_id` (string)

## Example request

```bash
curl -X GET 'https://evohub.io/api/v1/board/analytics' \
  -H 'Authorization: Bearer <TOKEN>'
```
