Skip to navigation

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

Call or conversation ends Post-call analysis Calls and conversationsoutcome, sentiment, telemetry,criteria, extracted fields /api/dashboard /api/analytics/* /api/reports /api/exports job Worker writes file /api/exports/:id/download

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:

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.

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:

Refused with 403 BILLING_ERRORStill available
Every GET under /api/analytics (call and conversation analytics, evaluation criteria and extracted data under /api/analytics/analysis/*, KPIs, event reads)POST /api/analytics/events, POST /api/analytics/events/batch, GET /api/dashboard
POST /api/reports and GET /api/reports/typesNone
POST /api/exports with type: "analytics"Exports of calls, contacts, conversations, agent_actions and audit_log, and reading, downloading and deleting export jobs

The refusal carries metadata.kind: "subscription_required":

{
"error": {
"code": "BILLING_ERROR",
"message": "«Analítica avanzada» requiere una suscripción activa. Este espacio de trabajo no tiene un plan activo: elige uno en Ajustes → Plan para continuar. Tus datos siguen disponibles para consultar y exportar.",
"metadata": { "tier": "none", "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:

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,
"effectiveCompleted": 731,
"noEffectiveContact": 251,
"analysed": 874,
"successes": 214,
"successRate": 29.27,
"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,
"effectiveCompleted": 731,
"analysed": 874,
"successes": 214,
"successRate": 29.27,
"successRateLabel": "conversionRate",
"avgDurationSeconds": 143,
"excludedMetrics": []
}
],
"outcomeBreakdown": { "sale_closed": 214, "rejected": 151, "callback_scheduled": 96, "no_effective_contact": 379 }
}
}
  • 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.
  • successRate is successes over effectiveCompleted, not over completedInteractions. effectiveCompleted is the connected calls that lasted long enough and carried enough turns to have gone either way; noEffectiveContact is the rest of the connected calls. See Counting and coverage.
  • analysed of totalInteractions is the coverage: how many of the window’s calls an evaluation actually ran on. A rate whose analysed is far below total describes the analysed slice, not the window.
  • outcomeBreakdown carries a no_effective_contact key 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.
  • avgDurationSeconds is null when no voice channel is involved, and excludedMetrics lists metrics that do not apply to a channel or category.
  • channelBreakdown rows carry effectiveCompleted and analysed but not noEffectiveContact; derive it as completed - effectiveCompleted for 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

EndpointOne row perFields
GET /api/analytics/calls-per-daydaydate (YYYY-MM-DD), total, completed, effectiveCompleted, successes, rejected, noEffectiveContact, analysed, avgSentiment, avgFrustration, avgDurationSeconds, avgLatencyMs, costUsd, interruptions
GET /api/analytics/calls-by-hourhour of day (0 to 23)hour, total, completed, effectiveCompleted, successes, noEffectiveContact, analysed, 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.

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.

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,
"effectiveCompleted": 54,
"successes": 17,
"rejected": 14,
"noEffectiveContact": 17,
"analysed": 63,
"avgSentiment": 0.28,
"avgFrustration": 0.19,
"avgDurationSeconds": 131,
"avgLatencyMs": 1180,
"costUsd": 13.6412,
"interruptions": 64
},
{
"date": "2026-09-14",
"total": 12,
"completed": 9,
"effectiveCompleted": 0,
"successes": 0,
"rejected": 0,
"noEffectiveContact": 9,
"analysed": 0,
"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 }. source uses the values of a call’s initiation_source, for example phone for a call over a number bought through Jelliu, sip_trunk or js_sdk.

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. rejected counts explicit refusals only.
effectiveCompletedConnected calls that became conversations — the denominator of conversionRate.
noEffectiveContactConnected calls that never became conversations. Its own column, never a refusal.
analysedCalls an evaluation ran on, beside totalCalls.
conversionRateSuccesses over effectiveCompleted, 0 to 100. It was over completed until 2026-09-16.
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, effectiveCompleted, noEffectiveContact, analysed, 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, effectiveCompleted, successes, rejected, noEffectiveContact, analysed, successRate, rejectionRate, successRateLabel, avgDurationSeconds, avgSentiment, avgFrustration, avgLatencyMs, costUsd, costPerCallUsd.

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:

EndpointReturns
GET /api/analytics/analysis/criteriawindowDays, agentId, and criteria: one entry per criterion with id, success, failure, unknown, total, suppressed, 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. 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:

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-summarydaystenantId, totalCalls, totalSuccessful, effectiveCompleted, noEffectiveContact, analysedCalls, 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.

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.

KeyLabelCounts
escalated_countEscalados a una personaCalls whose outcome is escalated_to_human — escalations, and only escalations
unresolved_countNo resueltosCalls whose outcome is rejected

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.

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: letters, digits, ., _, - and spaces, up to 100 characters), 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 connection rate, with calls, duration, sentiment and cost.
cost_per_callDaily cost per call and cost per minute, with totals.
agent_performanceAgents ranked by connection 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.

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

type
stringRequired

One of the types above.

period
string

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.

date_from
stringRequired

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.

date_to
stringRequired

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.

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

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.

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,
"connectionRate": 24.1,
"successRate": 24.1,
"avgDurationSeconds": 151,
"avgSentiment": 0.34,
"totalCost": 285.62,
"rank": 1
}
]
}
}

Report shapes

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

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.

ReportNew fieldOld fieldWhat it is
roi_analysisconnectedCallssuccessfulCallsCalls that connected
roi_analysiscostPerConnectedCallcostPerSuccessfulCalltotalCost divided by calls that connected
campaign_comparison, agent_performanceconnectionRatesuccessRateConnected calls over dialled calls, 0 to 100

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.

POST /api/exports (202) worker picks up file written (expires in 72 h) error recorded DELETE /api/exports/:id DELETE /api/exports/:id pending processing completed failed

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, effective_completed_calls, no_effective_contact_calls, analysed_calls, avg_duration_seconds, success_rate, avg_sentiment. Since 2026-09-15 date is the day in your workspace’s timezone, not the UTC daydate_from, date_to, campaign_id, agent_id, status
agent_actionsEvery tool call an agent made, with parameters (redacted), outcome, duration and cost attribution. Since 2026-09-15 this includes the agent’s in-call hand-offs, transfer_to_number and transfer_to_agentdate_from, date_to, agent_id, status (the action outcome)
audit_logThe workspace audit logdate_from, date_to

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:

{
"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 (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.

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, a report format other than json, invalid KPI category.
403FORBIDDENThe key’s scope does not cover the request, including audit_log and agent_actions exports without full.
403BILLING_ERRORThe workspace has no active subscription (metadata.kind: "subscription_required"), or the 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. 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-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.