Skip to content

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.

RouteScopeCost
POST /qa/reviewsqa:write10 rate-limit tokens
GET /qa/reviewsqa:read1
GET /qa/reviews/{id}qa:read1
DELETE /qa/reviews/{id}qa:write1

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" }
}
FieldRequiredMeaning
rubric_idYesAn active rubric of your organization. An archived one is a 409.
external_idNoYour own id for the call, up to 128 characters, unique among your reviews. A second is a 409 naming the first.
transcript.utterancesYesThe conversation, one turn each, in order. 1–5 000 turns, up to 250 000 characters in all.
utterances[].speakerYesagent or customer, as your platform attributed the turn.
utterances[].textYesWhat was said, up to 5 000 characters.
utterances[].start_msYesMilliseconds from the start of the recording. Each quote in the report is tied back to a turn by these.
utterances[].end_msYesNot before start_ms.
context.directionNoinbound or outbound.
context.started_atNoISO 8601 with an offset.
context.duration_secNoThe call's length.
context.outcomeNoThe disposition your agent filed, in your words, up to 200 characters. The report says whether the call supports it.
context.scriptNoThe 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.
metadataNoAny 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 }
}
FieldRequiredMeaning
audio.urlYesAn 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.channelsYesstereo: the agent and the customer each on their own channel. mono: both on one, told apart by voice.
audio.agent_channelNoStereo only: 1 or 2, the agent's channel.
audio.agent_speaks_firstNotrue 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:

  • label is the agent's voice: a channel ("1", "2") on a stereo recording, or the speaker heard ("A", "B") on a mono one.
  • source is how it was told: caller (your agent_channel or agent_speaks_first), model (worked out by the AI), or single_voice (only one voice spoke, and it was taken as the agent's).

status is where the review is:

statusMeaning
transcribingThe recording is being transcribed. Usually a few minutes.
reviewingThe AI review is running. Usually under a minute.
completedScored. report is filled in.
failedIt will not be scored. error says why.

error.code on a failed review — new codes may be added:

CodeMeaning
review_failedThe AI review did not complete. Send the call again to retry.
audio_unreachableThe recording could not be downloaded from audio.url.
rubric_unavailableThe rubric was archived before the review ran.
transcription_failedThe recording could not be transcribed.
audio_too_longThe recording is longer than 2 hours.

The report:

  • score_pct is the weighted total, 0–100. It is null when every criterion was N/A, and 0 when a criterion marked critical failed (auto_failed: true).
  • scores lists each criterion as it was worded when the call was scored, so editing the rubric later does not change a past report. value is 1/0 for pass/fail and 1–5 for a graded criterion; null is N/A. comment gives the reviewer's reasoning.
  • evidence quotes the transcript. start_ms/end_ms locate the quote in your recording, and side says whose words they were. All three are null when the quote could not be found in the transcript.
  • findings carries the reviewer's coaching notes, and three verdicts:
    • script_adherence and compliance are each pass, concern, fail or not_applicable.
    • outcome is agrees, disagrees, unclear or not_recorded. It compares the call with context.outcome.
    • objections lists each objection the customer raised. kind is one of price, not_interested, already_covered, no_need, call_back_later, needs_to_consult, trust, not_decision_maker, bad_timing, other.
  • detail_ok is false in the rare case a stored report could not be read back. scores and findings are 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.

ParameterMeaning
statusOne of the statuses above.
rubric_idReviews against one rubric.
external_idThe review for one of your calls.
created_sinceRequested at or after this instant (ISO 8601 with an offset).
created_beforeRequested 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.