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
}
]
}
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
| Param | Default | Notes |
|---|---|---|
window | today | today, yesterday, 7d, custom — see the warning below |
start_date | — | Required when window=custom, YYYY-MM-DD |
end_date | — | Required when window=custom |
force | false | true 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.
| Status | Cause |
|---|---|
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"} |
custom for periods longer than 7 daysStored 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.