Analytics and reports

Read call and conversation KPIs, time series, campaign KPIs, generated reports and bulk data exports.
View as Markdown

Jelliu analyzes every call and text conversation your agents handle: outcome, sentiment, frustration, response latency, talk share, cost, and the fields each agent is configured to extract. The analytics API exposes those numbers as aggregates you can pull into your own BI, CRM or reporting stack.

There are four surfaces, each for a different job:

SurfaceBase pathUse it for
Dashboard bundle/api/dashboardOne request with the workspace overview, agents, campaigns, today’s call count and the agent leaderboard.
Analytics/api/analytics/*Windowed KPIs and time series for calls and text conversations, per campaign or per agent.
Reports/api/reportsComputed reports over an explicit date range: ROI, campaign comparison, cost per call, agent performance, contact funnel.
Exports/api/exportsAsynchronous CSV, JSON or JSONL files of raw rows: calls, contacts, conversations, daily analytics, agent actions and the audit log.

How it works

Analytics and reports are computed from the rows Jelliu stores for each call and conversation. Two consequences follow from that:

  • Numbers appear after analysis. A call counts in volume as soon as it exists, but its outcome, sentiment and extracted fields only appear once post-call analysis has written them.
  • Averages are over the rows that carry the measurement. A call with no sentiment score does not pull the average toward zero; it is simply not in the sample. Most responses include the sample size next to the average (sentimentCallCount, sampleSize, sentimentSampleSize) so you can tell a fleet-wide signal from a three-call one.

null means “no data”, never zero. Sentiment runs from -1 to 1, where 0 is a real, neutral reading. When nothing in the window was scored, analytics endpoints return avgSentiment: null, not 0. Treat null as “not measured” in your charts. (The generated reports under /api/reports are the exception: they report 0 when there is no data.)

Authentication and access

All analytics routes are reads, so a read key is enough for every GET on this page. The exceptions:

OperationKey needed
Every GET under /api/dashboard, /api/analytics, /api/reports/types and /api/exportsread
POST /api/reports (generate a report)write
POST /api/analytics/events, POST /api/analytics/events/batchwrite
POST /api/exports and DELETE /api/exports/{id}write
Creating, reading or downloading an export of type audit_log or agent_actionsfull

See Authentication for how scopes map to roles.

Common query parameters

Most analytics endpoints share the same window and filters:

days
integerDefaults to 30

Size of the window, counted back from now, from 1 to 365. An out-of-range or non-numeric value falls back to 30 rather than failing.

direction
stringDefaults to all

inbound, outbound or all. Applies to call metrics.

campaignId
string (uuid)

Scope every call metric to one campaign.

agentId
string (uuid)

Scope every call metric to one agent. The agent is the one that actually handled the call, so manual calls and calls from reassigned campaigns are attributed correctly.

timezone
string

IANA timezone such as America/Bogota, used to bucket days and hours. An unknown timezone falls back to UTC.

A malformed campaignId or agentId (not a UUID) returns 400 VALIDATION_FAILED with the message Invalid query parameters. Filters are never silently dropped.

Dashboard bundle

GET /api/dashboard returns what the dashboard’s home page needs in one request.

tz
stringDefaults to UTC

Timezone used to decide what “today” means for callsToday.

curl -sS "https://api.jelliu.co/api/dashboard?tz=America/Bogota" \
-H "Authorization: Bearer $JELLIU_API_KEY"
FieldTypeDescription
overviewobject or nullThe 30-day workspace overview (same shape as /api/analytics/overview). null if that section could not be computed.
agentsarrayThe workspace’s agents.
campaignsarrayThe workspace’s campaigns.
callsTodayintegerCalls created today, as a calendar day in tz.
agentRankingarray30-day per-agent metrics (same shape as /api/analytics/agent-ranking).

Read per-agent performance from agentRanking, not from the metric fields on the agents list: those list fields are placeholders and are not computed here.

Call analytics

Workspace overview

GET /api/analytics/overview accepts days, direction, campaignId and agentId.

{
"data": {
"totalInteractions": 1240,
"completedInteractions": 982,
"successes": 214,
"successRate": 21.79,
"avgDurationSeconds": 143,
"avgSentiment": 0.31,
"sentimentCallCount": 911,
"totalAgents": 4,
"totalCampaigns": 6,
"activeCampaigns": 2,
"totalContacts": 5120,
"convertedContacts": 388,
"dncContacts": 41,
"channelBreakdown": [
{
"channel": "voice",
"category": "sales",
"interactions": 1240,
"completed": 982,
"successes": 214,
"successRate": 21.79,
"successRateLabel": "conversionRate",
"avgDurationSeconds": 143,
"excludedMetrics": []
}
],
"outcomeBreakdown": { "sale_closed": 214, "rejected": 402, "callback_scheduled": 96 }
}
}
  • Rates such as successRate are percentages from 0 to 100, rounded to two decimals.
  • A “success” depends on the campaign category (a sale for sales, a booking for scheduling, and so on). successRateLabel names it.
  • avgDurationSeconds is null when no voice channel is involved, and excludedMetrics lists metrics that do not apply to a channel or category.

Overview by direction

GET /api/analytics/overview/by-direction?days=90 splits call metrics into inbound, outbound and total. Each has totalCalls, avgDuration (null without voice calls), successRate, avgSentiment and sentimentCallCount.

Time series

EndpointOne row perFields
GET /api/analytics/calls-per-daydaydate (YYYY-MM-DD), total, completed, successes, rejected, avgSentiment, avgFrustration, avgDurationSeconds, avgLatencyMs, costUsd, interruptions
GET /api/analytics/calls-by-hourhour of day (0 to 23)hour, total, completed, successes, completionRate, successRate, avgSentiment
GET /api/analytics/sentiment-trendday with at least one scored calldate, avgSentiment, avgFrustration, avgSentimentLow, callCount (scored calls that day, not the day’s total)

All three accept days, direction, timezone, campaignId and agentId. Days with no calls are omitted, not returned as zero rows; fill the gaps on your side if your chart needs a continuous axis.

curl -sS -G "https://api.jelliu.co/api/analytics/calls-per-day" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
--data-urlencode "days=14" \
--data-urlencode "timezone=America/Mexico_City" \
--data-urlencode "agentId=7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4"
{
"data": [
{
"date": "2026-09-13",
"total": 88,
"completed": 71,
"successes": 17,
"rejected": 22,
"avgSentiment": 0.28,
"avgFrustration": 0.19,
"avgDurationSeconds": 131,
"avgLatencyMs": 1180,
"costUsd": 13.6412,
"interruptions": 64
},
{
"date": "2026-09-14",
"total": 12,
"completed": 9,
"successes": 0,
"rejected": 3,
"avgSentiment": null,
"avgFrustration": null,
"avgDurationSeconds": 97,
"avgLatencyMs": null,
"costUsd": null,
"interruptions": null
}
]
}

Conversation quality

GET /api/analytics/quality describes how calls went rather than how they ended. It accepts days, direction, campaignId and agentId. Every block carries its own sampleSize, because older calls do not have every measurement.

BlockFields
top levelwindowDays, totalCalls
latencysampleSize, avgMs, p50Ms, p90Ms, maxMs (agent response latency)
talksampleSize, agentSeconds, userSeconds, agentShare (percentage of spoken time that was the agent)
turnssampleSize, agentTurns, userTurns, interruptions, interruptionRate (percentage of agent turns cut off), interruptionsPerCall
sentimentsampleSize, avg, frustrationSampleSize, avgFrustration, frustratedCalls (frustration peaked at 0.5 or above), lowSampleSize, avgLow
costsampleSize, credits, usd, llmUsd, perCallUsd, perMinuteUsd
successScoresampleSize, avg
durationsampleSize, avgSeconds (completed calls)
terminationReasons, languages, sourcesUp to 12 entries each, most frequent first: { reason, count }, { language, count }, { source, count }

Cost figures in analytics describe what the calls consumed on the voice platform. They are operational telemetry, not your Jelliu invoice. For plan usage, see Billing and usage.

Agent ranking

GET /api/analytics/agent-ranking returns up to 50 agents, ordered by successes and then by call volume. It accepts days, direction, campaignId and agentId.

FieldDescription
agentId, agentNameThe agent.
totalCalls, completed, successes, rejectedCounts in the window.
conversionRateSuccesses over completed calls, 0 to 100.
rejectionRateRejections over all calls, 0 to 100.
avgSentiment, sentimentCallCount, avgFrustrationSentiment and its sample size.
avgDurationSeconds, avgLatencyMs, latencyCallCountDuration and response latency.
interruptionRate, agentTalkSharePercentages, or null without data.
costUsd, costPerCallUsd, avgSuccessScoreCost and the analysis success score.

Campaign analytics

EndpointReturns
GET /api/analytics/campaigns/{campaignId}One campaign: campaignId, channel, category, totalInteractions, completedInteractions, successes, successRate, successRateLabel, averageDurationSeconds (voice only), avgSentiment, outcomeBreakdown, excludedMetrics. Accepts direction.
GET /api/analytics/top-campaignsCampaigns ranked by success rate. Accepts days, limit (1 to 100, default 10), direction and agentId. Each row: campaignId, campaignName, agentName, status, channel, category, totalInteractions, completed, successes, rejected, successRate, rejectionRate, successRateLabel, avgDurationSeconds, avgSentiment, avgFrustration, avgLatencyMs, costUsd, costPerCallUsd.

A campaign analytics response may include _degraded, a list of sections that could not be computed (aggregates, outcomeBreakdown). The rest of the response is still valid. An unknown campaign returns 404 CAMPAIGN_NOT_FOUND.

Evaluation criteria and extracted data

Agents can be configured with success criteria and data-collection fields. These two endpoints roll up the results across both voice calls and text conversations:

EndpointReturns
GET /api/analytics/analysis/criteriawindowDays, agentId, and criteria: one entry per criterion with id, success, failure, unknown, total, and bySource.voice / bySource.text with the same counts.
GET /api/analytics/analysis/data-collectionwindowDays, agentId, and fields: one entry per field with id and top, the 10 most frequent values, each with value, count, voice and text.

Both accept days (1 to 365, default 30) and agentId. Here an invalid agentId returns 400 with the message Invalid agentId: expected a UUID. A verdict that is not success or failure is counted as unknown, so totals always add up.

Text conversation analytics

Call analytics read calls only. These endpoints cover WhatsApp, web chat, email, Instagram and Messenger threads:

EndpointQueryReturns
GET /api/analytics/conversations/overviewdays, channeltotal, analysed, pending, notAnalysable, outcomes (success, failure, unknown), avgSentiment, sentimentSampleSize, byChannel, days
GET /api/analytics/conversations/criteriadays, channelPer criterion: success, failure, unknown, successRate
GET /api/analytics/conversations/sentiment-trenddays, timezonePer day: avgSentiment, sentimentSampleSize, total
GET /api/analytics/conversations/attentiondays, limit (1 to 50, default 10)The threads most worth a human’s attention: id, channel, summary, sentimentScore, outcome, createdAt
GET /api/analytics/conversations/field/{field}daysDistribution of one extracted field: value, count
  • channel is one of whatsapp, webchat, email, instagram, messenger.
  • field must be a simple identifier (a letter, then letters, digits or underscores, up to 64 characters).
  • pending threads are still being analyzed and clear by themselves. notAnalysable threads will never be analyzed, so they are a permanent gap in the denominator.
  • In the conversation criteria endpoint, successRate is a ratio from 0 to 1 over decided verdicts only (unknown is excluded), or null if nothing was decided. This differs from the 0 to 100 percentages used by call analytics.
  • In the field breakdown, a thread where the agent extracted the field but found no value is reported with the literal value (sin dato).

Invalid parameters here return 400 VALIDATION_FAILED with the message Request validation failed and an issue list in details.

Page bundle

GET /api/analytics/page-load computes the whole analytics page in one request. It accepts days, limit, direction, timezone, campaignId and agentId, and returns:

FieldSame shape as
overview/api/analytics/overview
overviewByDirection/api/analytics/overview/by-direction
topCampaigns/api/analytics/top-campaigns
callsPerDay, callsByHour, sentimentTrendThe time series above
agentRanking/api/analytics/agent-ranking
quality/api/analytics/quality
conversations.overview, conversations.byChannel, conversations.criteria, conversations.sentimentTrendThe text conversation endpoints

Each section is computed independently. If one fails, it comes back as null (objects) or [] (lists) and the rest of the bundle is unaffected, so check for null before reading nested fields. The campaign filter is not applied to the conversations sections, because text threads do not belong to campaigns.

Campaign KPIs

Each campaign category has business KPIs derived from the data the agent collects (for example revenue_total and avg_ticket for sales, appointments_booked for scheduling, nps_average for surveys).

EndpointQueryReturns
GET /api/analytics/kpi/{campaignId}dayscategory, kpis (each key, label, value, count, unit, optional trend), and breakdown (distribution items with label, value, count, percentage)
GET /api/analytics/kpi/{campaignId}/timeserieskpi (required, a KPI key), daysArray of date, value, count
GET /api/analytics/kpi/{campaignId}/objectionsdays, limit (1 to 50, default 10)Array of objection, count, percentage, optional trend
GET /api/analytics/kpi/tenant-summarydaystotalCalls, totalSuccessful, successRate, avgDuration, categorySummaries
GET /api/analytics/kpi/templates/{category}noneThe data-collection fields and success evaluation suggested for a category

unit is one of currency, count, percentage, score or seconds. category is one of sales, support, scheduling, surveys, collections, retention, notifications, interview, language_assessment, personal, general.

curl -sS -G "https://api.jelliu.co/api/analytics/kpi/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90/timeseries" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
--data-urlencode "kpi=revenue_total" \
--data-urlencode "days=30"

Custom events

You can record your own product events against the workspace and query them back.

MethodPathBody or query
POST/api/analytics/eventsevent (required, 1 to 200 characters: letters, digits, ., _, -, spaces), properties (object, up to 50 keys), timestamp (ISO 8601, defaults to now). Returns 202 with { "data": { "accepted": true } }.
POST/api/analytics/events/batchevents: 1 to 100 events of the same shape. Returns 202 with accepted and count. Limited to 30 requests per minute.
GET/api/analytics/eventsdays (1 to 90, default 7), event (exact name), limit (1 to 500, default 50), offset, or cursor (the timestamp of the last row you received, for keyset pagination; takes precedence over offset). Newest first.
GET/api/analytics/events/summarydays (1 to 90, default 7). Up to 100 { event_name, count } rows, most frequent first.

Each event row has id, tenant_id, user_id, event_name, properties, source, timestamp and created_at.

Reports

Reports compute a finished analysis over an explicit date range and return it inline.

GET /api/reports/types lists the available types:

typeWhat it computes
roi_analysisMinutes used, estimated cost, estimated cost of human agents for the same minutes, savings and ROI.
campaign_comparisonCampaigns ranked by success rate, with calls, duration, sentiment and cost.
cost_per_callDaily cost per call and cost per minute, with totals.
agent_performanceAgents ranked by success rate, with outcome breakdowns, duration and sentiment.
contact_funnelContacts by status (pending, called, converted, dnc, plus any other status) with percentages and a conversion rate.

Generate a report

POST /api/reports

type
stringRequired

One of the types above.

period
stringRequired

daily, weekly, monthly, quarterly or custom. A label for the report; the range is set by date_from and date_to.

date_from
stringRequired

ISO 8601 date-time with offset (2026-08-01T00:00:00-05:00) or a date (2026-08-01).

date_to
stringRequired

Same format. The range cannot exceed one year.

campaign_ids
string[] (uuid)

Restrict roi_analysis and campaign_comparison to these campaigns. contact_funnel uses only the first ID. cost_per_call and agent_performance ignore it.

format
stringDefaults to json

Accepts json or pdf. Reports are always returned as JSON.

curl -sS -X POST "https://api.jelliu.co/api/reports" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "campaign_comparison",
"period": "monthly",
"date_from": "2026-08-01T00:00:00Z",
"date_to": "2026-08-31T23:59:59Z"
}'
{
"data": {
"type": "campaign_comparison",
"period": { "from": "2026-08-01T00:00:00Z", "to": "2026-08-31T23:59:59Z" },
"campaigns": [
{
"campaignId": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
"campaignName": "Renovaciones agosto",
"totalCalls": 812,
"successRate": 24.1,
"avgDurationSeconds": 151,
"avgSentiment": 0.34,
"totalCost": 285.62,
"rank": 1
}
]
}
}

Report shapes

typedata fields (besides type and period)
roi_analysistotalCalls, successfulCalls, avgDurationSeconds, totalMinutesUsed, costPerMinute, totalCost, estimatedHumanAgentCost, savingsVsHuman, roiPercent, costPerSuccessfulCall
campaign_comparisoncampaigns[]: campaignId, campaignName, totalCalls, successRate, avgDurationSeconds, avgSentiment, totalCost, rank
cost_per_calltimeSeries[]: date, totalCost, callCount, costPerCall, costPerMinute; totals: totalCost, totalCalls, avgCostPerCall, avgCostPerMinute
agent_performanceagents[]: agentId, agentName, totalCalls, successRate, avgDurationSeconds, avgSentiment, outcomeBreakdown, rank
contact_funnelstages[]: stage, count, percentage; totalContacts, conversionRate

Costs and ROI in reports are estimates: they multiply minutes by a platform-wide cost per minute and compare against a reference hourly rate for human agents. Use them for trends and comparisons, not as billing figures.

A plain date such as "2026-08-31" means the start of that day, so a range ending on "2026-08-31" excludes August 31. To include the whole last day, send a date-time such as "2026-08-31T23:59:59Z" or the next day’s date.

Exports

Exports produce a file of raw rows asynchronously. You create a job, poll it until it completes, then download the file.

Export types and filters

typeColumnsSupported filters
callsid, phone_number, status, outcome, duration_seconds, sentiment_score, summary, started_at, ended_at, call_directiondate_from, date_to, campaign_id, agent_id, status, outcome
contactsname, phone_number, email, status, call_attempts, last_called_at, campaign_namedate_from, date_to, campaign_id, agent_id, status
conversationsid, channel, status, created_at, agent_name, message_countdate_from, date_to, agent_id, status
analyticsOne row per day and campaign: date, campaign_name, total_calls, avg_duration_seconds, success_rate, avg_sentimentdate_from, date_to, campaign_id, agent_id, status
agent_actionsEvery tool call an agent made, with parameters (redacted), outcome, duration and cost attributiondate_from, date_to, agent_id, status (the action outcome)
audit_logThe workspace audit logdate_from, date_to

A filter the type cannot apply is rejected, never ignored:

{
"error": {
"code": "VALIDATION_FAILED",
"message": "Export type \"conversations\" cannot filter by campaign_id. Supported filters for \"conversations\": date_from, date_to, agent_id, status."
}
}

Create an export

POST /api/exports

type
stringRequired

calls, contacts, conversations, analytics, agent_actions or audit_log.

format
stringDefaults to csv

csv, json (one array) or jsonl (one JSON object per line; best for large files and SIEM ingestion).

filters
object

date_from and date_to (ISO 8601 date-times), campaign_id, agent_id (UUIDs), status, outcome. Only the filters supported by the type.

1

Create the job

curl -sS -X POST "https://api.jelliu.co/api/exports" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "calls",
"format": "csv",
"filters": {
"date_from": "2026-09-01T00:00:00Z",
"date_to": "2026-09-14T23:59:59Z",
"outcome": "sale_closed"
}
}'

The response is 202 Accepted with the job:

{
"data": {
"id": "3e1f0a9c-7b2d-4c6e-8f10-2a3b4c5d6e7f",
"tenant_id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"user_id": "user_2abcDEF123",
"type": "calls",
"format": "csv",
"status": "pending",
"filters": { "date_from": "2026-09-01T00:00:00Z", "date_to": "2026-09-14T23:59:59Z", "outcome": "sale_closed" },
"row_count": null,
"file_size_bytes": null,
"file_url": null,
"error": null,
"started_at": null,
"completed_at": null,
"expires_at": null,
"created_at": "2026-09-14T16:02:11.412Z"
}
}
2

Poll until it finishes

GET /api/exports/{id} returns the same object. Poll every few seconds until status is completed or failed. When completed, row_count, file_size_bytes, completed_at and expires_at are set. When failed, error explains why. file_url is internal bookkeeping: always download through the endpoint below.

GET /api/exports?limit=50&offset=0 lists jobs, newest first (limit 1 to 100).

3

Download the file

GET /api/exports/{id}/download streams the file as an attachment named {type}_export_{id}.{csv|json|jsonl}, with Content-Type text/csv, application/json or application/x-ndjson.

curl -sS -L "https://api.jelliu.co/api/exports/3e1f0a9c-7b2d-4c6e-8f10-2a3b4c5d6e7f/download" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-o calls.csv
4

Clean up (optional)

DELETE /api/exports/{id} removes a completed or failed job and returns 204. Deleting a job that is still pending or processing returns 400 with Can only delete completed or failed export jobs.

Export limits

LimitValue
Rows per file100,000. A larger result is truncated, and the completed job’s error field reads Results truncated to 100000 rows. Narrow the date range and export in slices.
Jobs in progress3 per workspace (pending plus processing). A fourth returns 429 RATE_LIMIT_EXCEEDED: Too many in-progress exports (max 3). Wait for an existing export to finish before starting another.
File lifetime72 hours after completion.
Creating and deleting jobsShare the 10 requests per minute budget for configuration changes. See Rate limits.

audit_log and agent_actions exports contain your compliance record. Creating, reading and downloading them requires a full key, checked against the job’s type on every request, so a lower-scoped key cannot download a file an administrator created.

A job that is not completed yet returns 404 with Export file is not ready for download. An expired file returns 410 with Export file has expired, or 404 with Export file has expired or is no longer available. Create a new export in either case.

Errors

StatusCodeWhen
400VALIDATION_FAILEDInvalid query or body: bad UUID, unsupported export filter, report range over a year, invalid KPI category.
403FORBIDDENThe key’s scope does not cover the request, including audit_log and agent_actions exports without full.
403BILLING_ERRORThe workspace plan does not include the analytics, report or export feature. All current plans include them.
404CAMPAIGN_NOT_FOUNDThe campaign does not exist in your workspace.
404NOT_FOUNDUnknown export job, or the file is not ready or no longer available.
410NOT_FOUNDThe export file expired.
429RATE_LIMIT_EXCEEDEDA rate limit, or 3 exports already in progress. Honor Retry-After.

See Errors for the envelope.

Limits and caching

  • Rate limits. Analytics endpoints are limited to 30 requests per minute per workspace, on top of the general limit. Use /api/analytics/page-load or /api/dashboard instead of many individual calls. See Rate limits.
  • Freshness. Aggregates are cached on the server for up to a few minutes and responses carry Cache-Control: private, max-age=30, stale-while-revalidate=900. A call that just ended may take a short while to show up. Do not poll analytics faster than once a minute.
  • Conditional requests. overview, agent-ranking, quality, top-campaigns and /api/dashboard return a weak ETag. Send it back in If-None-Match to receive 304 Not Modified when nothing changed.
  • Windows. days is capped at 365 for analytics and at 90 for custom events. Reports cover at most one year.

For real-time pipelines, do not poll analytics. Subscribe to the call.completed webhook, which carries each call’s outcome, sentiment, summary and extracted data as soon as analysis finishes, and aggregate on your side. See Webhooks.