Control plane API
Analytics
Window summaries, time series, and the request-by-request log, all from the gateway's telemetry and scoped to your organisation.
The analytics endpoints read the gateway's request telemetry, which is a separate store from the control plane's own records. Every call is scoped to the caller's organisation from the session. If the telemetry store is unreachable they answer 503 {"error": "analytics backend unavailable"} while every other endpoint keeps working.
Common filters
| Parameter | Meaning |
|---|---|
time_range | Lookback window: <int><unit>, unit s, m, h or d (30s, 5m, 24h, 7d, 30d). Default 24h. |
project_id | One project's id. Must be the caller's, else 404. |
key_id | One key's id. Same ownership check. |
model_alias (model on /requests) | The route id the caller sent, not the resolved upstream model. |
Blank values equal omitted ones. Averages are null, not 0, over a window with no matching requests.
GET /analytics/summary
{
"time_range": "24h",
"total_requests": 1420,
"avg_e2e_latency_ms": 812.4,
"avg_ttft_ms": 143.2,
"avg_tokens_per_sec": 61.3,
"total_token_usage": 9412338,
"total_cost_pkr": 61.05
}time_range echoes the window applied; label panels from the response, not the request. total_cost_pkr (plain PKR) is present only when the query names a key_id; on an organisation-wide summary the field is absent. Tokens per second is derived at query time from tokens generated over generation time.
GET /analytics/timeseries
Parameters: metric (volume, latency, ttft, tok_s, token_usage), interval (bucket width, 30s, 5m, 1h, 1d), group_by (project, key, model), plus the common filters.
{
"metric": "latency",
"interval": "1h",
"group_by": "model",
"time_range": "24h",
"data": [
{"bucket": "2026-08-25T19:00:00", "group": "paranine/gpt-oss-120b(Global)", "value": 1200.0}
]
}One row per bucket and group, a flat shape you can pivot however your charting library wants. group is the raw project id, key id or route id; resolve names with /projects and /keys. Bucket timestamps carry no timezone and are UTC. The endpoint does not stop you pairing interval=30s with time_range=30d; pick a sane pairing.
GET /analytics/requests
The request log: one row per request, newest first, paged.
| Parameter | Meaning |
|---|---|
time_range, project_id, key_id, model | As above. |
status_category | Exactly one of 2xx, 3xx, 4xx, 5xx. Any other non-blank value is 400. |
termination_reason | Exact match: success, proxy_guillotine (server-side timeout), client_disconnect, upstream_stream_aborted. An unknown value matches nothing. |
min_cost_pkr | Minimum settled cost, in PKR (0.01 excludes free error rows). |
search | Case-insensitive match on the request id or on the X-P9-Meta-* metadata, nothing else. |
limit, offset | Page size (default 50, clamped to 200) and row offset. Use total to know when to stop. |
{
"total": 1420,
"limit": 50,
"offset": 0,
"requests": [
{
"req_id": "k2i-1787578888306946000-0001",
"timestamp": "2026-08-24T12:34:56.789Z",
"model": "paranine/gpt-oss-120b(Global)",
"status": 200,
"stream": true,
"key": {"id": "e02dcba3-…", "name": "checkout-summariser-prod"},
"project": {"id": "3dbb877d-…", "name": "Assistant"},
"metrics": {
"total_ms": 420,
"ttft_ms": 110,
"tpot_ms": 6.8,
"pre_dispatch_ms": 5,
"prompt_tokens": 45,
"tokens_generated": 40,
"cache_creation_input_tokens": null,
"cache_read_input_tokens": null,
"total_tokens": 85,
"tokens_estimated": false
},
"financials": {"cost_pkr": 0.04512, "is_byok": 0},
"status_detail": {"termination_reason": "success", "upstream_error": null},
"upstream": {"model": "openai/gpt-oss-120b"},
"user_agent": "OpenAI/Python 1.51.0",
"metadata": {"user_tag": "emp-4092", "feature_tag": "ticket_summary"}
}
]
}Nulls are meaningful: key or project is null when the request had no attribution at all (as opposed to an object whose name is null because the key has since been deleted); metrics.ttft_ms and tpot_ms are null for non-streaming requests and for a stream whose first-token event has not flushed yet; metrics.prompt_tokens and the whole upstream block are null on rows served before 2026-08-24, when the gateway began recording them.
Prompt caching, billing mode and client
| Field | Meaning |
|---|---|
metrics.cache_creation_input_tokens | Tokens written into the provider's prompt cache by this request. |
metrics.cache_read_input_tokens | Tokens served out of that cache instead of being re-read as prompt. |
metrics.total_tokens | Prompt, completion and both cache legs. A request that read a large cache has a total far above prompt_tokens + tokens_generated; do not re-derive the total by adding those two. |
financials.is_byok | 1 when the request was served against your own provider key from the vault, 0 when it was served on P/9 capacity. Sent as an integer, not a boolean. |
user_agent | The User-Agent of the client that made the call, verbatim. |
Both cache counts are null on P/9's own global routes, which have no prompt cache. null there means the route does not cache, which is not the same claim as 0, and a dashboard or report that treats them alike will show a route with no cache as a route that never hits one.
is_byok is the explanation for a near-zero cost_pkr. On a BYOK request the provider bills you directly and P/9 settles only its own share, so a row can legitimately cost a fraction of a paisa. Read the cost against this flag, not on its own.
Something unclear or wrong on this page? Tell us. Machine-readable copies: llms.txt.