> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reorderjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Analytics

> Query KPI summaries, time-series trends, and export analytics data — plus trigger manual snapshot rebuilds for historical date ranges.

The subscription analytics endpoints power the Admin analytics dashboard. You can fetch KPI summary cards, retrieve grouped time-series data for charts, export a flattened dataset as JSON or CSV, and trigger a manual rebuild of daily analytics snapshots for a historical range. All routes require an authenticated Medusa Admin user. Read routes share a common filter contract; the rebuild route uses a separate request body.

<Info>All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`.</Info>

## Metric keys

| Key                           | Unit         | Description                                  |
| ----------------------------- | ------------ | -------------------------------------------- |
| `mrr`                         | `currency`   | Monthly recurring revenue                    |
| `churn_rate`                  | `percentage` | Subscriber churn rate                        |
| `ltv`                         | `currency`   | Estimated lifetime value                     |
| `active_subscriptions_count`  | `count`      | Number of active subscriptions               |
| `created_subscriptions_count` | `count`      | Number of subscriptions created on a UTC day |

`MRR` and `LTV` resolve to `null` when the selected dataset lacks a single valid currency context.

<Note>
  `created_subscriptions_count` is returned by the trends response only. KPI cards remain `mrr`, `churn_rate`, `ltv`, and `active_subscriptions_count`.
</Note>

## Shared filter parameters

All three read routes (`/kpis`, `/trends`, `/export`) accept the same filter parameters.

<ParamField query="date_from" type="string">
  ISO datetime lower bound. Must be on or before `date_to`. Maximum analytics window is 731 days.
</ParamField>

<ParamField query="date_to" type="string">
  ISO datetime upper bound.
</ParamField>

<ParamField query="status" type="string | string[]">
  Filter by subscription status. Supported values: `active`, `paused`, `cancelled`, `past_due`.
</ParamField>

<ParamField query="product_id" type="string | string[]">
  Filter by product ID. Accepts a single value or an array.
</ParamField>

<ParamField query="frequency" type="string | string[]">
  Filter by cadence using serialized tokens such as `week:1`, `month:1`, or `year:1`. Accepts a single value or an array.
</ParamField>

<ParamField query="group_by" type="string">
  Time bucket granularity. One of `day`, `week`, or `month`. Defaults to `day`.
</ParamField>

<ParamField query="timezone" type="string">
  Timezone for bucket boundaries. Only `UTC` is supported in MVP.
</ParamField>

***

## GET /admin/subscription-analytics/kpis

Returns the KPI summary payload used by the Admin analytics overview cards.

### Response

<ResponseField name="kpis" type="object[]" required>
  Array of KPI summaries, one per supported metric key.

  <Expandable title="KPI properties">
    <ResponseField name="key" type="string">Metric key.</ResponseField>
    <ResponseField name="label" type="string">Human-readable label.</ResponseField>
    <ResponseField name="value" type="number | null">Computed metric value, or `null` when not computable for the selected range.</ResponseField>
    <ResponseField name="unit" type="string">One of `currency`, `percentage`, or `count`.</ResponseField>
    <ResponseField name="currency_code" type="string | null">Currency code for currency metrics, or `null`.</ResponseField>
    <ResponseField name="precision" type="number">Decimal precision for display formatting.</ResponseField>
    <ResponseField name="previous_value" type="number | null">Value for the prior comparable period.</ResponseField>
    <ResponseField name="delta_value" type="number | null">Absolute change from previous period.</ResponseField>
    <ResponseField name="delta_percentage" type="number | null">Percentage change from previous period.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="filters" type="object" required>Resolved filter values used for the query.</ResponseField>
<ResponseField name="metrics_version" type="string" required>Analytics schema version identifier.</ResponseField>
<ResponseField name="generated_at" type="string" required>Timestamp when the response was generated.</ResponseField>

```json Response example theme={null}
{
  "filters": {
    "date_from": "2026-04-01T00:00:00.000Z",
    "date_to": "2026-04-30T23:59:59.999Z",
    "status": ["active", "past_due"],
    "product_id": ["prod_123"],
    "frequency": [
      {
        "interval": "month",
        "value": 1
      }
    ],
    "group_by": "day"
  },
  "metrics_version": "analytics-v1",
  "generated_at": "2026-05-01T10:00:00.000Z",
  "kpis": [
    {
      "key": "mrr",
      "label": "MRR",
      "value": 2480,
      "unit": "currency",
      "currency_code": "usd",
      "precision": 2,
      "previous_value": 2310,
      "delta_value": 170,
      "delta_percentage": 7.36
    },
    {
      "key": "churn_rate",
      "label": "Churn Rate",
      "value": 3.2,
      "unit": "percentage",
      "currency_code": null,
      "precision": 2,
      "previous_value": 4.1,
      "delta_value": -0.9,
      "delta_percentage": -21.95
    },
    {
      "key": "ltv",
      "label": "LTV",
      "value": 412,
      "unit": "currency",
      "currency_code": "usd",
      "precision": 2,
      "previous_value": 398,
      "delta_value": 14,
      "delta_percentage": 3.52
    },
    {
      "key": "active_subscriptions_count",
      "label": "Active Subscriptions",
      "value": 182,
      "unit": "count",
      "currency_code": null,
      "precision": 0,
      "previous_value": 176,
      "delta_value": 6,
      "delta_percentage": 3.41
    }
  ]
}
```

### Errors

| Code  | Error          | Meaning                                                                      |
| ----- | -------------- | ---------------------------------------------------------------------------- |
| `400` | `invalid_data` | Invalid filter shape, unsupported grouping value, or invalid frequency token |

***

## GET /admin/subscription-analytics/trends

Returns grouped time-series data for Admin analytics charts. Each series covers one metric key and contains one point per time bucket within the requested range.

<Note>
  `created_subscriptions_count` is a special-case series:

  * it is sourced from `subscription.created_at`
  * it always returns one UTC day bucket per point
  * it ignores `status`, `product_id`, `frequency`, and `group_by`
  * it zero-fills missing days inside the selected range
</Note>

### Response

<ResponseField name="series" type="object[]" required>
  Array of trend series, one per metric key.

  <Expandable title="series properties">
    <ResponseField name="metric" type="string">Metric key.</ResponseField>
    <ResponseField name="label" type="string">Human-readable label.</ResponseField>
    <ResponseField name="unit" type="string">One of `currency`, `percentage`, or `count`.</ResponseField>
    <ResponseField name="currency_code" type="string | null">Currency code for currency metrics, or `null`.</ResponseField>
    <ResponseField name="precision" type="number">Decimal precision for display formatting.</ResponseField>
    <ResponseField name="points" type="object[]">Ordered time-series points. Each point has `bucket_start`, `bucket_end`, and `value` (may be `null`).</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="filters" type="object" required>Resolved filter values.</ResponseField>
<ResponseField name="metrics_version" type="string" required>Analytics schema version identifier.</ResponseField>
<ResponseField name="generated_at" type="string" required>Timestamp when the response was generated.</ResponseField>

```json Response example theme={null}
{
  "filters": {
    "date_from": "2026-04-01T00:00:00.000Z",
    "date_to": "2026-04-30T23:59:59.999Z",
    "status": ["active"],
    "product_id": [],
    "frequency": [],
    "group_by": "week"
  },
  "metrics_version": "analytics-v1",
  "generated_at": "2026-05-01T10:00:00.000Z",
  "series": [
    {
      "metric": "mrr",
      "label": "MRR",
      "unit": "currency",
      "currency_code": "usd",
      "precision": 2,
      "points": [
        {
          "bucket_start": "2026-03-30T00:00:00.000Z",
          "bucket_end": "2026-04-05T23:59:59.999Z",
          "value": 2280
        },
        {
          "bucket_start": "2026-04-06T00:00:00.000Z",
          "bucket_end": "2026-04-12T23:59:59.999Z",
          "value": 2330
        }
      ]
    },
    {
      "metric": "active_subscriptions_count",
      "label": "Active Subscriptions",
      "unit": "count",
      "currency_code": null,
      "precision": 0,
      "points": [
        {
          "bucket_start": "2026-03-30T00:00:00.000Z",
          "bucket_end": "2026-04-05T23:59:59.999Z",
          "value": 174
        },
        {
          "bucket_start": "2026-04-06T00:00:00.000Z",
          "bucket_end": "2026-04-12T23:59:59.999Z",
          "value": 178
        }
      ]
    },
    {
      "metric": "created_subscriptions_count",
      "label": "Created Subscriptions",
      "unit": "count",
      "currency_code": null,
      "precision": 0,
      "points": [
        {
          "bucket_start": "2026-04-01T00:00:00.000Z",
          "bucket_end": "2026-04-01T23:59:59.999Z",
          "value": 3
        },
        {
          "bucket_start": "2026-04-02T00:00:00.000Z",
          "bucket_end": "2026-04-02T23:59:59.999Z",
          "value": 0
        }
      ]
    }
  ]
}
```

### Errors

| Code  | Error          | Meaning                                                                      |
| ----- | -------------- | ---------------------------------------------------------------------------- |
| `400` | `invalid_data` | Invalid filter shape, unsupported grouping value, or invalid frequency token |

***

## GET /admin/subscription-analytics/export

Returns an export payload aligned with the active analytics filters. The export is synchronous and supports `json` and `csv` formats. Rows represent one entry per time bucket.

### Additional query parameter

<ParamField query="format" type="string">
  Export format. One of `json` or `csv`. Defaults to `json`.
</ParamField>

### Response

<ResponseField name="format" type="string" required>Resolved export format.</ResponseField>
<ResponseField name="filters" type="object" required>Resolved filter values.</ResponseField>
<ResponseField name="metrics_version" type="string" required>Analytics schema version identifier.</ResponseField>
<ResponseField name="generated_at" type="string" required>Timestamp when the export was generated.</ResponseField>
<ResponseField name="file_name" type="string" required>Suggested file name for the export.</ResponseField>
<ResponseField name="content_type" type="string" required>MIME type for the export format.</ResponseField>
<ResponseField name="columns" type="string[]" required>Ordered column names for the export dataset.</ResponseField>
<ResponseField name="rows" type="object[]" required>Flattened export rows. Each row contains one value per column. Metric cells may be `null`.</ResponseField>

```json Response example theme={null}
{
  "format": "json",
  "filters": {
    "date_from": "2026-04-01T00:00:00.000Z",
    "date_to": "2026-04-30T23:59:59.999Z",
    "status": ["active"],
    "product_id": [],
    "frequency": [],
    "group_by": "month"
  },
  "metrics_version": "analytics-v1",
  "generated_at": "2026-05-01T10:00:00.000Z",
  "file_name": "subscription-analytics-2026-05-01.json",
  "content_type": "application/json",
  "columns": [
    "bucket_start",
    "bucket_end",
    "mrr",
    "churn_rate",
    "ltv",
    "active_subscriptions_count"
  ],
  "rows": [
    {
      "bucket_start": "2026-04-01T00:00:00.000Z",
      "bucket_end": "2026-04-30T23:59:59.999Z",
      "mrr": 2480,
      "churn_rate": 3.2,
      "ltv": 412,
      "active_subscriptions_count": 182
    }
  ]
}
```

### Errors

| Code  | Error          | Meaning                                                                                                 |
| ----- | -------------- | ------------------------------------------------------------------------------------------------------- |
| `400` | `invalid_data` | Invalid filter shape, unsupported grouping value, unsupported export format, or invalid frequency token |

***

## POST /admin/subscription-analytics/rebuild

Triggers a manual rebuild of daily analytics snapshots for a historical date range. Reuses the same shared workflow used by scheduled and incremental analytics jobs. Rebuild is day-level idempotent — rerunning the same range is safe.

### Body parameters

<ParamField body="date_from" type="string" required>
  ISO datetime start of the rebuild range.
</ParamField>

<ParamField body="date_to" type="string" required>
  ISO datetime end of the rebuild range. Maximum manual rebuild window is 365 days.
</ParamField>

<ParamField body="reason" type="string">
  Optional reason for the rebuild, recorded for audit purposes.
</ParamField>

```json Request example theme={null}
{
  "date_from": "2026-04-01T00:00:00.000Z",
  "date_to": "2026-04-30T23:59:59.999Z",
  "reason": "historical backfill after metrics review"
}
```

### Response

<ResponseField name="date_from" type="string" required>Rebuild range start.</ResponseField>
<ResponseField name="date_to" type="string" required>Rebuild range end.</ResponseField>
<ResponseField name="processed_days" type="number" required>Number of calendar days processed.</ResponseField>
<ResponseField name="processed_subscriptions" type="number" required>Total subscription records evaluated.</ResponseField>
<ResponseField name="upserted_rows" type="number" required>Snapshot rows created or updated.</ResponseField>
<ResponseField name="skipped_rows" type="number" required>Rows skipped during the rebuild.</ResponseField>
<ResponseField name="blocked_days" type="string[]" required>Days that could not be processed.</ResponseField>
<ResponseField name="failed_days" type="string[]" required>Days where processing failed. Partial failure does not change the HTTP status to `500`.</ResponseField>

```json Response example theme={null}
{
  "date_from": "2026-04-01T00:00:00.000Z",
  "date_to": "2026-04-30T23:59:59.999Z",
  "processed_days": 30,
  "processed_subscriptions": 1240,
  "upserted_rows": 1240,
  "skipped_rows": 0,
  "blocked_days": [],
  "failed_days": []
}
```

### Errors

| Code  | Error          | Meaning                                                 |
| ----- | -------------- | ------------------------------------------------------- |
| `400` | `invalid_data` | Invalid request body or rebuild window exceeds 365 days |
