OpenPhn docs
Concepts

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 hereLands 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:

DimensionWhat it shows
Voice enginePer-engine performance during the A/B (Gemini vs Grok, etc.).
Hour of dayWhen your calls happen + which hours perform best.
Day of weekWeekday vs weekend skew.
CarrierWhich carrier is failing / leading.
Phone numberPer-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_day

CSV 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.csv
  • cost-breakdown-30d.csv
  • top-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.csv

API summary

The /analytics page is a thin client over these endpoints:

EndpointWhat it returns
GET /v1/analytics/summaryTop-of-page KPI envelope (volume, success rate, cost, latency).
GET /v1/analytics/volume-by-statusPer-bucket call counts by status.
GET /v1/analytics/latency-percentilesp50 + p95 first-audio + turn latencies.
GET /v1/analytics/cost-breakdownSpend split across voice / extraction / carrier.
GET /v1/analytics/top-failuresTop-5 failure reasons with counts + percentages.
GET /v1/analytics/outcome-qualityHeatmap of confidence × outcome-presence by template.
GET /v1/analytics/*.csvCSV of any of the above (same params).

Every JSON endpoint accepts:

  • range24h / 7d / 30d / 90d / custom (with start + end)
  • directionall / inbound / outbound
  • start + end — ISO-8601 timestamps for range=custom
  • comparenone (default) or prior. When prior, the response wraps in {current, prior, delta_pct}.
  • breakdownnone (default), voice_engine, hour_of_day, day_of_week, carrier, number_id. When set, the response contains a groups array (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.

On this page