# Openrouter Analytics Schema

> Discover the OpenRouter analytics schema — available metrics, dimensions, filter operators, and granularities. Use when you need to know what analytics data is queryable, what dimensions you can break down by, or how to map a user's question to the right metric/dimension combination.

- Skill: `openrouterteam/openrouter-analytics-schema` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add openrouterteam/openrouter-analytics-schema`
- Raw SKILL.md: https://api.skillmd.com/api/skills/openrouterteam/openrouter-analytics-schema/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- Author: openrouterteam (https://skillmd.com/u/openrouterteam)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/openrouterteam/openrouter-analytics-schema

---


# OpenRouter Analytics Schema Discovery

Discover what analytics data is available for querying. The meta endpoint returns live, always-current definitions of metrics, dimensions, filter operators, and granularities.

## Prerequisites

- An OpenRouter **management key**. Management keys are separate from regular API keys — get one at https://openrouter.ai/settings/management-keys
- Pass it via `--api-key <key>` or set the `OPENROUTER_API_KEY` environment variable

## Discovery Endpoint

```
GET https://openrouter.ai/api/v1/analytics/meta
Authorization: Bearer sk-or-v1-...
```

Or via the `openrouter-analytics` skill scripts:

```bash
cd <openrouter-analytics-skill-path>/scripts && npx tsx discover-schema.ts
```

## Response Shape

```json
{
  "data": {
    "metrics": [
      { "name": "request_count", "display_label": "Request Count", "is_rate": false, "display_format": "number" }
    ],
    "dimensions": [
      { "name": "model", "display_label": "Model" }
    ],
    "operators": [
      { "name": "eq", "value_type": "scalar" }
    ],
    "granularities": [
      { "name": "day", "display_label": "Day" }
    ]
  }
}
```

## Understanding Metrics

Each metric has:

| Field | Meaning |
|---|---|
| `name` | Identifier to use in query requests |
| `display_label` | Human-readable label |
| `is_rate` | Whether this is a ratio/rate (averaged, not summed) |
| `display_format` | How the value should be formatted: `number`, `currency`, `percent`, `latency`, or `throughput` |

### Time Range Limits

Most volume and cost metrics support time ranges up to **365 days** with daily granularity. Latency/throughput metrics and some dimensions (`provider`, `origin`, `country`, `finish_reason`, `external_user`, `context_length_bucket`, `generation_id`) are limited to **31-day** time ranges. If a query times out, try narrowing the time range or removing latency/throughput metrics and per-generation dimensions.

### Metric Categories

**Volume metrics** (how much):
- `request_count` — number of API requests (up to 365 days)
- `tokens_total`, `tokens_prompt`, `tokens_completion` — token counts (up to 365 days)
- `reasoning_tokens` — tokens used for extended thinking (up to 365 days)
- `cached_tokens` — tokens served from cache (up to 365 days)
- `byok_request_count` — number of BYOK requests (up to 365 days)
- `guardrail_invoked_count` — count of requests that triggered guardrails (31-day limit)
- `response_cached_count` — count of responses served from cache (31-day limit)

**Cost metrics** (how much money):
- `total_usage` — total cost in USD, including BYOK inference cost (up to 365 days). Computed as `sum(usage) + sum(byok_usage_inference)` so it reflects true spend for both credits and BYOK users.
- `byok_usage` — BYOK (bring your own key) inference cost in USD (up to 365 days)
- `credits_usage` — all charges billed to OpenRouter credits in USD, including BYOK platform fees (up to 365 days)
- `openrouter_usage` — non-BYOK inference spend in USD; excludes requests made with user-provided keys (31-day limit)
- `byok_fees` — BYOK platform fees in USD; the platform fee portion of `credits_usage` charged on BYOK requests (31-day limit). `credits_usage` includes both non-BYOK inference charges and these BYOK platform fees.
- `usage_upstream` — provider-side (upstream) cost in USD (up to 365 days)
- `usage_cache` — cache cost component in USD (up to 365 days)
- `usage_data` — data logging cost adjustment in USD; typically negative when a data logging discount applies (up to 365 days)
- `usage_web` — web search cost in USD (up to 365 days)
- `usage_upstream_web` — provider-side web search cost in USD (up to 365 days)
- `usage_file` — file processing cost in USD (31-day limit)
- `usage_upstream_file` — provider-side file processing cost in USD (31-day limit)
- `usage_web_fetch` — web fetch cost in USD (31-day limit)
- `usage_upstream_web_fetch` — provider-side web fetch cost in USD (31-day limit)

**Performance metrics** (how fast):
- `avg_latency`, `p50_latency`, `p90_latency`, `p99_latency` — response latency in milliseconds
- `avg_throughput`, `p50_throughput`, `p90_throughput`, `p99_throughput` — tokens per second

**Efficiency metrics** (how well):
- `cache_hit_rate` — ratio of cached tokens to prompt tokens (0–1)
- `guardrail_invoked_rate` — ratio of requests that triggered guardrails
- `response_cached_rate` — ratio of responses served from cache

## Understanding Dimensions

Each dimension has:

| Field | Meaning |
|---|---|
| `name` | Identifier to use in query requests |
| `display_label` | Human-readable label |

Dimensions are what you break down by — "show me spend *by model*" means `dimensions: ["model"]`.

You can combine up to 2 dimensions in a single query (e.g., `["model", "provider"]`).

### Label Resolution

Some dimensions have their raw IDs automatically resolved to human-readable labels in query results. Data rows contain the resolved display names directly:

| Dimension | Resolved to |
|---|---|
| `api_key_id` | Key name/label |
| `app` | App title or origin URL |
| `user` | User name or email address |
| `workspace` | Workspace name |

All other dimensions (e.g., `model`, `provider`, `country`) are returned as-is without resolution.

> Rows with an empty `user` value represent traffic not attributed to a specific org member (e.g., API keys created at the org level).

### Dimension Categories

**Available with all time ranges:**
- `model` — the OpenRouter model ID (permaslug)
- `variant` — model variant (e.g., standard, extended)
- `api_key_id` — which API key made the request
- `user` — the creator user ID (for org-level queries)
- `workspace` — workspace ID
- `app` — application ID

**Limited to 31-day time ranges:**
- `generation_id` — unique ID for each generation (use to drill down to individual requests, then inspect via the `openrouter-generations` skill)
- `provider` — upstream provider name
- `origin` — request origin/source
- `country` — request country
- `finish_reason` — why the generation ended (stop, length, etc.)
- `external_user` — custom user ID passed by the caller
- `context_length_bucket` — bucketed context length (1K, 10K, 100K, etc.)

## Classifier Dimensions

Beyond the standard dimensions above, you can group by **classifier dimensions** — dynamic labels produced by a user-created classifier (e.g., topic, sentiment, category).

- Use the `classifier_dimensions` request field to group by classifier-produced values
- Use the `classifier_filters` request field to filter on classifier values
- The classifier must belong to your account (validated server-side)
- Classifier dimensions/filters limit the query time range to 31 days
- See the `openrouter-analytics-query` skill for the full request shape

### Mapping Questions to Classifier Queries

| Question pattern | Request fields | Notes |
|---|---|---|
| "Spend by topic" | `classifier_dimensions: { classifier_id, dimension_names: ["topic"] }` + `metrics: ["total_usage"]` | Single dimension → column aliased to `topic` |
| "Only billing-related requests" | `classifier_filters: { classifier_id, filters: [{ field: "category", operator: "eq", value: "billing" }] }` | Filters support `eq`, `neq`, `in`, `not_in` only |
| "Sentiment breakdown including unclassified" | `classifier_dimensions: { classifier_id, dimension_names: ["sentiment"], include_nulls: true }` | Includes rows without classification |

## Understanding Operators

Filter operators for the `filters` array in query requests:

| Operator | Value Type | Meaning |
|---|---|---|
| `eq` | scalar | Equals |
| `neq` | scalar | Not equals |
| `gt` | scalar | Greater than |
| `gte` | scalar | Greater than or equal |
| `lt` | scalar | Less than |
| `lte` | scalar | Less than or equal |
| `in` | array | In list |
| `not_in` | array | Not in list |

## Understanding Granularities

Time bucketing for time-series queries:

| Granularity | Use when |
|---|---|
| `minute` | Last few hours, real-time monitoring |
| `hour` | Last 1–3 days |
| `day` | Last week to 3 months |
| `week` | Last 3–12 months |
| `month` | Year-scale trends |

When no granularity is set, the query returns aggregate totals without time bucketing.

## Mapping Questions to Queries

Use this guide to translate natural-language questions into the right metric/dimension/filter combination:

| Question pattern | Metrics | Dimensions | Notes |
|---|---|---|---|
| "How much did I spend?" | `total_usage` | — | Add granularity for trends |
| "Which models cost the most?" | `total_usage` | `model` | Order by `total_usage` desc |
| "How many requests?" | `request_count` | — | Add `model` or `api_key_id` for breakdown |
| "How many tokens?" | `tokens_total` | — | Use `tokens_prompt` / `tokens_completion` for split |
| "Which provider is fastest?" | `avg_latency`, `p90_latency` | `provider` | 31-day limit |
| "What's my cache hit rate?" | `cache_hit_rate` | `model` | Rate metric — shows per-model caching |
| "Which API key uses the most?" | `request_count`, `total_usage` | `api_key_id` | — |
| "Usage over time" | `request_count` or `total_usage` | — | Set `granularity: "day"` |
| "Latency trends" | `p90_latency` | — | Set `granularity: "hour"`, 31d limit |
| "Usage by country" | `request_count` | `country` | 31-day limit |
| "How can I save money?" | `total_usage`, `cache_hit_rate`, `tokens_total` | `model` | See cost optimization in `openrouter-analytics` skill |
| "Show me individual requests" | `total_usage`, `tokens_total` | `generation_id` | 31-day limit. Use returned IDs with `openrouter-generations` skill for full metadata and content |
| "How much BYOK spend?" | `byok_usage` | `model` | Up to 365 days |
| "BYOK vs credits split?" | `byok_usage`, `credits_usage` | — | Both up to 365 days |
| "BYOK platform fees?" | `byok_fees` | `model` | 31-day limit |
| "Non-BYOK inference spend?" | `openrouter_usage` | `model` | 31-day limit |
| "How many guardrail triggers?" | `guardrail_invoked_count`, `guardrail_invoked_rate` | `model` | 31-day limit |
| "How many cached responses?" | `response_cached_count`, `response_cached_rate` | `model` | 31-day limit |
| "Where does my spend go?" | `usage_upstream`, `usage_cache`, `usage_data` | — | Full cost breakdown (up to 365 days) |
| "Web search costs?" | `usage_web`, `usage_upstream_web` | `model` | Up to 365 days |
| "File processing costs?" | `usage_file`, `usage_upstream_file` | `model` | 31-day limit |
| "Web fetch costs?" | `usage_web_fetch`, `usage_upstream_web_fetch` | `model` | 31-day limit |

## Filter Value Reference

Several dimensions are **label-resolved** in query results — the response shows human-readable names, but filters must use the underlying ID. Here's where to find each:

| Dimension | Filter value | Where to find it |
|---|---|---|
| `api_key_id` | Numeric ID **or** 64-char SHA-256 hash | Numeric ID: generation metadata (`api_key_id` field). Hash: `GET /api/v1/keys` (`key_hash` field). Hashes are auto-resolved server-side. If a hash can't be resolved, a sentinel value returns zero rows (no error). |
| `user` | Clerk user ID (e.g. `user_abc123`) | User settings or org member list — not the display name/email shown in results. |
| `workspace` | Workspace UUID | Workspace settings page or `GET /api/v1/workspaces` — not the workspace name shown in results. |
| `app` | Numeric app ID | Generation metadata (`app_id` field) or app settings — not the app title shown in results. |
| `model` | Permaslug (e.g. `openai/gpt-4o`) | Model page URL or `GET /api/v1/models` — not the display name. |

Other dimensions (`provider`, `origin`, `country`, `finish_reason`, `external_user`, etc.) are not enriched — filter values match what's returned in results.

## Constraints

- Maximum 2 dimensions per query
- Maximum 20 filters per query
- Maximum 10 classifier dimensions per query
- Maximum 10 classifier filters per query
- Maximum 10,000 rows returned per query (default 1,000)
- `group_limit` (1–10,000): controls max rows per dimension combination. Auto-computed on time-series queries with dimensions to guarantee full time-window coverage. Set explicitly to cap per-group rows (e.g., top N per model per day).
- Most volume/cost metrics: up to 365 days with daily granularity
- Latency/throughput metrics and per-generation dimensions: up to 31 days
- Classifier dimensions/filters: always limited to 31 days
- Minute granularity: only available when the time window is ≤ 3 hours
- Rate-limited to 64 requests per minute

