Customer Value Management · API

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.

Documentation Pricing
POST /v1/call-quality-assessment/analyze
Auth Bearer token Content-Type application/json Status Stable · v1

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 header
Authorization: Bearer ok_xxxxxxxxxxxxxxxxxxxxxxxx
Heads-up. API keys are tied to the user account that generated them. Credit deductions are applied to that user's balance. A key that is revoked or expired returns 401 unauthorized.

Parameters

JSON body fields

NameTypeRequiredDescription
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.

application/json
{
  "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

GradeScore rangeDecisionCoaching Priority
A+≥ 95Excellence demonstratednone
A 85-94Excellencenone
B 70-84Meets Standardlow
C 50-69Coaching Recommendedmedium
D 30-49Below Standardmedium
E 0-29Urgent Reviewhigh

Nine scoring dimensions

DimensionMax ptsWhat it measures
Empathy & Tone 12Emotional attunement, warmth, and professional tone throughout the call.
First Call Resolution 14Issue resolved in one call with no callback or follow-up needed.
Compliance Checklist 12Adherence to regulatory scripts, mandatory disclosures, and identity verification.
Active Listening 10Acknowledgement, paraphrasing, and confirming customer understanding.
Product / Plan Accuracy 12Correct and complete product, pricing, and plan information provided; penalises vague terms.
Solution Orientation 10Proactive effort to resolve the issue rather than deflect or escalate.
Brand / Upsell 10Brand representation quality and contextually relevant upsell identification.
Conversation Closure 10Quality of call wrap-up: summary, confirmation, warm sign-off.
Sentiment Delta 10Change 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.

ActionRateNotes
Text scoring · English5 credits / MB/upload, /analyze, /bulk-upload — gated by the lang hint on the request.
Text scoring · non-English8 credits / MBHindi, Tamil, Telugu, Kannada, Malayalam, Marathi, Punjabi.
Audio transcription3 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 rewrite7 credits / MB/coach-rewrite — the Coaching Mode add-on, priced by transcript size.
Non-billable outcomes are refunded. A junk, one-sided, or too-short transcript (e.g. under ~12 words) returns 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.
200 OK · GET /pricing
{
  "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.

FieldTypeDescription
turnCount integerTotal number of speaker turns in the transcript.
agentTurnCount integerTurns attributed to the agent.
customerTurnCount integerTurns attributed to the customer.
talkToListenRatio integerPercentage 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 %).
customerSentimentLabelstringCustomer 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.

Note. These endpoints require AWS Transcribe + S3 credentials configured on the platform. If not provisioned, both return 503 Service Unavailable.

Step 1 : Upload audio

POST /v1/call-quality-assessment/transcribe-upload

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.

FieldTypeRequiredDescription
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"
200 OK job started
{
  "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

GET /v1/call-quality-assessment/transcribe-job/{jobName}
Query paramValuesDescription
speakerMode string agentFirst (default) first detected speaker mapped to Agent. customerFirst first speaker mapped to Customer. Swap if the diarization assigns the wrong role.
200 OK completed
{
  "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.

POST /v1/call-quality-assessment/bulk-upload
FieldTypeRequiredDescription
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.
Credits. Each item is priced individually by its own language and transcript size — the same per-MB tiers as a single /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" }
    ]
  }'
200 OK
{
  "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.

MethodPathCreditsWhat it does
POST/scoreFreeScores a transcript exactly like /analyze but skips persistence — no callId, no dedupe, nothing written.
POST/preprocessFreeLive parse preview while the user is still typing/pasting: turnCount, agentTurnCount, customerTurnCount, hasSpeakerLabels, languageDetected, formatNotes.
POST/detectFreePre-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-rewrite7 credits / MBGenerates an AI-coached rewrite of the transcript. Body: { transcript, factors, weights, targetLang, callId }. Returns { improvedTranscript, improvedTranscriptEn, targetLang, focusDimensions }.
GET/calls/{'{'}id{'}'}FreeFetches a previously persisted assessment by callId (e.g. SE-CALL-a1b2c3d4e5f6).
GET/pricingFreeReturns the live, currently configured credit rates — see Pricing.
GET/referenceFreeReference catalog: supported domains, categories/sub-categories, and languages.
GET/weightsFreeCurrent per-dimension scoring weights used by the engine.

All paths above are relative to /v1/call-quality-assessment.

Errors

StatusCodeMeaning
400-No transcript provided (and none could be synthesized from the body), or transcripts is missing/empty/over 100 items on /bulk-upload.
401unauthorizedMissing, invalid, expired, or revoked API key.
402insufficient_creditsAccount 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.
503call_quality_assessor_not_configuredThe scoring/transcription service isn't provisioned for this workspace.
500-Scoring engine error. Safe to retry after a short delay.
Not an error. A junk, one-sided, or too-short transcript doesn't 4xx — it returns 200 OK with { "unscorable": true, "reason": "..." } instead of a fabricated grade, and any credits deducted for that request are refunded. See Pricing.
402 - insufficient_credits
{
  "error": {
    "code":     "insufficient_credits",
    "required": 25,
    "balance":  10
  }
}