Skip to main content

Reports

Generated documents summarising a window of calls — the thing you send to someone who is not going to log in and read a dashboard.

List reports​

GET /api/reports

{
"reports": [
{
"id": 18,
"uuid": "rp1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8",
"window": "7d",
"range": { "from": "2026-09-14T00:00:00Z", "to": "2026-09-21T00:00:00Z" },
"stats": { "...": "..." },
"narrative": { "...": "..." },
"leaderboard": { "...": "..." },
"customers": [],
"calls": [],
"goal_intelligence": [],
"generated_at": "2026-09-21T06:00:00Z",
"sent_at": null
}
]
}
The list returns whole reports, not summaries

Each entry is built by the same function as generate and carries the full stats, narrative, leaderboard, and the per-customer and per-call breakdowns. A workspace with many reports returns a large payload — read id, uuid, window, range and generated_at if all you need is a list to choose from.

The period is reported as range, with RFC 3339 timestamps. There are no start_date or end_date fields on a report — those names are query parameters on generate only. The timestamp is generated_at, not created_at.

Generate a report​

POST /api/reports/generate?window=7d

ParamDefaultNotes
windowtodaytoday, yesterday, 7d, custom — see the warning below
start_date—Required when window=custom, YYYY-MM-DD
end_date—Required when window=custom
forcefalsetrue or 1 regenerates instead of reusing
curl -X POST "https://your-host/api/reports/generate?window=custom&start_date=2026-09-01&end_date=2026-09-30" \
-H "Authorization: Bearer <token>"

The whole report comes back in the response — there is no second fetch:

{
"id": 1,
"uuid": "rp1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8",
"window": "custom",
"range": { "from": "2026-09-01T00:00:00Z", "to": "2026-10-01T00:00:00Z" },
"stats": {
"calls_total": 891,
"calls_inbound": 142,
"calls_outbound": 690,
"calls_uploaded": 59,
"picked_up": 543,
"pickup_rate": 0.61,
"avg_duration_seconds": 97.4,
"outcome_breakdown": { "follow_up_scheduled": 168 },
"sentiment_breakdown": { "positive": 312 },
"headline_metrics": {
"funnel": { "calls_total": 891, "picked_up": 543, "picked_up_rate": 0.61 }
},
"performance_score": { "current": 72, "previous": 64, "delta": 8 }
},
"narrative": {
"headline": "891 calls this period, 61% picked up, 57% positive sentiment.",
"wins": [],
"issues": [],
"recommended_action": "Review recent call summaries for follow-up opportunities."
},
"leaderboard": {
"ai_agents": [],
"human_agents": [
{
"agent_uuid": null,
"agent_name": "Arjun Rao",
"calls_total": 112,
"picked_up": 74,
"pickup_rate": 0.66,
"avg_duration_seconds": 104
}
],
"unassigned": []
},
"customers": [
{
"customer_id": 87,
"customer_uuid": "cu1a2b3c-…",
"name": "Priya Kumar",
"phone": "+918888888888",
"tags": ["north"],
"segment": "winback",
"calls_in_window": 4,
"last_call_at": "2026-09-20T14:33:54Z",
"last_call_id": 1422,
"outcome": "follow_up_scheduled",
"sentiment": "positive",
"summary": "Asked to be called back after the 27th.",
"next_action": null
}
],
"calls": [
{
"call_id": 1422,
"call_uuid": "ca1a2b3c-…",
"customer_name": "Priya Kumar",
"customer_phone": "+918888888888",
"started_at": "2026-09-20T14:32:01Z",
"duration_seconds": 113,
"outcome": "follow_up_scheduled",
"sentiment": "positive",
"agent_name": "Arjun Rao",
"agent_type": "human",
"summary": "Customer asked about delivery timing.",
"action_items": []
}
],
"goal_intelligence": [
{
"segment": "winback",
"achieved": 41,
"partially_achieved": 12,
"not_achieved": 9,
"pending": 3,
"flagged": 2,
"total": 65,
"achievement_rate": 0.63
}
],
"generated_at": "2026-09-23T10:28:18Z",
"sent_at": null,
"reused": true
}

Two things to note in stats. It carries eleven keys here — the nine the dashboard returns, plus headline_metrics and performance_score, which exist only on reports. And headline_metrics is a funnel of counts, not prose: the written summary is the separate top-level narrative block, which is model output and should be treated as a prompt for attention rather than a fact.

sent_at stays null until the report is emailed.

StatusCause
400{"detail": "invalid window"}
400{"detail": "start_date and end_date are required for window=custom"}
400{"detail": "start_date/end_date must be YYYY-MM-DD"}
Use custom for periods longer than 7 days

Stored reports support today, yesterday, 7d, and custom. For a 14- or 30-day report, pass window=custom with explicit start_date and end_date.

Reuse​

Generating a report for a window that already has one returns the existing report rather than building it again — reused is true in the response. Generation is expensive, and asking twice for last week's numbers should not cost twice.

Pass force=true when the underlying calls have changed: re-analysed calls, corrected segments, a late upload landing inside the window.

Download a report​

GET /api/reports/{id}/download

Streams the generated file.

Email a report​

POST /api/reports/{id}/send

Sends it to the recipients configured in settings.

Requires RESEND_API_KEY and RESEND_SENDER_EMAIL. Without them the send fails and says so, rather than reporting success and delivering nothing.

Delete a report​

DELETE /api/reports/{id} → 204

What a report contains​

Built from the same aggregates as the dashboard, plus per-customer and per-call detail for the window:

  • Volume, pickup rate, average duration
  • Outcome and sentiment breakdowns
  • A funnel across the call stages
  • Customer rows: calls in the window, last outcome, summary, next action
  • Call rows: the individual conversations behind the numbers

Automatic reports​

auto_reports_enabled in settings generates and sends reports on a schedule without anyone asking. Configure the recipients first — enabling it with an empty recipient list generates reports nobody receives.