Analytics and reports
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:
How it works
Analytics and reports are computed from the rows Jelliu stores for each call and conversation. Three 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. - Rates are recomputed on every read, never stored. Which is what lets a change in how Jelliu counts apply to your whole history at once, with no backfill — and also means a chart you drew last week over the same window may no longer match.
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.)
Before you chart a rate, read Counting and coverage. It is the contract for what every fraction on this page divides by: the no_effective_contact bucket for calls that connected and never became a conversation, the analysed coverage figure that travels beside each rate, and where a calendar day begins for your workspace. Since 2026-09-16 every success rate here divides by effectiveCompleted rather than by every connected call.
Authentication and access
All analytics routes are reads, so a read key is enough for every GET on this page. The exceptions:
See Authentication for how scopes map to roles.
Without an active subscription
Advanced analytics, reports and analytics exports are part of every plan, and they need the workspace to have an active subscription (active, trialing or past_due). Without one:
The refusal carries metadata.kind: "subscription_required":
Reports name Reportes personalizados and analytics exports name Exportación de analítica in the same sentence. Raw exports stay open so a workspace can always take its data out. See Without an active subscription.
Common query parameters
Most analytics endpoints share the same window and filters:
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.
inbound, outbound or all. Applies to call metrics.
Scope every call metric to one campaign.
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.
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.
Timezone used to decide what “today” means for callsToday.
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.
- Rates such as
successRateare percentages from 0 to 100, rounded to two decimals. - A “success” depends on the campaign category (a sale for
sales, a booking forscheduling, and so on).successRateLabelnames it. successRateissuccessesovereffectiveCompleted, not overcompletedInteractions.effectiveCompletedis the connected calls that lasted long enough and carried enough turns to have gone either way;noEffectiveContactis the rest of the connected calls. See Counting and coverage.analysedoftotalInteractionsis the coverage: how many of the window’s calls an evaluation actually ran on. A rate whoseanalysedis far belowtotaldescribes the analysed slice, not the window.outcomeBreakdowncarries ano_effective_contactkey holding every call that carries an outcome and failed the threshold. It is not a stored outcome and never appears on a call record — the buckets are disjoint, so the slices still sum to the window’s calls with an outcome.avgDurationSecondsisnullwhen no voice channel is involved, andexcludedMetricslists metrics that do not apply to a channel or category.channelBreakdownrows carryeffectiveCompletedandanalysedbut notnoEffectiveContact; derive it ascompleted - effectiveCompletedfor a channel if you need it.
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), successes, effectiveCompleted, noEffectiveContact, analysed, successRate, avgSentiment and sentimentCallCount. successRate is successes over effectiveCompleted. It accepts days, campaignId and agentId, which scope all three blocks. A malformed query (a campaignId or agentId that is not a UUID, an unknown direction, a timezone over 50 characters) returns 400 VALIDATION_FAILED with the message Invalid query parameters.
Time series
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.
Since 2026-09-16, successes and rejected in these series exclude calls with no effective contact, and the hour heatmap’s successRate divides by effectiveCompleted. An hour whose calls were all two-second pick-ups is not a bad hour to call — it is an hour with no data, and the heatmap’s job is to tell those apart. noEffectiveContact carries the count that used to be folded into rejected, and analysed beside total says how much of the day or hour an evaluation ran on.
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.
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.
Campaign analytics
In both, successRate divides by effectiveCompleted and the league table is sorted by the figure it prints. Before 2026-09-16 a campaign whose calls were mostly two-second pick-ups sank to the bottom of the ranking for calls nobody could have converted, and carried a rejection rate nobody had voiced.
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:
Both accept days (1 to 365, default 30) and agentId. Here an invalid agentId returns 400 with the message Invalid agentId: expected a UUID. Like the rest of /api/analytics, they need the owner, admin, member or viewer role and an active subscription, and they share the 30 requests per minute analytics budget. A verdict that is not success or failure is counted as unknown, so totals always add up.
suppressed (since 2026-09-16) counts verdicts left out of success, failure, unknown and total because the call had no effective contact. A two-second call cannot have resolved a query or closed a sale, so grading it failure fills the table with failures nobody could have avoided. Safety criteria — did the agent state something untrue, did it name the technology behind it — are not suppressed: those failures are real whatever the call’s length. Text conversations have no duration and are never suppressed. suppressed: 140 with failure: 0 reads very differently from total: 140 with failure: 0, which is why the number is reported rather than dropped. See Counting and coverage.
Text conversation analytics
Call analytics read calls only. These endpoints cover WhatsApp, web chat, email, Instagram and Messenger threads:
channelis one ofwhatsapp,webchat,email,instagram,messenger.fieldmust be a simple identifier (a letter, then letters, digits or underscores, up to 64 characters).pendingthreads are still being analyzed and clear by themselves.notAnalysablethreads will never be analyzed, so they are a permanent gap in the denominator.- In the conversation criteria endpoint,
successRateis a ratio from 0 to 1 over decided verdicts only (unknownis excluded), ornullif 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:
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).
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.
tenant-summary’s successRate changed denominator on 2026-09-16, and the change is large. It was totalSuccessful / totalCalls — every row in the window, rings nobody answered and failed dials included. It is now totalSuccessful / effectiveCompleted, the same definition every other analytics surface uses. A workspace converting two thirds of the people it actually spoke to used to read around 12% on this card and no other screen agreed with it. totalCalls is still reported: it is the volume, not the denominator. analysedCalls of totalCalls is the coverage.
Support KPIs split escalations from unresolved calls
On 2026-09-15 the support category went from five KPIs to six.
escalated_count used to count rejected as well as escalated_to_human, and its label said so (No resueltos / escalados). That was a workaround: nothing wrote escalated_to_human except the admin warm transfer, which almost nobody uses, so without rejected the card would have sat at a permanent zero. Now that the agent’s own hand-overs are recorded the outcome has real producers, so the two numbers are separated and each is named for what it is.
If you chart or alert on escalated_count, it will drop at the cut-over — the rejected half of it moved to unresolved_count. Add the new key and, for a like-for-like comparison with history, sum the two. The two are disjoint by construction: a call has exactly one outcome, so none feeds both.
KPI days are your workspace’s days
Since 2026-09-15 a KPI row is filed under the calendar day in your workspace’s timezone, and KPIs whose key ends in _today (revenue_today) cover the current local day. Before that it was the UTC day, which for a workspace at UTC-5 flipped at 19:00 local — an evening’s sales left “today” and reappeared the next morning. A workspace with no timezone set, or one whose stored value is not a valid IANA id, falls back to America/Bogota.
This is forward only. Rows written before 2026-09-15 stay on the UTC day they were filed under and were not backfilled, so a range that spans that date contains both kinds and older rows can sit one day off for workspaces west of UTC. The same applies to the daily series from GET /api/analytics/kpi/{campaignId}/timeseries.
This does not change the explicit timezone / tz query parameters described under Common query parameters: those still default to UTC and still mean exactly what you pass.
The same workspace clock decides the day everywhere you cannot name a zone yourself — a bare date_from / date_to in a report body, the cost_per_call daily series, and the date column of an analytics export. Internally that is a date_trunc('day', … AT TIME ZONE …) on your workspace’s IANA zone, not on UTC. Counting and coverage has the full table of which surface reads which clock.
Custom events
You can record your own product events against the workspace and query them back.
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:
Reports count connections, analytics counts conversations. These reports never measured success, and since 2026-09-16 their field names say so. They do not apply the no-effective-contact bucket: a report’s denominator is dials and connections, which answers a different and equally useful question — how much of your dialling reached a human at all. Read a success rate from /api/analytics; read a connection rate from here. The two are not supposed to match.
Generate a report
POST /api/reports
One of the types above.
Optional. daily, weekly, monthly, quarterly or custom. A label only: the range is set by date_from and date_to, and the report is the same whether you send it or not.
ISO 8601 date-time with offset (2026-08-01T00:00:00-05:00), or a bare date (2026-08-01), which since 2026-09-15 means 00:00 on that day in your workspace’s timezone.
Same format. A bare date means the last instant of that day in your workspace’s timezone, so the day itself is included — it used to stop at midnight UTC and leave the day out. The range cannot exceed one year.
Restrict roi_analysis and campaign_comparison to these campaigns. contact_funnel uses only the first ID. cost_per_call and agent_performance ignore it.
Only json. Any other value, including pdf, is refused with 400 VALIDATION_FAILED and the message Formato de reporte no disponible: solo se puede generar en "json". La exportación en PDF no está disponible. Omit the field or send json.
Report shapes
avgDurationSeconds and avgSentiment are null when no call in the group carries that measurement — not 0. outcomeBreakdown in agent_performance holds the stored outcomes and, unlike the analytics endpoints, contains no no_effective_contact key.
Report fields were renamed
Since 2026-09-16. Three report fields were renamed. No figure changed — the arithmetic is byte-for-byte what it was. Only the words were wrong.
successfulCalls was status = 'completed': the call connected. successRate was connected calls over dialled calls: the share of dials that were answered. Neither ever measured success. A report headed “Tasa de éxito: 82%” that means “82% of our dials connected” is the kind of wrong a customer repeats to their own board.
The old names are still sent, with the same values, so a client reading successRate today keeps working and reads exactly what it read yesterday.
The old names are kept for one release. They will be removed once the dashboard, the Zapier app and the MCP tools are on the new names — there is no longer window than that, and no separate sunset date to wait for. Switch on your next deploy.
If what you actually wanted was a success rate, the old field was never it. Read successRate from GET /api/analytics/overview, which divides by conversations rather than by dials.
campaign_comparison and agent_performance are sorted by connectionRate, which is the same ordering successRate produced, since the two carry the same value.
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" in date_to covers the whole of that day in your workspace’s timezone, so the day itself is included. It used to mean the start of the day, which made a one-day report (date_from equal to date_to) always come back empty. A full date-time is still taken exactly as given: send an instant and you mean that instant, on whatever offset you wrote.
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
success_rate in the analytics export changed denominator on 2026-09-16. It divided by COUNT(*) — every row of the day, unanswered rings included — while the dashboard’s rate for the same day divided by completed calls. Two numbers, both labelled “success rate”, which customers reconciled against each other and then asked which one was real. It now divides by effective_completed_calls, the definition every surface shares, and the three new columns let you check it line by line: an export is reconciled by hand, so a rate without its coverage is the one number here nobody can verify.
A filter the type cannot apply is rejected, never ignored:
Create an export
POST /api/exports
calls, contacts, conversations, analytics, agent_actions or audit_log.
csv, json (one array) or jsonl (one JSON object per line; best for large files and SIEM ingestion).
date_from and date_to (full ISO 8601 date-times — unlike POST /api/reports, a bare YYYY-MM-DD is rejected here), campaign_id, agent_id (UUIDs), status, outcome. Only the filters supported by the type.
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).
Export 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
See Errors for the envelope.
Limits and caching
- Rate limits. Every endpoint under
/api/analytics,/api/analytics/analysis/*included, shares a budget of 30 requests per minute per workspace, on top of the general limit. Use/api/analytics/page-loador/api/dashboardinstead 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-campaignsand/api/dashboardreturn a weakETag. Send it back inIf-None-Matchto receive304 Not Modifiedwhen nothing changed. - Windows.
daysis 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.

