8. API Reference Guide
Target audience: Developers, IT partners, integration teams
Description: The public API contract for pushing interactions into CQA and pulling analysis results out: authentication, endpoints, file schemas, callbacks, and limits. To get an API key, see 5.4 Access API. For an end-to-end setup and verification walkthrough, see the Standard API playbook.
8.1. Overview
The CQA API has four parts:
Part | What it does | Section |
|---|---|---|
Data Import API | Push interactions into CQA — one at a time, in a batch, or as a file. | 6.4 |
Analysis API | Retrieve quality analysis results. | 6.5 |
File schemas | CSV and NDJSON formats for file-based ingestion. | 6.6 |
Webhooks / callbacks | Notifications CQA sends to your system as interactions are processed. | 6.7 |
8.2. Authentication
All Data Import and Analysis API endpoints authenticate with an API key in the X-API-Key header.
API | Auth method | Header |
|---|---|---|
Data Import API | API key | X-API-Key: {key} |
Analysis API | API key | X-API-Key: {key} |
Getting a key. An Admin generates keys in Manage › Data Integration › Access API (page title API Keys). The key card shows the API Key and Account ID you need for every request. Follow the checklist in 5.4 Access API.
- Keys are scoped to one account and are used for all programmatic ingestion and analysis retrieval.
- An account can have at most 2 active keys.
- A revoked key disappears from the list immediately, but the API may continue to accept it for up to 24 hours. Put the replacement key in place before you revoke the old one.
- Store keys in a secrets manager. Never put them in tickets, chat, or documents.
8.3. Base URL and conventions
Base URL
All endpoints are served under the CQA context path /cqa/api/v1:
https://{host}/cqa/api/v1/accounts/{account_id}/...- {host} is your CQA console host — for example, cqa-console.in.exotel.com.
- {account_id} is the Account ID shown on your API key card.
Response envelope
All Data Import and Analysis API endpoints return the same envelope.
Success:
{
"status": 200,
"request_id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
"data": { }
}Error:
{
"status": 400,
"request_id": "d4f5a6b7-c8d9-4e0f-a1b2-c3d4e5f6a7b8",
"message": "Descriptive error message",
"error": {
"code": "VALIDATION_ERROR"
}
}Field | Type | Description |
|---|---|---|
status | integer | HTTP status code, mirrored in the body. |
request_id | string | Unique request identifier. Include it when you contact support. |
message | string | Human-readable detail. Present on most errors; may be omitted when redundant. |
data | object | Response payload (on success). |
error | object | Present on failure. Contains code only. |
Error codes
Code | HTTP status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Request failed validation (missing required fields, exceeded limits). |
INVALID_JSON | 400 | Request body is not valid JSON. |
INVALID_REQUEST | 400 | Request contains invalid arguments. |
UNAUTHORIZED | 401 | Missing or invalid API key. |
NOT_FOUND | 404 | The requested resource was not found. |
DUPLICATE | 409 | An interaction with this external_interaction_id already exists (single ingest). |
RATE_LIMIT_EXCEEDED | 429 | Tenant or user rate limit exceeded. See 6.8 Limits. |
TOO_MANY_JOBS | 429 | Too many concurrent file ingestion jobs for this account. |
CONVERSATION_CAP_EXCEEDED | 429 | The account's daily, weekly, or monthly conversation cap is reached. Admins can see caps and usage in footer Usage & Limit (7.1). |
INTERNAL_ERROR | 500 | An unexpected server error occurred. |
8.4. Data Import API
The Data Import API is the main way to push interaction data into CQA from external systems. It supports three modes: single, batch, and file.
Base path: /cqa/api/v1/accounts/{account_id}/ingress · Auth: X-API-Key header
Content and URL requirements
- Every interaction needs at least one of audio_url, transcript_url, or transcript_text.
- audio_url and transcript_url must be publicly accessible or pre-signed, so CQA can fetch them.
- If both transcript_url and transcript_text are sent, transcript_url is used and the inline text is ignored.
Ingest a single interaction
Submits one interaction for quality analysis and returns immediately with a tracking ID.
POST https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactionsRequest body (JSON)
Parameter | Required | Type | Description |
|---|---|---|---|
external_interaction_id | Yes | string | Your unique identifier for the interaction. Used for deduplication: an interaction with an ID that already exists is rejected. |
channel_type | Yes | string | Interaction channel. Recognized values: VOICE, CHAT, EMAIL, SMS, WHATSAPP. Other values are accepted. |
source | No | string | Originating system (for example, my-pbx, genesys). |
language | No | string | Language code (for example, en, hi, es). |
interaction_start_time | No | string (ISO-8601) | When the interaction started (for example, 2026-04-01T10:30:00Z). |
duration_seconds | No | integer | Duration of the interaction in seconds. |
audio_format | No | string | Audio format hint (for example, WAV, MP3, OGG). |
callback_url | No | string | URL for status notifications. See 6.7 Webhooks / callbacks. |
audio_url | If no transcript | string | Direct URL to the audio recording. |
transcript_url | If no audio or transcript_text | string | Direct URL to the transcript file. |
transcript_text | If no audio or transcript_url | string | Inline transcript text. Max 100 KB (UTF-8). |
pii_redacted | No | boolean | Default false. Set to true if PII is already redacted in the content you send — by your own process or by CQA's PII redaction — so CQA does not redact it again. |
metadata | No | object | Key-value pairs for tagging. Max 50 keys. Values can be strings, numbers, or booleans. These become the interaction's metadata. |
Example — voice call with audio URL
curl -X POST "https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactions" \
-H "X-API-Key: {your_api_key}" \
-H "Content-Type: application/json" \
-d '{
"external_interaction_id": "call-2026-04-01-001",
"channel_type": "VOICE",
"source": "my-pbx",
"language": "en",
"interaction_start_time": "2026-04-01T10:30:00Z",
"duration_seconds": 300,
"audio_format": "WAV",
"audio_url": "https://storage.example.com/recordings/call-001.wav",
"pii_redacted": false,
"callback_url": "https://my-app.example.com/webhooks/cqa",
"metadata": {
"agent_id": "agent-42",
"campaign": "retention-q2",
"disposition": "RESOLVED"
}
}'Example — chat with transcript URL
curl -X POST "https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactions" \
-H "X-API-Key: {your_api_key}" \
-H "Content-Type: application/json" \
-d '{
"external_interaction_id": "chat-2026-04-01-001",
"channel_type": "CHAT",
"source": "whatsapp",
"language": "en",
"interaction_start_time": "2026-04-01T10:30:00Z",
"transcript_url": "https://storage.example.com/messages/chat-001.txt"
}'Example — chat with inline transcript
curl -X POST "https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactions" \
-H "X-API-Key: {your_api_key}" \
-H "Content-Type: application/json" \
-d '{
"external_interaction_id": "chat-2026-09-03-001",
"channel_type": "CHAT",
"source": "web-chat",
"language": "en",
"transcript_text": "Agent: Hello! How can I help you today?\nCustomer: I need to reset my password.\nAgent: Sure, I can help with that. Let me send you a reset link."
}'Responses
201 Created — interaction queued:
{
"status": 201,
"request_id": "req-abc-123",
"message": "Queued for processing",
"data": {
"interaction_id": "550e8400-e29b-41d4-a716-446655440000",
"external_interaction_id": "call-2026-04-01-001",
"status": "queued"
}
}409 Conflict — duplicate external_interaction_id:
{
"status": 409,
"request_id": "req-abc-124",
"message": "Duplicate: interaction with this external_interaction_id already exists",
"error": { "code": "DUPLICATE" }
}429 Too Many Requests — conversation cap reached:
{
"status": 429,
"request_id": "req-abc-126",
"message": "Daily conversation cap exceeded: 50/50",
"error": { "code": "CONVERSATION_CAP_EXCEEDED" }
}400 Bad Request — validation error:
{
"status": 400,
"request_id": "req-abc-125",
"message": "metadata must not exceed 50 keys",
"error": { "code": "VALIDATION_ERROR" }
}Response fields (data)
Field | Type | Description |
|---|---|---|
interaction_id | string (UUID) | CQA-assigned unique identifier. |
external_interaction_id | string | Your identifier, echoed back. |
status | string | queued on success. |
message | string | Top-level hint on 201 (for example, "Queued for processing"). On errors, the detail text is in message, not inside error. |
On errors, the detail text is in the top-level message, not inside error.
Ingest a batch of interactions
Submits 1 to 100 interactions as a single asynchronous job and returns a job id for tracking.
POST https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactions/batchRequest body (JSON)
Parameter | Required | Type | Description |
|---|---|---|---|
interactions | Yes | array | 1–100 interaction objects, each with the same fields as single ingest. |
Each item's transcript_text can be at most 100 KB (102,400 bytes). If any item exceeds this, the whole request is rejected with 400 VALIDATION_ERROR and no job is created.
Response fields (data)
Field | Type | Description |
|---|---|---|
id | string | Job identifier. Use it with Track a batch or file job. |
type | string | Always batch. |
status | string | pending — the job is accepted and queued. |
Partial acceptance. If some items fail request-time validation and others are valid, the API returns 202 Accepted. Valid items are queued; invalid items are rejected immediately. The response can include message, accepted, rejected, and errors. Use this response for the rejected items — the tracking endpoint mainly reflects accepted items.
Submit a file for ingestion
Submits the URL of a CSV or NDJSON file. CQA downloads and processes it in the background.
POST https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactions/filesRequest body (JSON)
Parameter | Required | Type | Description |
|---|---|---|---|
file_url | Yes | string | URL to the file. Schemes: https:// (recommended), http:// (no transport encryption), s3://. Private and local addresses (localhost, 127.0.0.1, 10.x, 192.168.x, 172.16.x) are rejected. |
format | Yes | string | csv or ndjson. See 6.6 File schemas. |
source | No | string | Default source for rows that do not set one. |
pii_redacted | No | boolean | Default PII flag for rows that do not set one. |
callback_url | No | string | Default callback URL for rows that do not set one. |
column_mapping | No | object | CSV only. Maps your headers to canonical column names. See Column mapping. |
metadata | No | object | Default metadata merged into every row (row keys win). Keep each merged row within the 50-key limit. |
The response has the same shape as a batch submission (id, type = file, status = pending). Invalid rows do not fail the request; they are reported as rejected rows in the job results.
Get an interaction
Returns the current status and details of an ingested interaction.
GET https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactions/{interaction_identifier}Path parameter | Required | Description |
|---|---|---|
account_id | Yes | Your CQA Account ID. |
interaction_identifier | Yes | Either the CQA interaction_id (UUID) or your external_interaction_id. |
Response fields (data)
Field | Type | Description |
|---|---|---|
interaction_id | string (UUID) | CQA-assigned identifier. |
external_interaction_id | string | Your identifier. |
batch_id | string | Job ID if the interaction came from a batch or file. Omitted for single ingest. |
channel_type | string | Channel type as submitted. |
source | string | Originating system. |
status | string | queued, processing, completed, or failed. |
status_modified_at | string (ISO-8601) | When the status last changed. |
failure_reason | string | Why processing failed. Present only when status is failed. |
audio_url | string | Resolved audio URL. |
transcript_url | string | Resolved transcript URL. |
pii_redacted | boolean | Whether PII was flagged as already redacted. |
created_at | string (ISO-8601) | When the interaction was ingested. |
metadata | object | Key-value metadata. |
analyses | array | Analyses triggered for this interaction. Each has analysis_id, profile_id, and status. |
Interaction status lifecycle
Applies to status on interactions and analyses.
Status | Meaning |
|---|---|
queued | Accepted, waiting to be processed. |
processing | Analysis is underway. |
completed | All analyses finished successfully. |
failed | Processing failed. Check failure_reason. |
Job status lifecycle
Applies to status in the batch or file 202 response and to job_status in the tracking response. Job statuses are separate from interaction statuses.
Status | Meaning |
|---|---|
pending | Job accepted and queued. Returned in the initial response. |
processing | A worker is processing rows. |
completed | All rows processed. Check accepted and rejected for counts. |
failed | The whole job failed. Check error_message. |
Track a batch or file job
Returns the interactions in a batch or file job, with pagination and job-level status.
GET https://{host}/cqa/api/v1/accounts/{account_id}/ingress/interactions/batch/{id}Path parameter | Required | Description |
|---|---|---|
account_id | Yes | Your CQA Account ID. |
id | Yes | The job id returned when you submitted the batch or file (data.id). |
Query parameter | Default | Max | Description |
|---|---|---|---|
page | 0 | — | Zero-based page index. |
size | 20 | 100 | Interactions per page. Larger values are clamped to 100. |
Response fields (data)
Field | Type | Description |
|---|---|---|
id | string | The job identifier. |
total | integer | Total interactions in the job. |
interactions | array | Paginated interaction detail objects (same fields as Get an interaction). |
pagination | object | page, size, total_elements, total_pages. |
job_status | string | pending, processing, completed, or failed. |
job_type | string | batch or file. |
total_rows | integer | Rows found in the input (accepted + rejected). |
accepted | integer | Rows processed successfully. |
rejected | integer | Rows that failed validation. |
errors | array | Up to 100 entries, each with line (row number), reason, and external_interaction_id. |
error_message | string | Top-level error if the whole job failed. |
completed_at | string (ISO-8601) | When the job finished. |
8.5. Analysis API
Retrieve quality analysis results.
Base path: /cqa/api/v1/accounts/{account_id}/analyses · Auth: X-API-Key header
List analyses
Returns a paginated list of analyses that match your filters. A date filter is required, and the range can be at most 31 days.
POST https://{host}/cqa/api/v1/accounts/{account_id}/analyses?limit={limit}&offset={offset}Query parameter | Default | Max | Description |
|---|---|---|---|
limit | 20 | 100 | Results per page. |
offset | 0 | 10000 | Zero-based result offset. |
Request body (JSON)
Parameter | Required | Type | Description |
|---|---|---|---|
other_filters | Yes | array | Filter objects. Must include a date filter. |
quality_profile_uid | No | string | Filter by Quality Profile UUID. |
metadata_filter_group | No | array of arrays | Conditions on interaction metadata. Each inner array is an AND group; the outer array is OR. |
Date filter (required)
Field | Value |
|---|---|
field | "date" |
operator | "range" |
value | Object with start_date and end_date (YYYY-MM-DD or ISO-8601). Max range 31 days. |
Score filter (optional)
Field | Value |
|---|---|
field | "score" |
operator | "greater_than", "less_than", "greater_than_or_equal", "less_than_or_equal", or "equals" |
value | Numeric score. |
Example request
curl -X POST "https://{host}/cqa/api/v1/accounts/{account_id}/analyses?limit=5&offset=0" \
-H "X-API-Key: {your_api_key}" \
-H "Content-Type: application/json" \
-d '{
"other_filters": [
{"field": "date", "operator": "range", "value": {"start_date": "2026-07-01", "end_date": "2026-07-15"}},
{"field": "score", "operator": "greater_than_or_equal", "value": 9}
]
}'Example response (200 OK)
{
"status": 200,
"request_id": "51f6705d-6639-41ed-9029-d6053c5df1c2",
"data": {
"analyses": [
{
"analysis_id": "9f77853f-c4d4-482c-9651-b13394d65020",
"interaction_id": "a30f0175-2ba0-46d8-8002-5b84667bfb44",
"external_interaction_id": "file_7b1eeafb-f720-40bb-862f-60107f118e93",
"profile_id": "e7d721ce-035d-4f79-ab62-e9cc2138f449",
"status": "completed",
"ai_score": 19.0,
"qa_score": 21.0,
"final_score": 21.0,
"criticality_adjusted_score": 21.0,
"metadata": {
"source": "TestEvaluation",
"agent_id": "qa_user",
"campaign_id": "100"
},
"created_at": "2026-05-29T17:16:46.259993+05:30"
}
],
"pagination": { "limit": 5, "offset": 0, "total": 1 }
}
}Analysis fields
Field | Type | Description |
|---|---|---|
analysis_id | string (UUID) | Unique analysis identifier. |
interaction_id | string (UUID) | The interaction this analysis belongs to. |
external_interaction_id | string | Your identifier. May be null if the interaction record is unavailable. |
profile_id | string | Quality Profile used for scoring. |
status | string | completed, failed, or queued. |
ai_score | float | AI-generated score. |
qa_score | float | Manual QA score. Omitted if there was no manual review. |
final_score | float | Effective score: QA score if present, otherwise AI score. |
criticality_adjusted_score | float | Score after criticality rules are applied. |
metadata | object | Interaction metadata. |
created_at | string (ISO-8601) | When the analysis was created, in the account time zone. |
Pagination fields: limit (requested page size), offset (current offset), total (matching results across all pages).
Errors
Scenario | HTTP | Code | Example message |
|---|---|---|---|
Missing date filter | 400 | VALIDATION_ERROR | "Date filter is required. Provide a date filter with start_date and end_date in other_filters." |
Date range over 31 days | 400 | VALIDATION_ERROR | "Date range must not exceed 31 days" |
start_date after end_date | 400 | VALIDATION_ERROR | "start_date must be before or equal to end_date" |
Invalid date format | 400 | VALIDATION_ERROR | "Invalid start_date format. Expected: YYYY-MM-DD or ISO-8601" |
Invalid score operator | 400 | VALIDATION_ERROR | "Unsupported score filter operator" |
limit out of range | 400 | VALIDATION_ERROR | "limit must be between 1 and 100" |
offset out of range | 400 | VALIDATION_ERROR | "offset must be between 0 and 10000" |
Get analysis detail
Returns the full scoring breakdown for one analysis: categories, sub-categories, and KPIs.
GET https://{host}/cqa/api/v1/accounts/{account_id}/analyses/{analysis_id}Path parameter | Required | Description |
|---|---|---|
account_id | Yes | Your CQA Account ID. |
analysis_id | Yes | The analysis UUID (from the analyses array in Get an interaction, or from List analyses). |
Response fields (data)
Field | Type | Description |
|---|---|---|
analysis_id | string (UUID) | Unique analysis identifier. |
interaction_id | string (UUID) | The interaction this analysis belongs to. |
external_interaction_id | string | Your identifier. |
profile_id | string | Quality Profile used for scoring. |
profile_name | string | Quality Profile name. |
status | string | queued, processing, completed, or failed. |
ai_score | float | AI-generated score. |
qa_score | float | Manual QA score. Omitted if no manual review has occurred. |
final_score | float | QA score if present, otherwise AI score. |
criticality_adjusted_score | float | Score after criticality rules are applied. |
max_score | float | Maximum possible score for this profile. |
analysis_completed_at | string (ISO-8601) | Timestamp derived from the interaction's last status change in the account timezone (not a separate analysis-completion clock). May not equal a pure "analysis finished" instant in all edge cases. |
failure_reason | string | Not populated in the current response (null omitted). Use status and support channels when an analysis fails. |
categories | array | Scored categories (see below). |
metadata | object | Interaction metadata. |
Category object
Field | Type | Description |
|---|---|---|
name | string | Category name (for example, "Communication Skills"). |
ai_score | float | AI score for the category. |
qa_score | float | Manual QA score. Omitted if not set. |
final_score | float | Final category score. |
criticality_adjusted_score | float | Criticality-adjusted score. |
max_score | float | Maximum possible score. |
sub_categories | array | Sub-categories in this category. |
Sub-category object
Field | Type | Description |
|---|---|---|
name | string | Sub-category name. |
qa_score | float | Manual QA score. Omitted if not set. |
kpis | array | KPIs scored in this sub-category. |
KPI object
Field | Type | Description |
|---|---|---|
kpi_name | string | KPI name (for example, "Proper Greeting"). |
ai_response | string | The AI's answer (for example, "Yes", "No", "Partially"). |
ai_justification | string | The AI's reasoning. |
ai_suggestion | string | AI coaching suggestion. Omitted if not set. |
ai_score | float | AI score for the KPI. |
qa_score | float | Manual QA score. Omitted if not set. |
qa_justification | string | QA justification. Omitted if not set. |
qa_selected_kpi_option_uid | string | UID of the KPI option the reviewer selected, if any. |
qa_selected_kpi_option_value | string | Label of the KPI option the reviewer selected. |
user_comment | string | Free-text reviewer comment. |
final_score | float | Final KPI score (may reflect a QA override). |
criticality_adjusted_score | float | Criticality-adjusted score. |
max_score | float | Maximum possible score for the KPI. |
8.6. File schemas
The file endpoint (Submit a file for ingestion) accepts CSV and NDJSON. The same CSV layout is used when you import files through an S3 or SFTP connector (Customer S3, Customer SFTP).
Every row needs at least one of audio_url, transcript_url, or transcript_text. Recording and transcript URLs must be publicly accessible or pre-signed.
CSV schema
The first row must contain column headers. Headers are trimmed and lowercased before they are matched to the canonical names below.
Column | Required | Type | Description |
|---|---|---|---|
external_interaction_id | Yes | string | Your unique interaction identifier. |
channel_type | Yes | string | VOICE, CHAT, EMAIL, SMS, or WHATSAPP. |
source | No | string | Originating system identifier. |
language | No | string | Language code (for example, en). |
interaction_start_time | No | string (ISO-8601, UTC) | For example, 2026-04-01T10:00:00Z. |
duration_seconds | No | integer | Interaction duration in seconds. |
audio_format | No | string | Format hint (for example, WAV, MP3). |
callback_url | No | string | Per-row callback URL. See 6.7. |
pii_redacted | No | boolean | true or false. |
audio_url | If no transcript | string | Audio file URL(s). Separate multiple URLs with ;. |
transcript_url | If no audio or transcript_text | string | Transcript file URL(s). Separate multiple URLs with ;. |
transcript_text | If no audio or transcript_url | string | Transcript text. If transcript_url is also set, transcript_url is used. |
file_url | No | string | Generic file URL, used with file_type when no audio_url or transcript_url is set. |
file_type | No | string | File extension for type resolution. Audio: mp3, wav, ogg, flac, m4a, aac, wma, amr. Transcript: txt, pdf, doc, docx, srt, vtt. |
Extra columns become metadata
Any header that is not a canonical column is added to the row's metadata. For example, columns named agent, campaign, or disposition become metadata keys with no extra configuration. To use those fields for filters, routing, access control, or AI context, configure them in 5.2 Metadata.
Column mapping
If your CSV uses different headers, send a column_mapping object with the file request. Keys are your original headers (trimmed, lowercased); values are canonical names.
For a CSV with headers call_id,type,recording,agent,campaign:
{
"column_mapping": {
"call_id": "external_interaction_id",
"type": "channel_type",
"recording": "audio_url"
}
}After mapping, agent and campaign are not canonical, so they become metadata.
Example CSV
external_interaction_id,channel_type,audio_url,transcript_url,transcript_text,language,agent,campaign
call-001,VOICE,https://s3.example.com/rec-001.wav,,,en,agent-42,retention
call-002,VOICE,https://s3.example.com/rec-002.wav,,,hi,agent-15,support
chat-001,CHAT,,,"Agent: Hello! How can I help you today?\nCustomer: I need to reset my password.",en,agent-42,retention
call-003,CHAT,,,,,agent-42,retention- call-001 and call-002 are valid through audio_url.
- chat-001 is valid through transcript_text.
- call-003 is rejected because it has no audio_url, transcript_url, or transcript_text.
NDJSON schema
NDJSON (newline-delimited JSON) has one JSON object per line. Each object uses the same fields as the single ingest request body, including a nested metadata object. Blank lines are skipped; a line that is not valid JSON is reported as a rejected row. column_mapping does not apply to NDJSON.
{"external_interaction_id":"call-001","channel_type":"VOICE","audio_url":"https://s3.example.com/rec-001.wav","language":"en","metadata":{"agent":"agent-42","campaign":"retention"}}
{"external_interaction_id":"chat-001","channel_type":"CHAT","transcript_text":"Agent: Hello! How can I help you today?\nCustomer: I need to reset my password.","language":"en","metadata":{"agent":"agent-42"}}Request-level defaults
For both CSV and NDJSON, fields on the file request apply to every row:
Request field | Behavior |
|---|---|
source | Applied to rows where source is null or empty. |
pii_redacted | Applied to rows where the value is null. |
callback_url | Applied to rows where the value is null. |
metadata | Merged with each row's metadata. Row-level keys take precedence. |
8.7. Webhooks / callbacks
When a callback_url is set — on a single ingest, on a batch or file request, or per row — CQA sends HTTP POST notifications to that URL at key points in the interaction's or file job's lifecycle.
Enablement. Callback delivery is turned on per account by Exotel. Until it is on, callback_url values are stored but no requests are sent. Contact your Customer Success Manager to enable it.
Delivery
- Method: HTTP POST
- Content-Type: application/json
- Timeout: 10 seconds (connect and read)
Event types
Event | Trigger |
|---|---|
INTERACTION_INGESTED | The interaction has been accepted and stored. |
INTERACTION_ANALYSIS_IN_PROGRESS | Analysis has started for a Quality Profile. |
INTERACTION_ANALYSIS_COMPLETED | One analysis completed. Payload includes scores and KPI results. |
INTERACTION_ANALYSIS_FAILED | One analysis failed. Payload includes error details. |
INTERACTION_DISPUTE_RAISED | A dispute was raised on an analysis. See 3.9 Disputes. |
INTERACTION_DISPUTE_RESOLVED | A dispute was resolved. |
FILE_INGESTION_COMPLETED | A file ingestion job finished processing. |
Payload structure
Every callback body is a JSON object with these top-level fields:
Field | Type | Description |
|---|---|---|
event | string | The event type. |
deliveryId | string (UUID) | Unique identifier for this delivery attempt. |
timestamp | string (ISO-8601) | When the callback was generated (for example, 2026-04-01T10:35:42.123Z). |
accountId | string | Your account identifier. |
interactionId | string (UUID) | CQA's interaction identifier. Present for interaction-level events. |
externalInteractionId | string | Your external_interaction_id, when available. |
data | object | Event-specific data (below). |
Data by event type
INTERACTION_INGESTED
{ "status": "INGESTED" }INTERACTION_ANALYSIS_IN_PROGRESS
{
"analysisId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"profileId": "prof-001",
"profileName": "Inbound Support"
}INTERACTION_ANALYSIS_COMPLETED — kpiResults is included when KPI-level results are available. Each entry has the KPI, its category and sub-category, the AI's response and justification, and the scores.
{
"analysisId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "COMPLETED",
"profileId": "prof-001",
"aiScore": 85.0,
"qaScore": null,
"finalScore": 85.0,
"criticalityAdjustedScore": 85.0,
"analysisCompletedAt": "2026-04-01T10:35:42Z",
"kpiResults": [
{
"kpiId": "kpi-101",
"categoryId": "cat-01",
"subCategoryId": "subcat-01",
"aiResponse": "Yes",
"aiJustification": "The agent greeted the customer by name.",
"aiScore": 5.0,
"qaScore": null,
"finalScore": 5.0,
"criticalityAdjustedScore": 5.0
}
]
}INTERACTION_ANALYSIS_FAILED
{
"errorCode": "ANALYSIS_FAILED",
"errorMessage": "Transcript processing timed out"
}INTERACTION_DISPUTE_RAISED
{
"analysisId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"disputeId": "d1e2f3a4-b5c6-7890-abcd-ef1234567890"
}INTERACTION_DISPUTE_RESOLVED
{
"analysisId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"disputeId": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
"resolution": "ACCEPTED",
"updatedScore": 90.0
}FILE_INGESTION_COMPLETED
{
"fileJobId": "job-2026-04-01-001",
"status": "FILE_INGESTION_COMPLETED",
"totalRows": 500,
"accepted": 498,
"rejected": 2,
"completedAt": "2026-04-01T11:05:00Z",
"errors": [
{ "row": 42, "reason": "Missing required field: external_interaction_id" }
]
}Full example
A complete callback for a completed analysis:
{
"event": "INTERACTION_ANALYSIS_COMPLETED",
"deliveryId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"timestamp": "2026-04-01T10:35:42.123Z",
"accountId": "{account_id}",
"interactionId": "550e8400-e29b-41d4-a716-446655440000",
"externalInteractionId": "call-2026-04-01-001",
"data": {
"analysisId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "COMPLETED",
"profileId": "prof-001",
"aiScore": 85.0,
"qaScore": null,
"finalScore": 85.0,
"criticalityAdjustedScore": 85.0,
"analysisCompletedAt": "2026-04-01T10:35:42Z",
"kpiResults": [
{
"kpiId": "kpi-101",
"categoryId": "cat-01",
"subCategoryId": "subcat-01",
"aiResponse": "Yes",
"aiJustification": "The agent greeted the customer by name.",
"aiScore": 5.0,
"qaScore": null,
"finalScore": 5.0,
"criticalityAdjustedScore": 5.0
}
]
}
}Verify the signature (HMAC)
Each callback includes headers you can use to check that it came from CQA:
Header | Description |
|---|---|
X-CQA-Signature | HMAC-SHA256 of the request body, formatted as sha256=<hex>. |
X-CQA-Timestamp | ISO-8601 time the request was sent (for example, 2026-04-01T10:35:42.123Z). |
To verify a callback:
- Take the hex digest from X-CQA-Signature (remove the sha256= prefix).
- Compute HMAC-SHA256(secret, requestBody) with your signing secret.
- Compare your digest with the one from step 1.
The signing secret is derived from your active API key and is shared with you during onboarding. If your account has two active keys, or you rotate a key, confirm the current signing secret with CQA Support.
Retry policy
If your endpoint returns 5xx or 429, or the request times out, CQA retries. There are 3 attempts in total:
Attempt | Delay before attempt |
|---|---|
1st (initial) | Immediate |
2nd (1st retry) | About 10 seconds |
3rd (2nd retry) | About 30 seconds |
After 3 failed attempts, the delivery is marked FAILED. Responses with 2xx or 4xx (other than 429) are not retried.
8.8. Limits
This is the single reference for API limits. Defaults are typical per account (tenant) and can differ by plan.
Rate limits
Requests over the limit receive 429 Too Many Requests with RATE_LIMIT_EXCEEDED. Too many concurrent file jobs is a separate 429 with TOO_MANY_JOBS.
Endpoint pattern | Method | Default limit |
|---|---|---|
/ingress/interactions* (ingest) | POST | 100 requests per minute |
/ingress/** (tracking) | GET | 300 requests per minute |
Request and file limits
Constraint | Value |
|---|---|
Interactions per batch | 1–100 |
Metadata keys per interaction | 50 (keep merged file-row metadata within this) |
transcript_text size | 100 KB (UTF-8) |
Rows per file job | 100,000 |
File size per file job | 100 MB |
Concurrent file jobs per account | 5 (default) |
File formats | csv, ndjson |
File URL schemes | https (recommended), http, s3 |
Channel types | VOICE, CHAT, EMAIL, SMS, WHATSAPP (other values accepted) |
Batch tracking page size | 100 max (larger values clamped) |
List analyses date range | 31 days max |
List analyses limit / offset | 100 / 10,000 max |
Conversation caps
Each account has daily, weekly, or monthly conversation caps. When a cap is reached, ingest requests return 429 CONVERSATION_CAP_EXCEEDED until the period resets or the plan changes. Trial accounts with no explicit cap default to 300 conversations per month.
Admins can see caps and usage in footer Usage & Limit (7.1 Usage & Limit) and can set up warning emails in Alert & Notification.