#Calls Analytics
Retrieve call records, transcripts, sentiment analysis, and export call data. Protected endpoints accept either X-API-Key or Authorization: Bearer <jwt> authentication and return data scoped to the resolved AuthContext.
Production API: https://api.aidial.ai
This page omits environment-specific host names, internal topology, and deployment details.
#Authentication And Scoping
Authentication is resolved in this order:
X-API-KeyAuthorization: Bearer <jwt>
If both headers are present, the API key is used. Missing or invalid authentication returns 401 Unauthorized.
Bearer authentication is for AiDial Portal sessions. Bearer validation requires ZITADEL_ISSUER, ZITADEL_AUDIENCE, and ZITADEL_JWKS_URL; these settings are required and have no fallback defaults. Bearer tenant assignment is resolved from portal_users and portal_user_client_assignments.
Both authentication modes resolve to client_id, data_env, scope, and principal metadata. Analytics and call data are routed by the strict (client_id, data_env) mapping in the tenant routing map; metadata, authentication, portal assignment, project configuration, export state, and audit records use the local API metadata store on the API host. A missing tenant route is server misconfiguration and returns 500 Internal Server Error. If the routed data host, tunnel, or database is unavailable, the API returns 503 Service Unavailable.
PII-sensitive fields respect the client-level PII flags in aidial_clients. Phone and transcript decryption goes through server-side decryption pathways when plaintext access is permitted. If PII permissions are missing, permission lookup fails, or decryption fails, the API fails closed by redacting or omitting PII. Raw encrypted database fields such as *_encrypted, ciphertext, wrapped keys, and nonces are not response fields.
#GET /v1/calls/summary
Returns aggregate call statistics for your organisation.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
start_date | query | string | No | — | Start date (inclusive). Accepts YYYY-MM-DD or ISO 8601 |
end_date | query | string | No | — | End date (inclusive). Accepts YYYY-MM-DD or ISO 8601 |
project_id | query | string | No | All projects | Filter by project |
solution_id | query | string | No | All solutions | Filter by solution |
#Response
| Field | Type | Description |
|---|---|---|
total_calls | integer | Total number of calls |
completed_calls | integer | Calls completed successfully |
missed_calls | integer | Calls that were missed |
failed_calls | integer | Calls that failed |
total_duration_seconds | integer | Non-null combined duration of all scoped calls; returns 0 when no calls match |
avg_duration_seconds | number or null | Average call duration |
total_cost_usd | number or null | Total cost in USD |
avg_cost_usd | number or null | Average cost per call in USD |
currency | string or null | Local currency code (e.g. AUD, USD) when all matching rows share one non-null currency and complete local cost values; otherwise null |
total_cost_local | number or null | Total cost in the local currency; null when currency is null |
avg_cost_local | number or null | Average cost per call in the local currency; null when currency is null |
human_escalation_count | integer | Number of calls escalated to a human |
sentiment_distribution | object | Counts by overall sentiment (positive, neutral, negative) |
caller_sentiment_distribution | object | Counts by caller sentiment (positive, neutral, negative). For this aggregate the values currently mirror sentiment_distribution because the summary query derives both from sentiment_overall. Use GET /v1/calls/sentiment for a distinct caller-sentiment breakdown |
disconnection_reason_distribution | object | Normalized Aidial API disconnection reason counts: caller_hangup, agent_or_system_end, transferred, missed, failed, unknown |
avg_ai_response_seconds | number or null | Average AI first-response time |
Display guidance: prefer the local-currency fields only when currency is non-null; otherwise display the USD fields explicitly labelled as USD rather than guessing a currency. The USD fields remain unchanged for all callers.
Disconnection reason guidance:
disconnection_reason_distributionis a normalized Aidial API taxonomy, not raw LiveKit, SIP, telephony, or provider metadata.- Counts use the same resolved
client_id,data_env, date,project_id, andsolution_idscope as the rest of the summary response. - The fallback order is transfer evidence, missed, failed, explicit caller hangup, explicit agent/system end, then
unknown. - Transfer evidence comes from persisted
ai_calls.call_metadata.transfer.attemptsnamed targets (target,destination_key, ordestination_name) in keyed-object or array-shaped attempt metadata, or transfer resolution metadata. Direct-call and in-progress transfer markers are ignored. call_status = completedby itself is not enough evidence forcaller_hanguporagent_or_system_end; missing, legacy, blank, or unrecognized metadata counts asunknown.- Empty scoped results return all six buckets as
0; non-empty distributions sum tototal_calls. - A hidden legacy partner passthrough is also served at
/v1/calls/summaryby the partner call-control router (excluded from the OpenAPI schema and registered ahead of the public summary route). It delegates to the same handler and returns the same normalized response model after the caller's target-client authority is resolved.
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/summary?start_date=2026-03-01&end_date=2026-03-15" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE"Response (200 OK):
{
"total_calls": 142,
"completed_calls": 120,
"missed_calls": 15,
"failed_calls": 7,
"total_duration_seconds": 28400,
"avg_duration_seconds": 200,
"total_cost_usd": 42.60,
"avg_cost_usd": 0.30,
"currency": "AUD",
"total_cost_local": 64.39,
"avg_cost_local": 0.45,
"human_escalation_count": 8,
"sentiment_distribution": {"positive": 85, "neutral": 42, "negative": 15},
"caller_sentiment_distribution": {"positive": 85, "neutral": 42, "negative": 15},
"disconnection_reason_distribution": {
"caller_hangup": 82,
"agent_or_system_end": 25,
"transferred": 13,
"missed": 15,
"failed": 7,
"unknown": 0
},
"avg_ai_response_seconds": 1.2
}#GET /v1/calls/outcomes
Returns the Rev 6 dashboard conversation outcome aggregate for one scoped project and date range.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
project_id | query | string | Yes | — | Selected project. Out-of-scope projects return non-enumerating 404 |
start_date | query | string | Yes | — | Start date or ISO 8601 boundary |
end_date | query | string | Yes | — | End date or ISO 8601 boundary |
client_id | query | string | No | Authenticated client | Target client id. Partner and internal bearer callers may use an assigned/in-scope client; unauthorized targets return non-enumerating 404 |
solution_id | query | string | No | All solutions | Optional solution filter |
#Source And Empty-State Semantics
- Category ids and labels come only from the selected project's API-managed dashboard
outcome_enum. - Calls are counted from routed tenant call records scoped by the resolved target
client_id, authenticateddata_env,project_id, and date range. - Primary outcome derivation uses persisted evidence only: exact in-taxonomy values from
booking_result,transfer_result, orcall_resolution_status, then capability/status rollups only when the project taxonomy explicitly declares a known outcome family label key. - Categories not present in the project taxonomy are not returned, even when matching call evidence exists.
- Missing taxonomy or unsupported data returns
categories: [],classified_count: 0, andunclassified_countequal to the scoped total. - The API does not infer families from English taxonomy ids or labels. Additional SQL evidence keys require a documented project taxonomy extension contract before they are added.
- The response never includes caller PII, transcript text, summaries, raw
analysis_data, prompt text, encrypted fields, recordings, API keys, bearer tokens, or secret-backend references.
#Empty / Unknown Rendering Guidance
- When
total_conversations == 0, render the outcomes panel as an empty range state. Do not render a donut wedge. - When
total_conversations > 0 && classified_count == 0, render an unknown taxonomy/evidence state. The donut may show a single portal-owned "Unclassified" wedge using a portal i18n key such asoutcomes.unclassified.title. - When
total_conversations > 0 && unclassified_count > 0, include an "Unclassified" wedge or equally visible coverage indicator so the classified donut does not imply full coverage. The unclassified label is portal-owned i18n, not API-provided taxonomy text. - When
categories[].capabilityis present, clients may surface a follow-up affordance tied to that bucket. Absence ofcapabilitymust not be treated as "capability disabled"; it only means the taxonomy entry was not mapped to an enabled booking or transfer capability hint. sourceandtaxonomy_revisionare contract identifiers for developers and diagnostics. Do not show them directly as customer-facing copy; map them to localized labels or hide them.
#Response
| Field | Type | Description |
|---|---|---|
project_id | string | Selected project id |
start_date | string | Echoed selected start boundary |
end_date | string | Echoed selected end boundary |
total_conversations | integer | All scoped conversations in the selected range |
classified_count | integer | Sum of returned bucket counts |
unclassified_count | integer | Scoped calls not matched to the project taxonomy |
taxonomy_revision | string or null | Dashboard metadata source revision |
source | string | project_taxonomy_derived_call_evidence |
categories | array | Ordered non-zero taxonomy buckets |
categories[].id | string | Project taxonomy id |
categories[].label | string | Project taxonomy label |
categories[].label_key | string or null | Optional stable portal i18n key for known outcome families; null for custom taxonomy entries |
categories[].count | integer | Bucket count |
categories[].percentage | number | Share of total_conversations |
categories[].capability | string, optional | booking or transfer when the project metadata marks that capability enabled |
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/outcomes?project_id=proj_example_001&start_date=2026-03-01&end_date=2026-03-15" \
-H "Authorization: Bearer PORTAL_SESSION_JWT"Response (200 OK):
{
"project_id": "proj_example_001",
"start_date": "2026-03-01",
"end_date": "2026-03-15",
"total_conversations": 142,
"classified_count": 130,
"unclassified_count": 12,
"taxonomy_revision": "published:7:safe",
"source": "project_taxonomy_derived_call_evidence",
"categories": [
{
"id": "booked_appointment",
"label": "Booked appointment",
"label_key": "outcomes.booking.title",
"count": 38,
"percentage": 26.76,
"capability": "booking"
}
]
}#GET /v1/calls/dashboard/privacy-aggregates
Returns privacy-safe Rev 6 dashboard data for the recent calls list and caller mix card.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
project_id | query | string | Yes | - | Selected project. Out-of-scope projects return non-enumerating 404 |
start_date | query | string | Yes | - | Start date or ISO 8601 boundary |
end_date | query | string | Yes | - | End date or ISO 8601 boundary |
client_id | query | string | No | Authenticated client | Target client id. Partner and internal bearer callers may use an assigned/in-scope client; unauthorized targets return non-enumerating 404 |
solution_id | query | string | No | All solutions | Optional solution filter |
recent_limit | query | integer | No | 5 | Number of recent call summaries to return. Minimum 1, maximum 10 |
#Source And Privacy Semantics
- Recent-call outcomes use the same
project_taxonomy_derived_call_evidenceclassifier asGET /v1/calls/outcomes. - Recent-call rows are intentionally small: call id, start time, duration, normalized sentiment, safe outcome label, and entitlement-driven detail availability only.
- Caller mix is an aggregate only. The API may use server-side caller identity evidence to count unique, new, and returning callers over a 30-day lookback, but grouping uses a tenant-scoped HMAC-SHA-256 key inside the routed database and no identity value or hash is returned.
caller_mix.status = availablewithreason_code = readyincludes aggregate counts, percentages, total calls, and average calls per caller.caller_mix.status = availablewithreason_code = no_callsreturns zero metrics for an empty range when matching is supported.caller_mix.status = unavailablewithreason_code = unsupportedmeans the project has no supported privacy-safe caller mix capability.caller_mix.status = unavailablewithreason_code = identity_unavailablemeans calls exist but safe matching evidence is unavailable or only partially covers the selected calls.caller_mix.status = unavailablewithreason_code = below_kmeans identity evidence exists but the unique caller population, new caller bucket, or returning caller bucket is below the minimum caller-mix threshold of5; identity-derived metrics are omitted.- Unclassified recent-call outcomes carry
label_key = "outcomes.unclassified.title"so the portal can localize the safe fallback label. detail_availableis true only for the owning tenant'sclient_adminsessions and assignedpartner_adminsessions; read-only, internal, API-key, or partial-scope callers receivefalse.- The API emits
dashboard_privacy_aggregates_readaudit events for successful reads and non-enumerating project-scope denials. Audit metadata records actor client, target client, project, and reason code, and excludes caller identity, HMAC output, and salts. - The response never includes caller names, full or masked phone numbers, CRM patient labels, caller ids, stable identity hashes, transcript text, summaries, recordings, raw
analysis_data, encrypted fields, API keys, bearer tokens, secrets, or secret-backend references.
#Response
| Field | Type | Description |
|---|---|---|
project_id | string | Selected project id |
start_date | string | Echoed selected start boundary |
end_date | string | Echoed selected end boundary |
source | string | project_taxonomy_derived_call_evidence |
recent_calls | array | Newest-first safe recent call summaries |
recent_calls[].call_id | string | Non-PII call identifier used for navigation |
recent_calls[].started_at_utc | string | Call start timestamp |
recent_calls[].duration_seconds | integer or null | Call duration when available |
recent_calls[].sentiment | string | positive, neutral, upset, or unclassified |
recent_calls[].outcome | object | Safe outcome with id, label, optional label_key, and state |
recent_calls[].detail_available | boolean | True only when the caller has call-detail entitlement for the scoped project |
caller_mix | object | Caller mix availability state and aggregate metrics |
caller_mix.status | string | available or unavailable |
caller_mix.reason_code | string | Machine-readable availability reason: ready, unsupported, identity_unavailable, below_k, or no_calls |
caller_mix.lookback_days | integer | Caller-mix lookback window in days; currently 30 |
caller_mix.metrics | object or null | Aggregate counts and percentages when ready or empty-supported |
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/dashboard/privacy-aggregates?project_id=proj_example_001&start_date=2026-03-01&end_date=2026-03-15&recent_limit=5" \
-H "Authorization: Bearer PORTAL_SESSION_JWT"Response (200 OK):
{
"project_id": "proj_example_001",
"start_date": "2026-03-01",
"end_date": "2026-03-15",
"source": "project_taxonomy_derived_call_evidence",
"recent_calls": [
{
"call_id": "LK-room-001",
"started_at_utc": "2026-03-15T02:10:00Z",
"duration_seconds": 184,
"sentiment": "positive",
"outcome": {
"id": "booked_appointment",
"label": "Booked appointment",
"label_key": "outcomes.booking.title",
"state": "classified"
},
"detail_available": true
}
],
"caller_mix": {
"status": "available",
"reason_code": "ready",
"lookback_days": 30,
"metrics": {
"total_calls": 70,
"unique_callers": 52,
"new_callers": 36,
"returning_callers": 16,
"new_callers_percent": 69.23,
"returning_callers_percent": 30.77,
"avg_calls_per_caller": 1.35
}
}
}#GET /v1/calls/list
Returns a paginated list of call records with optional filtering and sorting.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
start_date | query | string | No | — | Start date (inclusive). YYYY-MM-DD or ISO 8601 |
end_date | query | string | No | — | End date (inclusive). YYYY-MM-DD or ISO 8601 |
project_id | query | string | Yes | — | Selected project. Missing project id returns 400; out-of-scope projects return non-enumerating 404 |
solution_id | query | string | No | All solutions | Filter by solution |
status | query | string | No | All | Filter by call status (completed, missed, failed) |
sentiment | query | string | No | All | Filter by sentiment (positive, neutral, negative) |
resolution | query | string | No | All | Filter by resolution status |
call_type | query | string | No | All | Filter by call direction/type (inbound, outbound) |
attention_needed | query | boolean | No | All | Filter calls requiring owner attention |
booking_result | query | string | No | All | Filter by booking result |
transfer_result | query | string | No | All | Filter by transfer result |
follow_up_status | query | string | No | All | Filter by follow-up status |
sms_status | query | string | No | All | Filter by SMS delivery status |
email_status | query | string | No | All | Filter by email delivery status |
min_duration_seconds | query | integer | No | — | Minimum duration in seconds |
max_duration_seconds | query | integer | No | — | Maximum duration in seconds. Must be greater than or equal to min_duration_seconds |
caller | query | string | No | — | Search by caller identifier |
agent | query | string | No | — | Search by agent name |
summary_search | query | string | No | — | Privacy-safe search over redaction-safe summary text and call metadata. Maximum 160 characters. Phone numbers, email addresses, Medicare-like identifiers, and DOB-shaped values are rejected with 400 SEARCH_QUERY_PII_NOT_ALLOWED before any database access. The raw query text is never logged, audited, or echoed in error envelopes; accepted searches emit a calls.search audit event with a SHA-256 fingerprint and matched count only. Transcript text, transcript ciphertext, encrypted columns, recordings, and secret-backend artifacts are never searched. |
primary_outcome | query | string | No | — | Filter to rows whose API-owned safe outcome id matches this value. Slug shape (^[a-z0-9_-]+$, max 80 characters). Values come from the selected project's outcome taxonomy as returned by /v1/calls/outcomes for the same project. The reserved value unclassified selects rows whose primary_outcome.state is unclassified OR unknown_taxonomy. Unknown ids return 400 INVALID_PRIMARY_OUTCOME and the response body does NOT echo the raw value. |
sort_by | query | string | No | started_at_desc | Sort order. Options: started_at_desc, started_at_asc, duration_desc, duration_asc, cost_desc, cost_asc |
page | query | integer | No | 1 | Page number (1-based, minimum 1) |
page_size | query | integer | No | 50 | Requested results per page (1-500). Effective maximum is 100 unless include_transcript=true, which allows 500 |
include_transcript | query | boolean | No | false | Include pre-redacted transcript messages in each call |
#Response
| Field | Type | Description | ||||
|---|---|---|---|---|---|---|
calls | array | List of call record objects | ||||
calls[].call_id | string | Unique call identifier | ||||
calls[].client_id | string | Client identifier | ||||
calls[].project_id | string | Project identifier | ||||
calls[].solution_id | string | Solution identifier | ||||
calls[].started_at_utc | string (ISO 8601) | Call start timestamp | ||||
calls[].ended_at_utc | string (ISO 8601) or null | Call end timestamp | ||||
calls[].duration_seconds | number or null | Call duration | ||||
calls[].call_status | string | Status (completed, missed, failed) | ||||
calls[].call_direction | string or null | Direction (inbound, outbound) | ||||
calls[].channel | string or null | Communication channel | ||||
calls[].from_number | string or null | Caller number (redacted based on PII config) | ||||
calls[].to_number | string or null | Called number | ||||
calls[].summary | string or null | AI-generated call summary. Returned as null whenever summary_status is not ok | ||||
calls[].summary_status | string | Backend-owned summary availability marker. One of ok, redacted_failed, or unavailable | ||||
calls[].sentiment_label_extension | string or null | Backend-derived escalation extension label. Set to escalation_suggested only when backend-owned human-escalation evidence is present; null otherwise. Frontend sentiment, duration, or score heuristics do not populate this field | ||||
calls[].transfer_destination | object or null | Redaction-safe current transfer destination display. When present: `{ "label": string, "destination_type": "internal_extension" \ | "external_number" \ | "queue" \ | "agent", "revision_id": string }. The label is resolved from the named runtime transfer key to the current active Portal destination label, so historical rows can change when a destination is renamed. null` when the call did not transfer to a named destination, the destination is deleted/inactive, lookup fails, or the current label fails the safety check | |
calls[].primary_outcome | object | API-owned safe outcome label derived from the project's outcome taxonomy. Same id-space and label_key vocabulary as /v1/calls/outcomes. Shape: `{ "id": string, "label": string \ | null, "label_key": string \ | null, "state": "classified" \ | "unclassified" \ | "unknown_taxonomy" }. classified = row evidence matched a taxonomy bucket. unclassified = project taxonomy present but the row did not match. unknown_taxonomy = project taxonomy is missing or empty (the id is the reserved "unclassified", label is null, label_key is "outcomes.unclassified.title"`). |
calls[].call_resolution_status | string | Resolution status; defaults to unavailable when not populated | ||||
calls[].booking_result | string | Booking result status; defaults to unavailable when not populated | ||||
calls[].transfer_result | string | Transfer result status; defaults to unavailable when not populated | ||||
calls[].follow_up_status | string | Follow-up status; defaults to unavailable when not populated | ||||
calls[].sms_status | string | SMS delivery status; defaults to unavailable when not populated | ||||
calls[].email_status | string | Email delivery status; defaults to unavailable when not populated | ||||
calls[].sentiment_overall | string or null | Overall sentiment | ||||
calls[].caller_sentiment | string or null | Caller sentiment | ||||
calls[].message_count | integer or null | Number of transcript messages | ||||
calls[].human_escalation_needed | boolean | Whether human escalation was triggered | ||||
calls[].cost_usd | number or null | Call cost in USD | ||||
calls[].llm_token_used | integer | LLM tokens consumed | ||||
calls[].stt_minutes_processed | number | Speech-to-text minutes | ||||
calls[].tts_characters_synthesized | integer | Text-to-speech characters | ||||
calls[].llm_provider | string or null | LLM provider name | ||||
calls[].llm_model | string or null | LLM model name | ||||
calls[].transcript_messages | array or null | Transcript (only when include_transcript=true) | ||||
pagination | object | Pagination metadata | ||||
pagination.page | integer | Current page | ||||
pagination.page_size | integer | Requested page size | ||||
pagination.total_records | integer | Total records matching the filters | ||||
pagination.total_pages | integer | Total pages based on the requested page size | ||||
scope | object | Response-level scope metadata | ||||
scope.empty_state_reason | string or null | Reason the list is empty. no_calls_in_period for default/date-only empty results, filtered_out when non-default filters narrow an otherwise non-empty range to zero, or null when calls are present |
Pagination note: Results use offset-based pagination. If new records are inserted between page requests, results may shift. For consistent snapshots, use narrow date ranges.
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/list?project_id=proj_example_001&page=1&page_size=2&start_date=2026-03-01&sort_by=started_at_desc" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE"Response (200 OK):
{
"calls": [
{
"call_id": "call_00000000-0000-4000-a000-000000000001",
"client_id": "client_example_001",
"project_id": "proj_example_001",
"solution_id": "sol_example_001",
"started_at_utc": "2026-03-15T09:30:00Z",
"ended_at_utc": "2026-03-15T09:35:22Z",
"duration_seconds": 322,
"call_status": "completed",
"call_direction": "inbound",
"channel": "phone",
"from_number": "+61 400 000 001",
"to_number": "+61 2 0000 0001",
"summary": "Caller requested an appointment booking for next Tuesday.",
"summary_status": "ok",
"sentiment_label_extension": null,
"transfer_destination": {
"label": "Front desk",
"destination_type": "external_number",
"revision_id": "trv_5f8c20a47b9b41acb2"
},
"primary_outcome": {
"id": "booked_appointment",
"label": "Booked appointment",
"label_key": "outcomes.booking.title",
"state": "classified"
},
"call_resolution_status": "resolved",
"booking_result": "booked",
"transfer_result": "unavailable",
"follow_up_status": "sent",
"sms_status": "unavailable",
"email_status": "sent",
"sentiment_overall": "positive",
"caller_sentiment": "positive",
"message_count": 12,
"human_escalation_needed": false,
"cost_usd": 0.48,
"llm_token_used": 1250,
"stt_minutes_processed": 5.37,
"tts_characters_synthesized": 3200,
"llm_provider": "openai",
"llm_model": "gpt-4o",
"transcript_messages": null
}
],
"pagination": {
"page": 1,
"page_size": 2,
"total_records": 142,
"total_pages": 71
},
"scope": {
"empty_state_reason": null
}
}#GET /v1/calls/timeline
Returns time-series call metrics for charting.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
start_date | query | string | No | — | Start date (inclusive). YYYY-MM-DD or ISO 8601 |
end_date | query | string | No | — | End date (inclusive). YYYY-MM-DD or ISO 8601 |
project_id | query | string | No | All projects | Filter by project |
solution_id | query | string | No | All solutions | Filter by solution |
interval | query | string | No | day | Time interval: day, week, or month |
#Response
| Field | Type | Description |
|---|---|---|
timeline | array | Time-series data points |
timeline[].period | string (ISO 8601) | Start of the time bucket |
timeline[].total_calls | integer | Total calls in this period |
timeline[].completed_calls | integer | Completed calls |
timeline[].missed_calls | integer | Missed calls |
timeline[].failed_calls | integer | Failed calls |
timeline[].total_duration_seconds | number or null | Total duration |
timeline[].avg_duration_seconds | number or null | Average duration |
timeline[].total_cost_usd | number or null | Total cost in USD |
timeline[].human_escalation_count | integer | Human escalation count |
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/timeline?start_date=2026-03-10&end_date=2026-03-15&interval=day" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE"Response (200 OK):
{
"timeline": [
{
"period": "2026-03-10T00:00:00Z",
"total_calls": 18,
"completed_calls": 15,
"missed_calls": 2,
"failed_calls": 1,
"total_duration_seconds": 3600,
"avg_duration_seconds": 200,
"total_cost_usd": 5.40,
"human_escalation_count": 1
}
]
}#GET /v1/calls/sentiment
Returns sentiment distribution across calls. Both start_date and end_date are required.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
start_date | query | string | Yes | — | Start date (inclusive). Accepts YYYY-MM-DD or ISO 8601 |
end_date | query | string | Yes | — | End date. Accepts YYYY-MM-DD or ISO 8601 |
project_id | query | string | No | All projects | Filter by project |
solution_id | query | string | No | All solutions | Filter by solution |
Date boundary semantics match /v1/calls/summary:
- A
YYYY-MM-DDend_dateis inclusive of that whole UTC day. - An ISO 8601 datetime
end_dateis the exclusive UTC upper bound and is not expanded by an extra day. - A reversed range (start after end) returns
400; mixed date/datetime ranges are compared safely on UTC boundaries. - For an identical window and scope, the distribution
total_callsaligns with/v1/calls/summarytotal_calls.
#Response
| Field | Type | Description |
|---|---|---|
overall | object | Overall sentiment distribution |
overall.positive | integer | Positive count |
overall.neutral | integer | Neutral count |
overall.negative | integer | Negative count |
overall.analyzed | integer | Total analysed |
caller | object | Caller sentiment distribution (same structure) |
agent | object | Agent sentiment distribution (same structure) |
total_calls | integer | Total calls in range |
start_date | string | Echoed exactly as supplied (YYYY-MM-DD or ISO 8601) |
end_date | string | Echoed exactly as supplied (YYYY-MM-DD or ISO 8601) |
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/sentiment?start_date=2026-03-01&end_date=2026-03-15" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE"Response (200 OK):
{
"overall": {"positive": 85, "neutral": 42, "negative": 15, "analyzed": 142},
"caller": {"positive": 90, "neutral": 38, "negative": 14, "analyzed": 142},
"agent": {"positive": 100, "neutral": 35, "negative": 7, "analyzed": 142},
"total_calls": 142,
"start_date": "2026-03-01",
"end_date": "2026-03-15"
}#GET /v1/calls/sentiment/timeline
Returns sentiment trend over time. Both start_date and end_date are required. The response includes dense time buckets for the requested interval; buckets with no matching calls return zero counts and zero percentages.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
start_date | query | string | Yes | — | Start date (inclusive). Accepts YYYY-MM-DD or ISO 8601 |
end_date | query | string | Yes | — | End date. Accepts YYYY-MM-DD or ISO 8601 |
project_id | query | string | No | All projects | Filter by project |
solution_id | query | string | No | All solutions | Filter by solution |
interval | query | string | No | day | Time interval: day, week, or month |
Date boundary semantics match /v1/calls/summary:
- A
YYYY-MM-DDend_dateis inclusive of that whole UTC day. - An ISO 8601 datetime
end_dateis the exclusive UTC upper bound and is not expanded by an extra day. The dense bucket range still spans the requested start/end calendar window. - A reversed range (start after end) returns
400; mixed date/datetime ranges are compared safely on UTC boundaries.
#Response
| Field | Type | Description |
|---|---|---|
timeline | array | Time-series sentiment data points |
timeline[].period | string (ISO 8601) | Start of the time bucket |
timeline[].total_calls | integer | Total scoped calls in this period, including calls without a sentiment value |
timeline[].positive_count | integer | Positive sentiment count |
timeline[].neutral_count | integer | Neutral sentiment count |
timeline[].negative_count | integer | Canonical backend field for negative sentiment count; the portal may label this bucket as "Upset" |
timeline[].positive_percent | number | Positive percentage of calls with classified sentiment |
timeline[].neutral_percent | number | Neutral percentage of calls with classified sentiment |
timeline[].negative_percent | number | Negative percentage of calls with classified sentiment |
start_date | string | Echoed exactly as supplied (YYYY-MM-DD or ISO 8601) |
end_date | string | Echoed exactly as supplied (YYYY-MM-DD or ISO 8601) |
interval | string | Interval used |
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/sentiment/timeline?start_date=2026-03-10&end_date=2026-03-15&interval=day" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE"Response (200 OK):
{
"timeline": [
{
"period": "2026-03-10T00:00:00Z",
"total_calls": 18,
"positive_count": 12,
"neutral_count": 5,
"negative_count": 1,
"positive_percent": 66.7,
"neutral_percent": 27.8,
"negative_percent": 5.6
}
],
"start_date": "2026-03-10",
"end_date": "2026-03-15",
"interval": "day"
}#GET /v1/calls/provider-performance
Returns scoped provider and latency analytics for dashboard comparison views.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>) Scopes: client_staff, client_admin, client_manager, partner_admin, partner_user, aidial_admin, aidial_operator, read-only, or full-access
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
start_date | query | string | Yes | — | Inclusive ISO 8601 UTC datetime with timezone |
end_date | query | string | Yes | — | Exclusive ISO 8601 UTC datetime with timezone. Must be after start_date and no more than 90 days later |
timezone | query | string | Yes | — | IANA timezone used for daily trend labels |
client_id | query | string | No | Authenticated client | Target client id. Partner and internal bearer callers may use an assigned/in-scope client; unauthorized targets return non-enumerating 404 |
project_id | query | string | No | All projects | Optional project filter |
category | query | string | No | Role default | One of perceived_response, stt, llm, or tts. client_staff may request only perceived_response |
provider | query | string | No | All providers | Provider filter for provider-visible scopes only: partner_admin, partner_user, aidial_admin, aidial_operator, read-only, or full-access. Not valid with category=perceived_response |
#Response
| Field | Type | Description |
|---|---|---|
generated_at | string (ISO 8601) | Response generation time |
range | object | Echoed UTC range, requested timezone, and granularity: "day" |
visibility | object | Role-derived projection rules, including provider-name visibility, provider-filter permission, safe-reference visibility, comparison projection, and allowed categories. API-key read-only and full-access scopes are provider-visible and are reported as partner_admin in visibility.role |
filters.projects | array | Project filter options visible in the scoped result set |
filters.categories | array | Category options allowed for the caller |
filters.providers | array | Provider filter options. Empty when provider names are not visible |
summary | object | Counts for total calls, completed calls, valid metric samples, missing metrics, privacy exclusions, outliers, and cold-start outliers |
comparisons | array | Category or provider comparison rows. Empty for client_staff, category-level for client_admin and client_manager, provider-level for provider-visible scopes where provider data exists |
trends | array | Daily latency trend rows by category |
safe_call_references | array, optional | Up to 20 safe outlier call references. Present only for partner_admin, aidial_admin, and aidial_operator |
methodology | object | Percentile method, thresholds, and sparse-sample rules |
The endpoint reads only allowlisted call timing and provider fields from routed tenant call records. It does not return transcripts, summaries, phone numbers, recording URLs, encrypted fields, API keys, bearer tokens, or secret-backend references.
Error semantics:
400 INVALID_DATE_RANGE,DATE_RANGE_TOO_LARGE,INVALID_TIMEZONE, orINVALID_FILTERfor invalid query input.404 NOT_AUTHORIZEDfor out-of-scope clients, projects, roles, categories, or provider filters.503 UPSTREAM_DATA_UNAVAILABLEwhen routed analytics data cannot be read.
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/provider-performance?start_date=2026-03-01T00:00:00Z&end_date=2026-03-15T00:00:00Z&timezone=Australia%2FBrisbane&project_id=proj_example_001&category=llm" \
-H "Authorization: Bearer PORTAL_SESSION_JWT"#GET /v1/calls/{call_id}
Returns scoped detail for a single call. Transcript and phone visibility depend on the authenticated role and client PII configuration.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>) Access: Scoped to the resolved tenant and project. Out-of-scope calls return non-enumerating 404
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
call_id | path | string | Yes | — | Unique call identifier. The API looks up the call by LiveKit room SID or room name |
include_original_transcript | query | boolean | No | false | When true, attempts to return the original transcript through the server-side transcript-decryption path if the caller and client PII flags permit it. The default response uses pre-redacted transcript messages |
#Response
| Field | Type | Description |
|---|---|---|
call_id | string | Unique call identifier |
client_id | string | Client identifier |
project_id | string | Project identifier |
solution_id | string | Solution identifier |
compliance_required | boolean | Whether compliance handling was required for the call |
compliance_jurisdiction | string or null | Compliance jurisdiction when available |
started_at_utc | string (ISO 8601) | Call start timestamp |
ended_at_utc | string (ISO 8601) or null | Call end timestamp |
duration_seconds | number or null | Call duration |
call_status | string | Call status |
call_direction | string or null | Direction (inbound, outbound) |
channel | string or null | Communication channel |
from_number | string or null | Caller number, redacted based on role and PII configuration |
to_number | string or null | Called number, redacted based on role and PII configuration |
summary | string or null | AI-generated call summary. Returned as null whenever summary_status is not ok |
summary_status | string | Backend-owned summary availability marker: ok, redacted_failed, or unavailable |
sentiment_label_extension | string or null | escalation_suggested only when backend-owned human-escalation evidence is present |
transfer_destination | object or null | Redaction-safe current transfer destination display. Same shape and rules as documented on /v1/calls/list |
primary_outcome | object | API-owned safe outcome label. Same shape and rules as documented on /v1/calls/list ({ id, label, label_key, state }). |
sentiment_overall | string or null | Overall sentiment |
caller_sentiment | string or null | Caller sentiment |
agent_sentiment | string or null | Agent sentiment |
message_count | integer or null | Number of transcript messages |
transcript | array or null | Transcript messages, each with speaker, timestamp, text, and optional sentiment. May be redacted or omitted based on PII configuration and decryption outcome |
call_resolution_status | string or null | Resolution status |
ai_first_response_seconds | number or null | AI first-response time |
human_escalation_needed | boolean | Whether human escalation was needed |
human_intervention_count | integer | Number of human interventions |
cost_usd | number or null | Call cost in USD |
cost_local | number or null | Call cost in local currency |
cost_local_currency | string or null | Local currency code |
cost_breakdown | object or null | Detailed cost breakdown when available |
llm_token_used | integer | LLM tokens consumed |
stt_minutes_processed | number | Speech-to-text minutes |
tts_characters_synthesized | integer | Text-to-speech characters |
cloud_provider | string | Cloud provider |
cloud_region | string | Cloud region |
llm_provider | string or null | LLM provider |
llm_model | string or null | LLM model |
stt_provider | string or null | STT provider |
tts_provider | string or null | TTS provider |
tts_voice | string or null | TTS voice |
telephony_provider | string or null | Telephony provider |
telephony_call_id | string or null | Provider call identifier |
livekit_room_name | string or null | LiveKit room name |
agent_identity | string or null | Agent identity |
caller_identity | string or null | Caller identity |
latency_metrics | object or null | Latency metrics when available |
analysis_data | object or null | Redacted analysis data when available |
The response model does not include recording URLs, partner tags, external references, or raw encrypted transcript artifacts.
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/call_00000000-0000-4000-a000-000000000001" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE"Response (200 OK):
{
"call_id": "call_00000000-0000-4000-a000-000000000001",
"client_id": "client_example_001",
"project_id": "proj_example_001",
"solution_id": "sol_example_001",
"compliance_required": true,
"compliance_jurisdiction": "AU",
"started_at_utc": "2026-03-15T09:30:00Z",
"ended_at_utc": "2026-03-15T09:35:22Z",
"duration_seconds": 322,
"call_status": "completed",
"call_direction": "inbound",
"channel": "phone",
"from_number": "+61 400 000 001",
"to_number": "+61 2 0000 0001",
"summary": "Caller requested an appointment booking for next Tuesday.",
"summary_status": "ok",
"sentiment_label_extension": null,
"transfer_destination": null,
"primary_outcome": {
"id": "booked_appointment",
"label": "Booked appointment",
"label_key": "outcomes.booking.title",
"state": "classified"
},
"sentiment_overall": "positive",
"caller_sentiment": "positive",
"agent_sentiment": "neutral",
"message_count": 2,
"transcript": [
{"speaker": "agent", "timestamp": 0.2, "text": "Good morning, how can I help you today?", "sentiment": null},
{"speaker": "caller", "timestamp": 3.1, "text": "I would like to book an appointment.", "sentiment": "positive"}
],
"call_resolution_status": "resolved",
"ai_first_response_seconds": 1.2,
"human_escalation_needed": false,
"human_intervention_count": 0,
"cost_usd": 0.48,
"cost_local": 0.73,
"cost_local_currency": "AUD",
"cost_breakdown": null,
"llm_token_used": 1250,
"stt_minutes_processed": 5.37,
"tts_characters_synthesized": 3200,
"cloud_provider": "aws",
"cloud_region": "ap-southeast-2",
"llm_provider": "openai",
"llm_model": "gpt-4o",
"stt_provider": "deepgram",
"tts_provider": "elevenlabs",
"tts_voice": "voice_example",
"telephony_provider": "twilio",
"telephony_call_id": "CA00000000000000000000000000000001",
"livekit_room_name": "room_00000000-0000-4000-a000-000000000001",
"agent_identity": "agent_example",
"caller_identity": null,
"latency_metrics": null,
"analysis_data": null
}#GET /v1/calls/{call_id}/inline-preview
Returns the closed, redacted preview used by Portal call-list row expansion.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
call_id | path | string | Yes | — | Unique call identifier. The API looks up the call by LiveKit room SID or room name |
#Response
| Field | Type | Description |
|---|---|---|
call_id | string | Unique call identifier |
project_id | string | Project identifier |
client_id | string | Client identifier |
request_id | string or null | Request or correlation id |
started_at_utc | string (ISO 8601) | Call start timestamp |
ended_at_utc | string (ISO 8601) or null | Call end timestamp |
call_status | string | Call status |
from_number | string or null | PII-redacted caller number |
message_count | integer or null | Number of transcript messages |
caller_sentiment | string or null | Caller sentiment |
agent_sentiment | string or null | Agent sentiment |
transfer_destination | object or null | Redaction-safe current transfer destination display |
end_reason | string | One of caller_hangup, transferred, failed, missed, or unknown |
consent_state | string | One of obtained, not_required, or unavailable |
transcript_state | string | One of ok, empty, redacted_hidden, live, redacted_failed, retention_expired, or error |
transcript | array or null | Redacted transcript preview, present only when transcript_state is ok. Preview output is capped at 200 turns |
summary_status | string | Backend-owned summary status |
transcript_truncated | boolean | True when the source transcript exceeded the preview cap |
The inline preview uses the pre-redacted transcript path and does not decrypt original transcript ciphertext. Partner bearer sessions hide transcript text and return transcript_state: "redacted_hidden". Missing or out-of-scope calls return 404; routed data-host failures return 503.
#Transcript Quality
Transcript quality endpoints expose redacted transcript review state and allow eligible Portal users to record corrections, annotations, and disputes.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Endpoints
| Method | Path | Description |
|---|---|---|
GET | /v1/calls/{call_id}/transcript-quality | Returns the transcript quality snapshot |
POST | /v1/calls/{call_id}/transcript-quality/corrections | Creates a transcript correction revision |
POST | /v1/calls/{call_id}/transcript-quality/annotations | Creates a call-level or message-level annotation |
POST | /v1/calls/{call_id}/transcript-quality/disputes | Opens, resolves, or rejects a message dispute |
#Roles
Snapshot reads are available to client_admin, client_manager, client_staff, and API-key full-access. Partner roles, internal roles, and API-key read-only receive non-enumerating 404 responses for transcript-quality reads.
Mutations require bearer Portal authentication with a portal user principal. Corrections and disputes are allowed for client_admin and client_manager. Annotations are allowed for client_admin, client_manager, and client_staff.
#Snapshot Response
| Field | Type | Description |
|---|---|---|
call_id | string | Unique call identifier |
client_id | string | Client identifier |
project_id | string | Project identifier |
transcript_available | boolean | Whether the redacted transcript can be reviewed |
unavailable_reason | string or null | consent_blocked, retention_blocked, deleted, or missing |
can_correct | boolean | Whether the caller can create corrections |
can_annotate | boolean | Whether the caller can create annotations |
can_resolve_disputes | boolean | Whether the caller can resolve or reject disputes |
messages | array | Transcript messages with display text, correction history, annotations, and dispute state |
call_annotations | array | Call-level annotations |
analytics_source_status | string | original_transcript or reanalysis_unavailable |
quality_summary | object | Correction, annotation, open-dispute, and last-activity summary |
#Mutation Bodies
| Endpoint | Body |
|---|---|
| Corrections | { "message_index": 0, "corrected_text": "...", "reason": "..." } |
| Annotations | { "scope": "message", "message_index": 0, "content": "..." } or { "scope": "call", "content": "..." } |
| Disputes | { "message_index": 0, "status": "open" }, { "message_index": 0, "status": "resolved", "note": "..." }, or { "message_index": 0, "status": "rejected", "note": "..." } |
User-entered quality text is normalized before persistence. Phone numbers, email addresses, card-like numbers, and identifier-like values are replaced with safe placeholders; secret-like content is rejected. Rejected or unchanged quality text returns 422 QUALITY_TEXT_REJECTED. Invalid request bodies return 400; denied mutations return 403 for read-entitled callers without mutation rights and non-enumerating 404 for callers without transcript-content access; missing or out-of-scope calls return 404; workflow failures return 503.
#GET /v1/calls/{call_id}/transcript-download
Downloads the redacted transcript for a call.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>) Scopes: client_admin, client_manager, client_staff, or partner_admin
Data handling: Transcripts may contain personal information subject to the Privacy Act 1988 (Cth). All transcript access is audit-logged. Partners should refer to their AiDial partner agreement for data handling obligations. Transcripts are redacted by default based on your client PII configuration. Secondary-use policy does not gate this direct redacted transcript access path; it continues to apply to reuse/export, webhook delivery, reporting, and model-training decisions.
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
call_id | path | string | Yes | — | Unique call identifier |
format | query | string | No | txt | Download format. Current implementation supports txt only |
include_quality_history | query | boolean | No | false | Include separately labelled correction, annotation, and dispute history in the TXT output |
#Response
Returns the transcript as a streamed text attachment (Content-Type: text/plain; charset=utf-8). The attachment filename uses AiDial-Transcript_<project-display-or-id>_<YYYY-MM-DD>_<call-id-suffix>.txt, where <call-id-suffix> is the last 12 characters of the sanitised call id and the date segment is evaluated in the project timezone when available. The text starts with readable call metadata headers (Business, Call date, Duration, Direction, From, To) followed by role-prefixed transcript lines. Project display name and timezone are loaded through tenant-scoped project metadata when they are not present on the routed call row; unavailable metadata falls back to project id and UTC. When include_quality_history=true, correction, annotation, and dispute history is appended in separately labelled sections. Unsupported formats return 400 EXPORT_UNSUPPORTED_FORMAT. If the call is out of scope, unavailable, or the caller lacks transcript access, the endpoint returns 404 Not Found. Download throttling returns 429 with code DOWNLOAD_THROTTLED; generation failures return 503 DOWNLOAD_FAILED.
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/call_00000000-0000-4000-a000-000000000001/transcript-download" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \
-o transcript.txt#GET /v1/calls/{call_id}/transcript/pdf
Downloads the redacted transcript for a call as a PDF attachment.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>) Scopes: client_admin, client_manager, client_staff, or partner_admin
Data handling: The PDF is generated from the same redacted transcript path as TXT download. Transcript access is audit-logged. Secondary-use policy does not gate this direct redacted transcript access path; it continues to apply to reuse/export, webhook delivery, reporting, and model-training decisions.
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
call_id | path | string | Yes | — | Unique call identifier |
#Response
Returns the transcript as a streamed PDF attachment (Content-Type: application/pdf). The attachment filename uses AiDial-Transcript_<project-display-or-id>_<YYYY-MM-DD>_<call-id-suffix>.pdf, where <call-id-suffix> is the last 12 characters of the sanitised call id and the date segment is evaluated in the project timezone when available, and the PDF header uses the same tenant-scoped project display name, timezone, and redacted call metadata as the TXT export. Per-message labels use elapsed MM:SS offsets when offset or timestamp evidence is available. Transcripts over the 500 KB raw transcript size guard return 413 with code DOWNLOAD_PDF_TOO_LARGE; use TXT download for oversized transcripts. Download throttling returns 429 with code DOWNLOAD_THROTTLED.
#Example
Request:
curl -X GET "https://api.aidial.ai/v1/calls/call_00000000-0000-4000-a000-000000000001/transcript/pdf" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \
-o transcript.pdf#POST /v1/calls/export
Exports filtered call data as a CSV file.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>) Scopes (metadata export — sub-surface S-006-METADATA): client_admin, client_manager, client_staff, or partner_admin Scopes (sensitive opt-ins — sub-surfaces S-006-CALLER_INFO, S-006-TRANSCRIPT, S-006-AUDIO_LINKS): client_admin only (S19). Audio-link opt-in requests additionally require project audio retention enabled, but the current CSV writer does not emit a signed audio-link column.
Export behaviour: When rows match, this endpoint returns a streamed CSV file directly in the response body (Content-Type:
text/csv; charset=utf-8). It is not an asynchronous job. Large date ranges may result in longer response times. Set appropriate client-side timeouts (recommended minimum: 300 seconds for full-range exports).
#Request Body
| Field | Type | Required | Description |
|---|---|---|---|
project_id | string | Yes | Project to export |
start_date | string | No | Start date (inclusive). YYYY-MM-DD or ISO 8601 |
end_date | string | No | End date (inclusive). YYYY-MM-DD or ISO 8601 |
status | string | No | Filter by call status |
caller | string | No | Filter by caller |
agent | string | No | Filter by agent |
displayed_matched_count | integer | No | Advisory browser-side matched count for audit context (0-100000) |
format | string | No | Export format. Currently only csv |
include_caller_info | boolean | Yes (S19) | Opt-in for raw caller fields. Honoured only for client_admin. |
include_transcript_text | boolean | Yes (S19) | Opt-in for decrypted transcript text. Honoured only for client_admin. Triggers server-side transcript decrypt. |
include_audio_links | boolean | Yes (S19) | Opt-in for signed audio links. The request is role- and retention-gated, but this release does not include signed audio links in the CSV or in options_honoured. Raw object-storage paths and keys are never returned. |
#Response
When rows match, returns a CSV attachment with these columns:
| Column | Description |
|---|---|
call_id | Unique call identifier |
project_id | Project identifier |
call_started_at | Call start timestamp |
call_status | Call status |
channel | Communication channel |
caller | Caller number, redacted based on role and PII configuration |
agent | Agent identity |
duration_seconds | Call duration |
summary | Call summary |
transcript | Collapsed redacted transcript text |
CSV values that could be interpreted as spreadsheet formulas are neutralised before streaming.
Sensitive export audit metadata records selected options separately from honoured options. include_audio_links=true is retained in selected options after it passes role and retention checks, but audio_links is deliberately excluded from honoured options until signed-link generation is implemented.
If no rows match, the endpoint returns 200 OK with JSON:
{
"code": "EXPORT_EMPTY",
"message": "No matching calls found.",
"messageKey": "calls.export.messages.empty"
}Synchronous exports are capped at 10,000 matching rows and guarded by a 30 second wall-time limit. Row counts over the hard cap return 413 EXPORT_TOO_LARGE before the cursor opens. If the wall-time guard trips during streaming, the export stops and the audit row records EXPORT_TOO_LARGE with failure_reason: "wall_time_exceeded". The legacy EXPORT_OVER_CAP (400) code remains for the browser-claimed advisory count cap.
S19 error semantics:
422— one or more required typed booleans are missing or are not JSON booleans. This validation happens before router role, capability, count, or audit handling.400 EXPORT_PROJECT_REQUIRED—project_idwas omitted or blank.400 EXPORT_UNSUPPORTED_FORMAT— unsupported export format, when the request reaches the router. The current request model accepts onlycsv, so most non-CSV values fail request validation before this branch.404 EXPORT_DENIED— the caller's base role cannot use the export surface, or the selected project is out of scope.403 EXPORT_DENIED— sensitive opt-in requested by a non-client_adminrole; the request is rejected before any count, cursor, decrypt, or signed-link generation runs.400 EXPORT_UNAVAILABLE—include_audio_links=trueagainst a project whose audio retention is disabled or cannot be confirmed.413 EXPORT_TOO_LARGE— synchronous row cap exceeded before streaming starts. Wall-time overruns are recorded in export audit metadata while streaming is stopped.
S19 example body (metadata-only):
{
"project_id": "proj_example_001",
"start_date": "2026-03-01",
"end_date": "2026-03-15",
"format": "csv",
"include_caller_info": false,
"include_transcript_text": false,
"include_audio_links": false
}#Example
Request:
curl -X POST "https://api.aidial.ai/v1/calls/export" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"project_id": "proj_example_001",
"start_date": "2026-03-01",
"end_date": "2026-03-15",
"format": "csv",
"include_caller_info": false,
"include_transcript_text": false,
"include_audio_links": false
}' \
-o calls_export.csv#Rate Limiting
Only one export can run at a time for the same user and tenant. Concurrent exports return 409 with code EXPORT_IN_PROGRESS. If rate-limited, the export endpoint returns:
Response (429 Too Many Requests):
{
"error": "rate_limited",
"code": "EXPORT_THROTTLED",
"message": "Export throttled. Please wait and try again.",
"messageKey": "calls.export.messages.throttled",
"retry_after_seconds": 60
}#POST /v1/calls/{call_id}/audit-pii-access
Logs that PII for a call was accessed. This endpoint records the access against the routed call record and emits a portal audit event.
Authentication: Required (X-API-Key or Authorization: Bearer <jwt>)
#Parameters
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
call_id | path | string | Yes | — | Unique call identifier |
#Request Body
| Field | Type | Required | Description |
|---|---|---|---|
access_type | string | Yes | Type of access, such as view_transcript, export_transcript, or view_phone |
accessed_by | string | Yes | User identifier, such as a portal user ID or email |
access_context | string | No | Access context. Defaults to api |
#Response
| Field | Type | Description |
|---|---|---|
status | string | success when the audit event is recorded |
message | string | Human-readable status message |
#Example
Request:
curl -X POST "https://api.aidial.ai/v1/calls/call_00000000-0000-4000-a000-000000000001/audit-pii-access" \
-H "X-API-Key: sk_live_YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"access_type": "view_transcript", "accessed_by": "user@example.com", "access_context": "portal"}'Response (200 OK):
{
"status": "success",
"message": "PII access logged successfully"
}#Data Scoping
All responses are scoped to your authenticated client_id and data_env. You only see calls belonging to your organisation and routed environment. Accessing calls outside your scope returns 404 Not Found.
#Related Pages
- Authentication — API key and bearer authentication
- Error Responses — error format details
- Rate Limiting — handling 429 responses