QA reviews
Scores a call your agents handled on your own phone platform against one of your QA rubrics, using the same AI review as calls placed through DialCru. Send the call's transcript, or a link to its recording for us to transcribe, and the rubric to score it against. The review runs in the background. A qa_review.completed webhook says when it has finished, and GET /qa/reviews/{id} returns the report.
Rubrics are written in the dashboard under Manage → QA, or through QA rubrics. Reviews requested here never appear in the dashboard's QA reports: those describe calls placed through DialCru. Your agents are yours to track, so put your own agent or campaign reference in metadata. It is returned on every read.
| Route | Scope | Cost |
|---|---|---|
POST /qa/reviews | qa:write | 10 rate-limit tokens |
GET /qa/reviews | qa:read | 1 |
GET /qa/reviews/{id} | qa:read | 1 |
DELETE /qa/reviews/{id} | qa:write | 1 |
Seller organizations only. Each request also spends one token from your organization's own bucket of 60 a minute, shared by all its keys.
What it costs
Each review is charged to your organization's prepaid credit at what the AI review costs us, the same as a review in the dashboard. A recording is also charged for its transcription, per second of audio, the same as transcribing a call in the dashboard. A mono recording costs more, since its voices are told apart by diarization. When a mono recording does not say which voice is the agent's, a short extra AI step works it out, charged like the review. A request is refused with 402 PAYMENT_REQUIRED when the balance cannot cover one. A review that fails because the AI's answer could not be used is still charged, since it was charged to us. So is the transcription of a recording that turns out too long to review. Delete the review, and the charge stays on your usage log.
POST /qa/reviews
Answers 202 Accepted with the review, its status reviewing for a transcript and transcribing for a recording. Send exactly one of transcript or audio. Send an idempotency-key so a retried request makes one review, not two (Idempotency).
json
{
"rubric_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"external_id": "call-8812",
"transcript": {
"utterances": [
{
"speaker": "agent",
"text": "Good morning, this is Thandi from Acme.",
"start_ms": 0,
"end_ms": 3100
},
{
"speaker": "customer",
"text": "Morning.",
"start_ms": 3400,
"end_ms": 4000
}
]
},
"context": {
"direction": "outbound",
"started_at": "2026-11-03T09:15:00+02:00",
"duration_sec": 312,
"outcome": "sale",
"script": "Greet the customer by name. Read the recording notice word for word: …"
},
"metadata": { "agent_ref": "emp-123", "campaign": "Q4 funeral" }
}| Field | Required | Meaning |
|---|---|---|
rubric_id | Yes | An active rubric of your organization. An archived one is a 409. |
external_id | No | Your own id for the call, up to 128 characters, unique among your reviews. A second is a 409 naming the first. |
transcript.utterances | Yes | The conversation, one turn each, in order. 1–5 000 turns, up to 250 000 characters in all. |
utterances[].speaker | Yes | agent or customer, as your platform attributed the turn. |
utterances[].text | Yes | What was said, up to 5 000 characters. |
utterances[].start_ms | Yes | Milliseconds from the start of the recording. Each quote in the report is tied back to a turn by these. |
utterances[].end_ms | Yes | Not before start_ms. |
context.direction | No | inbound or outbound. |
context.started_at | No | ISO 8601 with an offset. |
context.duration_sec | No | The call's length. |
context.outcome | No | The disposition your agent filed, in your words, up to 200 characters. The report says whether the call supports it. |
context.script | No | The script and any disclosures to be read word for word, as plain text, up to 60 000 characters. Without it, script and compliance checks are not_applicable. |
metadata | No | Any JSON object up to 4 KB, returned on every read and never read by the review. |
The whole body may be at most 2 MB.
Sending a recording
Send audio in place of transcript:
json
{
"rubric_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"external_id": "call-8813",
"audio": {
"url": "https://recordings.example.com/call-8813.wav?signature=…",
"channels": "stereo",
"agent_channel": 1
},
"context": { "direction": "outbound", "duration_sec": 312 }
}| Field | Required | Meaning |
|---|---|---|
audio.url | Yes | An https link to the recording, up to 4 096 characters. It is fetched once, by our transcription provider. It must work with no sign-in, for at least an hour: a presigned link is fine. |
audio.channels | Yes | stereo: the agent and the customer each on their own channel. mono: both on one, told apart by voice. |
audio.agent_channel | No | Stereo only: 1 or 2, the agent's channel. |
audio.agent_speaks_first | No | true when the agent is the first voice heard, false when the customer is. |
Any common audio format works. A recording may be at most 2 hours long. A context.duration_sec over that is refused at once with a 400. Without one, the length is known only once the recording is transcribed, and a longer recording then fails as audio_too_long.
Who is the agent. On a stereo recording with agent_channel, the agent is on that channel. Otherwise agent_speaks_first decides, when you send it. With neither, the AI works out which voice is the agent's from how the call opens (who introduces themselves and the company). The review then judges that voice. agent_speaker on the review says which voice was taken as the agent's, and how. If it is wrong, delete the review and send it again with a hint. Do not assume whoever speaks first is the agent: on an outbound call, the customer usually answers first.
The review
json
{
"data": {
"id": "3f6b2c1d-8e4a-4b7c-9d2e-1a0f5c6b7d8e",
"external_id": "call-8812",
"source": "transcript",
"status": "completed",
"error": null,
"rubric": {
"id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"name": "Sales calls"
},
"context": {
"direction": "outbound",
"started_at": "2026-11-03T07:15:00.000Z",
"duration_sec": 312,
"outcome": "sale",
"has_script": true
},
"metadata": { "agent_ref": "emp-123", "campaign": "Q4 funeral" },
"agent_speaker": null,
"report": {
"score_pct": 84,
"auto_failed": false,
"summary": "A clear, friendly call that confirmed the sale…",
"model": "gpt-6.1-sol",
"completed_at": "2026-11-03T09:16:30.000Z",
"detail_ok": true,
"scores": [
{
"criterion_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"name": "Recording notice",
"scale": "binary",
"weight": 3,
"critical": true,
"value": 1,
"comment": "Read the notice in full before any questions.",
"evidence": [
{
"quote": "this call is recorded",
"start_ms": 5200,
"end_ms": 8900,
"side": "agent"
}
]
}
],
"findings": {
"strengths": ["Confirmed the customer's details before quoting."],
"improvements": [],
"script_adherence": { "verdict": "pass", "notes": [] },
"compliance": { "verdict": "pass", "notes": [] },
"outcome": {
"verdict": "agrees",
"note": "The customer accepted the quoted premium."
},
"objections": [
{
"kind": "price",
"quote": "that's a lot a month",
"start_ms": 61000,
"end_ms": 62500,
"handled": true,
"resolved": true
}
]
}
},
"created_at": "2026-11-03T09:16:01.000Z",
"updated_at": "2026-11-03T09:16:01.000Z"
}
}source is what you sent: transcript or audio.
agent_speaker is set on a recording once its agent is known, and is null for a transcript, whose turns already say who spoke:
labelis the agent's voice: a channel ("1","2") on a stereo recording, or the speaker heard ("A","B") on a mono one.sourceis how it was told:caller(youragent_channeloragent_speaks_first),model(worked out by the AI), orsingle_voice(only one voice spoke, and it was taken as the agent's).
status is where the review is:
status | Meaning |
|---|---|
transcribing | The recording is being transcribed. Usually a few minutes. |
reviewing | The AI review is running. Usually under a minute. |
completed | Scored. report is filled in. |
failed | It will not be scored. error says why. |
error.code on a failed review — new codes may be added:
| Code | Meaning |
|---|---|
review_failed | The AI review did not complete. Send the call again to retry. |
audio_unreachable | The recording could not be downloaded from audio.url. |
rubric_unavailable | The rubric was archived before the review ran. |
transcription_failed | The recording could not be transcribed. |
audio_too_long | The recording is longer than 2 hours. |
The report:
score_pctis the weighted total, 0–100. It isnullwhen every criterion was N/A, and0when a criterion marked critical failed (auto_failed: true).scoreslists each criterion as it was worded when the call was scored, so editing the rubric later does not change a past report.valueis1/0for pass/fail and1–5for a graded criterion;nullis N/A.commentgives the reviewer's reasoning.evidencequotes the transcript.start_ms/end_mslocate the quote in your recording, andsidesays whose words they were. All three arenullwhen the quote could not be found in the transcript.findingscarries the reviewer's coaching notes, and three verdicts:script_adherenceandcomplianceare eachpass,concern,failornot_applicable.outcomeisagrees,disagrees,unclearornot_recorded. It compares the call withcontext.outcome.objectionslists each objection the customer raised.kindis one ofprice,not_interested,already_covered,no_need,call_back_later,needs_to_consult,trust,not_decision_maker,bad_timing,other.
detail_okisfalsein the rare case a stored report could not be read back.scoresandfindingsare then empty, and the headline fields still stand.
GET /qa/reviews/{id}?include=transcript adds transcript: the turns as stored, each with speaker, text, start_ms and end_ms. A recording's turns are named agent and customer as agent_speaker says. Until that is known, or on a recording that failed, they are unknown.
GET /qa/reviews
Your organization's reviews, newest first, paged by cursor (Pagination). Each review carries report with the headline fields only: scores and findings are left out, so fetch a review for those.
| Parameter | Meaning |
|---|---|
status | One of the statuses above. |
rubric_id | Reviews against one rubric. |
external_id | The review for one of your calls. |
created_since | Requested at or after this instant (ISO 8601 with an offset). |
created_before | Requested before this instant. |
DELETE /qa/reviews/{id}
Deletes the review, its transcript and its report, including our copy of a recording's transcription with its word timings, and answers { "data": { "id": "…", "deleted": true } }. A review still transcribing or reviewing is a 409; delete it once it has finished. An id that is not one of your reviews is a 404.
Where your data goes
The transcript you send is reviewed by OpenAI, in the United States, through Cloudflare's AI Gateway. A recording is fetched from your link and transcribed by AssemblyAI, in the European Union. Its transcript is then reviewed as above, and AssemblyAI's copy is deleted once we have it. We never download or keep the audio, and we keep nothing that can fetch it again: the link goes once the recording has been sent for transcription. We keep the transcript, the request's context and the report until you delete the review, or your organization is deleted. Under POPIA each is a transfer of personal information outside South Africa, so make sure your customers' consent covers it.