Analytics
Reading the dashboard — deltas, drilldown, breakdown, share, CSV.
The /analytics page answers two operator questions at a glance: is
this better or worse than before and which calls drove the change. Every
KPI, chart, and metric is paired with a period-over-period delta, every
chart drills down to the underlying calls in one click, and every view is
shareable via URL.
Reading the deltas
Every KPI card reads as a sentence:
1,247 calls completed Up 18% from 1,053 last week
Color polarity always means the change is good for this metric, not the raw sign of the number:
- Cost going down is green; cost going up is red.
- Volume going up is green; volume going down is red.
- Latency going down is green; latency going up is red.
If a metric has no prior-period data to compare against, the delta reads
"No data last week to compare" — never NaN% or Infinity%.
A one-line headline insight above the KPI row picks one of six templates based on the delta signs. The worst-news signal wins: if both volume is up AND cost-per-call jumped 30%, the headline calls out the cost spike, not the volume win.
"Didn't catch" — low-confidence speech
The Didn't catch KPI counts calls where the AI flagged at least one phrase it wasn't confident it heard correctly. It's a quality signal for how often callers had to repeat themselves.
It's measured only on engines that report per-word confidence (the
speech-to-text path), so the rate's denominator is STT-measured calls,
not all calls. When a window has no measured calls (e.g. every call ran on
a native-audio engine), the metric reads —, never 0% — "not measured"
can't masquerade as "caught everything." A per-engine breakdown table
shows which engine drives the rate.
This pairs with a flow's clarify_on_uncertainty
block: turn it on to have the agent ask callers to repeat instead of
guessing, then watch the rate here.
Time range + compare toggle
Pick the window with the range selector: 24h, 7d, 30d, 90d, or a custom start/end pair. Each chart panel has its own "Compare to last <range>" toggle. When ON, the chart renders a faint dashed reference line for the prior period; tooltip shows both values plus the percentage diff.
Compare toggle state lives in the URL — sharing the URL preserves it.
Drilldown from charts to calls
Every chart bar / heatmap cell / failure row navigates to /calls
filtered to just the rows behind that data point:
| Click here | Lands at |
|---|---|
| Volume "Failed" chip | /calls?status=failed&range=… |
| "View calls sorted by duration" | /calls?range=…&sort=duration_desc |
Cost wedge for voice | /calls?range=…&voice_engine=… |
| A row in Top Failures | /calls?status=failed&range=… |
| A confidence-band cell on the heatmap | /calls?has_outcome=true&min_confidence=…&max_confidence=… |
A pill bar above the /calls table renders each active filter in
English ("Failed calls", "Inbound", "Grok engine") with × to clear
individual filters. A separate "From volume chart, last 7 days" source
pill shows where you came from and detaches independently.
If a filter combination returns zero rows, the empty state lists every active filter + a one-click "Show all calls" link.
Breakdown by dimension
Group the entire dashboard by one of five universal dimensions using the Group by selector:
| Dimension | What it shows |
|---|---|
| Voice engine | Per-engine performance during the A/B (Gemini vs Grok, etc.). |
| Hour of day | When your calls happen + which hours perform best. |
| Day of week | Weekday vs weekend skew. |
| Carrier | Which carrier is failing / leading. |
| Phone number | Per-line performance — useful when one tenant runs many lines. |
When set, every chart on the page renders as small multiples — one panel per group — with a top-8 cap and an "Other" overflow group for the remainder. Color assignments are consistent across panels: "Grok" gets the same color in the volume, latency, and cost charts.
The carrier breakdown uses a lookup table seeded with ~60 US/CA area- code prefixes. Unknown prefixes fall into a "(unknown)" group. A fuller LRN-API integration is planned.
Shareable URL
The /analytics page is URL-driven. Every control — range, direction
toggle, breakdown, per-chart compare toggles — round-trips on refresh
and back/forward. Share a view by clicking the Share button: it
copies window.location.href to the clipboard with a toast confirmation.
Anyone with the link sees the same view.
Example shareable URLs:
/analytics?range=30d&breakdown=voice_engine
/analytics?range=7d&direction=inbound&compare-volume=1
/analytics?range=custom&start=2026-05-01&end=2026-05-15&breakdown=hour_of_dayCSV export
Every chart has a small download icon in its toolbar. Click it to get a CSV of the underlying data points (one row per data point). Filenames include the active range:
volume-by-status-7d.csvcost-breakdown-30d.csvtop-failures-2026-05-01_to_2026-05-15.csv
Endpoints under /v1/analytics/*.csv are streaming + tenant-scoped via
the calls:read API scope. They accept the same range, direction,
start, end, and breakdown query params as the JSON endpoints, plus
an additional group column in the CSV when breakdown is set.
curl "https://api.openphn.com/v1/analytics/volume-by-status.csv?range=7d&direction=inbound" \
-H "Authorization: Bearer sk_live_..." \
-o inbound-volume-7d.csvAPI summary
The /analytics page is a thin client over these endpoints:
| Endpoint | What it returns |
|---|---|
GET /v1/analytics/summary | Top-of-page KPI envelope (volume, success rate, cost, latency). |
GET /v1/analytics/volume-by-status | Per-bucket call counts by status. |
GET /v1/analytics/latency-percentiles | p50 + p95 first-audio + turn latencies. |
GET /v1/analytics/cost-breakdown | Spend split across voice / extraction / carrier. |
GET /v1/analytics/top-failures | Top-5 failure reasons with counts + percentages. |
GET /v1/analytics/outcome-quality | Heatmap of confidence × outcome-presence by template. |
GET /v1/analytics/*.csv | CSV of any of the above (same params). |
Every JSON endpoint accepts:
range—24h/7d/30d/90d/custom(withstart+end)direction—all/inbound/outboundstart+end— ISO-8601 timestamps forrange=customcompare—none(default) orprior. Whenprior, the response wraps in{current, prior, delta_pct}.breakdown—none(default),voice_engine,hour_of_day,day_of_week,carrier,number_id. When set, the response contains agroupsarray (top-8 + "Other") instead of an aggregate.
AI-generated dashboard panels (text-to-chart) and custom drag-drop dashboards are not in OpenPhn today. The fixed page covers most operator questions for AI-receptionist use cases. If you need a slice the current surface doesn't expose, the API + CSV exports cover the gap; otherwise ping us.