Campaign insights
GET /api/v1/campaign/{id}/insights — funnel, outcome breakdown, retry effectiveness, cost & duration, timeline, and pacing in one request.
GET /api/v1/campaign/{campaign_id}/insightsDeep campaign analytics in a single round trip. Everything is computed live from the ledger (contact-level funnel + per-attempt outcomes) and the campaign's call records (cost, duration, timeline) with indexed GROUP BYs — there are no cached aggregates, so the numbers are always current. One request returns seven sections: funnel, outcomes, retries, cost, duration, timeline, and pacing.
Cost and duration
cost and duration are aggregated over the campaign's completed calls.
Response
{
"funnel": { "contacts": 500, "dialed": 480, "connected": 310, "completed": 268 },
"outcomes": {
"by_outcome": { "completed": 268, "voicemail": 42, "no_answer": 90, "busy": 55, "failed": 25 },
"pending": 12,
"retry_scheduled": 8,
"dialing": 3,
"in_flight": 2,
"dispatch_failed": 4
},
"retries": {
"success_by_attempt": { "1": 220, "2": 40, "3": 8 },
"conversion_by_reason": {
"no_answer": { "total": 90, "completed": 34, "rate": 0.377 },
"busy": { "total": 55, "completed": 14, "rate": 0.254 }
},
"pending": [
{
"queued_run_id": 9021,
"phone_number": "+14155552698",
"retry_reason": "busy",
"scheduled_for": "2026-07-19T15:05:00Z"
}
]
},
"cost": {
"billed_calls": 310,
"total_usd": 12.74,
"avg_usd": 0.0411,
"per_connected_usd": 0.0411
},
"duration": {
"total_seconds": 18620.0,
"avg_seconds": 60.06,
"distribution": [
{ "bucket": "0-15s", "count": 40 },
{ "bucket": "15-30s", "count": 61 },
{ "bucket": "30-60s", "count": 92 },
{ "bucket": "1-2m", "count": 78 },
{ "bucket": "2-5m", "count": 34 },
{ "bucket": "5m+", "count": 5 }
]
},
"timeline": {
"bucket": "minute",
"buckets": [
{ "bucket_start": "2026-07-19T14:00:00Z", "dialed": 22, "connected": 14 },
{ "bucket_start": "2026-07-19T14:01:00Z", "dialed": 25, "connected": 17 }
]
},
"pacing": { "rate_limit_per_second": 5, "max_concurrency": 5, "in_flight": 2 }
}Sections
funnel
Contact-level funnel (each stage counts contacts, not attempts, so a contact retried three times counts once).
| Field | Meaning |
|---|---|
contacts | Total contacts in the campaign. |
dialed | Contacts with at least one dispatched attempt. |
connected | Contacts that reached the callee at least once (completed or voicemail). |
completed | Contacts with a completed call (ran to its natural end). |
outcomes
by_outcome is a per-attempt count of terminal call outcomes (keys are only those that occurred). The remaining fields are live, non-terminal attempt counts:
| Field | Meaning |
|---|---|
pending | Queued, not yet dialed (no scheduled_for). |
retry_scheduled | Queued for a future retry. |
dialing | Claimed by a batch, dialing now. |
in_flight | Dispatched; awaiting a terminal outcome. |
dispatch_failed | Attempt failed at dispatch (never reached the carrier — provider rejected, pool exhausted, etc.). |
retries
Retry effectiveness:
| Field | Meaning |
|---|---|
success_by_attempt | Completed calls keyed by attempt number ("1" = first try, "2" = first retry, …). |
conversion_by_reason | For each retry reason, how many retried attempts reached a terminal outcome (total), how many completed, and the rate (completed / total, or null when total is 0). |
pending | Up to 50 upcoming (future-scheduled) retries: queued_run_id, phone_number, retry_reason, scheduled_for. Future-only — already-fired retries are not listed here. |
cost (agent campaigns only, else null)
Aggregated over completed calls (status = 'completed'):
| Field | Meaning |
|---|---|
billed_calls | Number of completed (billed) calls. |
total_usd | Total cost across those calls. |
avg_usd | Average cost per billed call (null if none). |
per_connected_usd | total_usd divided by the funnel's connected-contact count (null if none connected). |
duration
| Field | Meaning |
|---|---|
total_seconds | Sum of call durations. |
avg_seconds | Average call duration (null if none). |
distribution | Histogram over fixed buckets: 0-15s, 15-30s, 30-60s, 1-2m, 2-5m, 5m+. Always all six buckets, count 0 where empty. |
timeline
Calls over time. bucket auto-scales to the campaign's wall-clock span: minute (under 3 hours), hour (under 3 days), or day (longer). Each entry carries bucket_start, dialed, and connected. A campaign that never started returns { "bucket": null, "buckets": [] } (an empty bucket list).
pacing
| Field | Meaning |
|---|---|
rate_limit_per_second | The campaign's dial-rate cap. |
max_concurrency | Configured max concurrent calls (null if unset). |
in_flight | Calls currently dispatched and awaiting an outcome (same value as outcomes.in_flight). |
Empty campaigns are safe
A campaign with no contacts (or one that hasn't started) returns zeros, empty lists, and nulls — never an error. There is no ETA field; the dashboard derives ETA client-side from the timeline and funnel.
Examples
curl https://dashboard.zoxa.ai/api/v1/campaign/7/insights \
-H "X-API-Key: zsk_..."Errors
| Status | detail | When |
|---|---|---|
404 | "Campaign not found" | Id not in your org. |
Related
GET /campaign/{id}/contacts— the per-contact ledger behind these aggregatesWS /campaign/{id}/events— refetch insights when a relevant event lands