Call Quality Assessment API
Submit a call-centre transcript and get an instant quality score, letter grade (A+-E), nine-dimension breakdown with per-rule observations, and sentiment delta powered by a deterministic Service Excellence Index.
When to use this API
Use this API when you need a consistent, auditable quality score for a customer-care call transcript without building your own scoring pipeline.
The API accepts a JSON body carrying the transcript as plain text (with
speaker labels), runs it through the nine-dimension Service Excellence
Index, and returns a structured result: overall score (0-100), grade letter
(A+ to E), per-dimension scores with rule-level observations, call-type
classification, and sentiment delta. If your transcript lives in a
.txt, .json, or .html file, read it
client-side first (HTML tags stripped) and send its text content as the
transcript field — /upload is kept as an alias
of this same endpoint for callers migrating from the older agent-page name.
Language is auto-detected transcripts in Hindi, Tamil, Telugu, Kannada,
Malayalam, Marathi, or Punjabi are translated to English for scoring and the
original language is preserved in the response.
Authentication
All requests require a user-scoped API key passed as a bearer token.
Generate one in the platform under Settings → API Keys.
Keys are prefixed ok_ and expire after 90 days.
Authorization: Bearer ok_xxxxxxxxxxxxxxxxxxxxxxxx
401 unauthorized.
Parameters
JSON body fields
| Name | Type | Required | Description |
|---|---|---|---|
| transcript | string | required |
The call transcript to score, as a single-line string (escape newlines
as \n). Speaker labels are expected on each turn:
AGENT: ... / CUSTOMER: ... (case-insensitive;
colon or dash separator; CCE/CLIENT/CALLER
are also recognised). If the body has no transcript field,
the API also accepts a raw call-record shape (e.g.
{ "dialogue": [{ "speaker", "text" }] } ) and synthesizes one.
|
| lang | string | optional |
Language code to override auto-detection.
Accepted values: en, hi, ta,
te, kn, ml, mr,
pa. Omit to let the API detect from character scripts.
|
| categoryId | string | optional |
Call category code to override auto-detection.
Accepted values: CMPL (Complaint), SALE (Sales),
UPSL (Upsell), RETN (Retention),
QURY (Service Query), INFO (Information).
|
| subCategoryId | string | optional |
Sub-category code (e.g. CMPL.BIL for Billing Dispute,
SALE.PST for Postpaid New Connection). Supply alongside
categoryId. Omit to let the API infer.
|
| domain | string | optional |
Industry domain to override auto-detection. Accepted values:
telecom, banking, ecommerce,
food, tech. Omit to let the API classify the
domain from transcript content.
|
| accountDomain | string | optional |
Soft domain tiebreak hint (same accepted values as domain)
used only when the content-based classifier is ambiguous — a
low-confidence hint is ignored rather than forcing the wrong domain.
|
Request
# NOTE: transcript must be a single-line string — escape newlines as \n
curl -X POST https://platform.openknowra.ai/api/bff/v1/call-quality-assessment/analyze \
-H "Authorization: Bearer $OPENKNOWRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"transcript":"AGENT: Thank you for calling. How can I help you today?\nCUSTOMER: Hi, I have an issue with my bill."}'import os
import requests
API_KEY = os.environ["OPENKNOWRA_API_KEY"]
API_URL = "https://platform.openknowra.ai/api/bff/v1/call-quality-assessment/analyze"
def score_transcript(transcript: str) -> dict:
res = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={"transcript": transcript},
)
if res.status_code == 402:
err = res.json()["error"]
raise ValueError(
f"Insufficient credits: need {err['required']}, have {err['balance']}"
)
res.raise_for_status()
return res.json()
with open("./call_transcript.txt") as f:
result = score_transcript(f.read())
assessment = result["assessment"]
print(f"Grade: {assessment['grade']} Score: {assessment['overallScore']}/100")
for factor in assessment.get("factors", []):
print(f" {factor['label']}: {factor['raw']}/{factor['max']}")const API_KEY = process.env.OPENKNOWRA_API_KEY!;
const API_URL = "https://platform.openknowra.ai/api/bff/v1/call-quality-assessment/analyze";
async function scoreTranscript(transcript: string) {
const res = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ transcript }),
});
if (res.status === 402) {
const err = await res.json() as { error: { required: number; balance: number } };
throw new Error(
`Insufficient credits: need ${err.error.required}, have ${err.error.balance}`
);
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
const result = await scoreTranscript(
"AGENT: Thank you for calling. How can I help you today?\nCUSTOMER: Hi, I have an issue with my bill."
);
const { grade, overallScore, factors } = result.assessment;
console.log(`Grade: ${grade} Score: ${overallScore}/100`);
console.log(JSON.stringify(factors, null, 2));Response
200 OK Returns the scored call with overall grade, nine-dimension factor breakdown, call metadata, and sentiment delta.
{
"callId": "SE-CALL-a1b2c3d4e5f6",
"idempotent": false,
"assessment": {
"overallScore": 79,
"grade": "B",
"decision": "meets_standard",
"coachingPriority": "low",
"language": "en",
"callType": { "id": "CMPL", "label": "Complaint" },
"subCategory": { "id": "CMPL.BIL", "label": "Billing Dispute" },
"turnCount": 18,
"agentTurnCount": 9,
"customerTurnCount": 9,
"talkToListenRatio": 48,
"talkToListenBand": "balanced",
"customerSentimentLabel": "positive",
"sentimentTrend": "improving",
"outcomeLabel": "resolved",
"factors": [
{
"key": "empathy", "label": "Empathy & Tone", "raw": 10, "max": 12,
"percentScore": 83, "summary": "Strong empathetic tone throughout.",
"observations": [
{ "rule": "empathy_phrase_hit", "matched": true, "count": 3, "description": "Used empathetic acknowledgements 3 times." }
]
},
{ "key": "fcr", "label": "First Call Resolution", "raw": 11, "max": 14, "percentScore": 79, "observations": [] },
{ "key": "compliance", "label": "Compliance Checklist", "raw": 10, "max": 12, "percentScore": 83, "observations": [] },
{ "key": "listening", "label": "Active Listening", "raw": 8, "max": 10, "percentScore": 80, "observations": [] },
{ "key": "accuracy", "label": "Product / Plan Accuracy", "raw": 9, "max": 12, "percentScore": 75, "observations": [] },
{ "key": "solution", "label": "Solution Orientation", "raw": 8, "max": 10, "percentScore": 80, "observations": [] },
{ "key": "brand", "label": "Brand / Upsell", "raw": 7, "max": 10, "percentScore": 70, "observations": [] },
{ "key": "closure", "label": "Conversation Closure", "raw": 9, "max": 10, "percentScore": 90, "observations": [] },
{ "key": "sentiment", "label": "Sentiment Delta", "raw": 7, "max": 10, "percentScore": 70, "observations": [] }
]
},
"newBalance": 875
}
Each factor's score lives in raw/max (not
score/maxScore — that pair only appears on
historical assessments returned by GET /calls/{'{'}id{'}'}, once
persisted). percentScore, summary, and rule-level
observations are computed on every response.
Grade bands
| Grade | Score range | Decision | Coaching Priority |
|---|---|---|---|
| A+ | ≥ 95 | Excellence demonstrated | none |
| A | 85-94 | Excellence | none |
| B | 70-84 | Meets Standard | low |
| C | 50-69 | Coaching Recommended | medium |
| D | 30-49 | Below Standard | medium |
| E | 0-29 | Urgent Review | high |
Nine scoring dimensions
| Dimension | Max pts | What it measures |
|---|---|---|
| Empathy & Tone | 12 | Emotional attunement, warmth, and professional tone throughout the call. |
| First Call Resolution | 14 | Issue resolved in one call with no callback or follow-up needed. |
| Compliance Checklist | 12 | Adherence to regulatory scripts, mandatory disclosures, and identity verification. |
| Active Listening | 10 | Acknowledgement, paraphrasing, and confirming customer understanding. |
| Product / Plan Accuracy | 12 | Correct and complete product, pricing, and plan information provided; penalises vague terms. |
| Solution Orientation | 10 | Proactive effort to resolve the issue rather than deflect or escalate. |
| Brand / Upsell | 10 | Brand representation quality and contextually relevant upsell identification. |
| Conversation Closure | 10 | Quality of call wrap-up: summary, confirmation, warm sign-off. |
| Sentiment Delta | 10 | Change in customer emotional tone from call open to call close. |
Pricing
Credits are charged per MB of transcript (ceiling'd, minimum 1 MB) at a rate
gated by language, not a flat per-call fee. Rates are configurable per
workspace and can drift from these defaults — call
GET /pricing to read the live, currently configured figures
rather than hardcoding them.
| Action | Rate | Notes |
|---|---|---|
| Text scoring · English | 5 credits / MB | /upload, /analyze, /bulk-upload — gated by the lang hint on the request. |
| Text scoring · non-English | 8 credits / MB | Hindi, Tamil, Telugu, Kannada, Malayalam, Marathi, Punjabi. |
| Audio transcription | 3 credits / minute | /transcribe-upload. Ceiling'd, no free allowance; duration is read from the uploaded file, not the client. Scoring the resulting transcript afterward is billed separately at the text rate above. |
| Coached rewrite | 7 credits / MB | /coach-rewrite — the Coaching Mode add-on, priced by transcript size. |
200 with
unscorable: true and no fabricated score — any credits
deducted upfront for that request are refunded automatically, same as any
other failed/non-2xx outcome.
{
"textEnPerMb": 5,
"textNonEnPerMb": 8,
"audioPerMinute": 3,
"coachRewritePerMb": 7,
"creditPriceUsd": 0.1
}
Dashboard KPIs
Every scored response includes derived analytics fields alongside the
factors array. These do not affect overallScore
they are surface labels computed from the transcript for client-facing dashboards.
| Field | Type | Description |
|---|---|---|
| turnCount | integer | Total number of speaker turns in the transcript. |
| agentTurnCount | integer | Turns attributed to the agent. |
| customerTurnCount | integer | Turns attributed to the customer. |
| talkToListenRatio | integer | Percentage of total spoken words contributed by the agent (0-100). Healthy band: 30-50. |
| talkToListenBand | string |
One of under_engaged (<30 %), balanced (30-49 %),
agent_led (50-64 %), agent_dominates (≥65 %).
|
| customerSentimentLabel | string | Customer mood at call close: positive, neutral, or negative. |
| sentimentTrend | string | Direction of mood change: improving, stable, or worsening. |
| outcomeLabel | string |
Conversation outcome: resolved, partial,
unresolved, or transferred.
|
Audio Transcription (optional)
When you have a call recording instead of a ready-made transcript, use the
two-step audio pipeline. The API uploads the file to AWS Transcribe, performs
speaker diarization, and returns a AGENT: / CUSTOMER:
transcript you can pass straight to the upload endpoint.
503 Service
Unavailable.
Step 1 : Upload audio
Send as multipart/form-data. The uploaded file is pushed straight to
S3 and a Transcribe job is started. Billed at 3 credits per minute of audio
(ceiling'd, no free allowance) — duration is measured from the uploaded
file itself, deducted before the job starts and refunded if the upload or job
start fails. See Pricing.
| Field | Type | Required | Description |
|---|---|---|---|
| audio | file | required |
The audio file. Accepted formats: mp3, wav,
m4a, webm, ogg, flac.
Maximum size 30 MB.
|
| lang | string | optional |
AWS Transcribe language code, e.g. en-US, hi-IN,
ta-IN. Defaults to en-US.
|
curl -X POST https://platform.openknowra.ai/api/bff/v1/call-quality-assessment/transcribe-upload \
-H "Authorization: Bearer $OPENKNOWRA_API_KEY" \
-F "audio=@call_recording.mp3"{
"jobName": "se-call-1719388800000-a1b2c3d4",
"s3Key": "se-transcribe/inbox/1719388800000-a1b2c3d4.mp3",
"mediaFormat": "mp3",
"languageCode": "en-US",
"status": "IN_PROGRESS",
"message": "Transcription started · poll GET /transcribe-job/se-call-...",
"durationSeconds": 187,
"creditsCharged": 9,
"newBalance": 866
}
Step 2 : Poll job status
| Query param | Values | Description |
|---|---|---|
| speakerMode | string |
agentFirst (default) first detected speaker mapped to Agent.
customerFirst first speaker mapped to Customer.
Swap if the diarization assigns the wrong role.
|
{
"status": "COMPLETED",
"language": "en-US",
"transcript": "AGENT: Thank you for calling. How can I help?\nCUSTOMER: Hi, I have an issue with my bill.",
"turns": [
{ "speaker": "Agent", "startMs": 0, "endMs": 2400, "text": "Thank you for calling. How can I help?" },
{ "speaker": "Customer", "startMs": 2800, "endMs": 5100, "text": "Hi, I have an issue with my bill." }
]
}
When status is IN_PROGRESS, the response contains only
{ "status": "IN_PROGRESS", "language": "..." } poll again after a few
seconds. Transcription typically completes in 30-90 seconds for a 10-minute call.
Pass the returned transcript string as the transcript field
in a subsequent analyze request. Scoring it is billed
separately at the text rate — "transcribe once, score once", no extra flat
surcharge for the audio path.
Bulk scoring
Score up to 100 transcripts in a single request instead of calling
/analyze once per transcript.
| Field | Type | Required | Description |
|---|---|---|---|
| transcripts | array | required |
Array of items, up to 100 per request (25 MB total request
body). Each item is either a plain transcript string, or an object
{ "transcript": "...", "categoryId"?, "subCategoryId"?, "lang"? }
carrying its own per-item overrides explicit metadata on an item always
wins over any top-level default, and is otherwise auto-detected from that
item's text exactly as a single /analyze call would.
|
| categoryId / subCategoryId / lang | string | optional | Top-level defaults applied to every item that doesn't specify its own override. |
/analyze call (see Pricing) — and the
sum is deducted upfront. Anything that comes back ok: false or
unscorable: true junk, one-sided, or too-short input (e.g. a
transcript under ~12 words) returns unscorable: true with a
reason instead of a fabricated score is refunded that item's own
cost, never a flat count × rate.
# transcripts is an array of strings and/or objects up to 100 per request
curl -X POST https://platform.openknowra.ai/api/bff/v1/call-quality-assessment/bulk-upload \
-H "Authorization: Bearer $OPENKNOWRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"transcripts": [
"AGENT: Thank you for calling...\nCUSTOMER: Hi, I am calling about...",
{ "transcript": "AGENT: Hello!\nCUSTOMER: I want to upgrade my plan...", "categoryId": "SALE" }
]
}'{
"total": 2,
"succeeded": 1,
"failed": 1,
"results": [
{ "index": 0, "ok": true, "callId": "SE-CALL-9d21ab77e5f6", "assessment": { "overallScore": 74, "grade": "B", "decision": "meets_standard", "coachingPriority": "low", "factors": [ /* ... */ ] } },
{ "index": 1, "ok": true, "unscorable": true, "reason": "This transcript is not a scorable agent-customer conversation (too short, or missing a real two-sided exchange). Provide a fuller call with AGENT: and CUSTOMER: turns." }
],
"newBalance": 850
}
The platform's own Bulk Upload UI additionally accepts .json files
(a single object, or an array of items in the same shape above) and .zip
files (bundling multiple .txt/.html/.json
entries, including sub-folders one transcript per file, or many if a .json
entry holds an array); the same 100 transcripts / 25 MB limit applies whether you use
that UI or call the API directly as shown above.
Additional endpoints
Free, unmetered helper endpoints for building a scoring UI, plus a way to
re-fetch a previously scored call — none of these deduct credits except
/coach-rewrite.
| Method | Path | Credits | What it does |
|---|---|---|---|
| POST | /score | Free | Scores a transcript exactly like /analyze but skips persistence — no callId, no dedupe, nothing written. |
| POST | /preprocess | Free | Live parse preview while the user is still typing/pasting: turnCount, agentTurnCount, customerTurnCount, hasSpeakerLabels, languageDetected, formatNotes. |
| POST | /detect | Free | Pre-flight domain/category/sub-category/language detection only — no scoring. Lets a UI show detected defaults before the user commits to a paid call. |
| POST | /coach-rewrite | 7 credits / MB | Generates an AI-coached rewrite of the transcript. Body: { transcript, factors, weights, targetLang, callId }. Returns { improvedTranscript, improvedTranscriptEn, targetLang, focusDimensions }. |
| GET | /calls/{'{'}id{'}'} | Free | Fetches a previously persisted assessment by callId (e.g. SE-CALL-a1b2c3d4e5f6). |
| GET | /pricing | Free | Returns the live, currently configured credit rates — see Pricing. |
| GET | /reference | Free | Reference catalog: supported domains, categories/sub-categories, and languages. |
| GET | /weights | Free | Current per-dimension scoring weights used by the engine. |
All paths above are relative to /v1/call-quality-assessment.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | - | No transcript provided (and none could be synthesized from the body), or transcripts is missing/empty/over 100 items on /bulk-upload. |
| 401 | unauthorized | Missing, invalid, expired, or revoked API key. |
| 402 | insufficient_credits | Account balance too low. Response includes required (credits needed) and balance (current balance). |
| 502 | - | Upstream scoring service unreachable or timed out. Credits already deducted are refunded automatically (creditsRefunded/newBalance in the response); safe to retry. |
| 503 | call_quality_assessor_not_configured | The scoring/transcription service isn't provisioned for this workspace. |
| 500 | - | Scoring engine error. Safe to retry after a short delay. |
200 OK with
{ "unscorable": true, "reason": "..." } instead of a fabricated
grade, and any credits deducted for that request are refunded. See
Pricing.
{
"error": {
"code": "insufficient_credits",
"required": 25,
"balance": 10
}
}