QA: scorecards, the rubric and insights
Quality checks without listening to every call: a few seconds after a call with an agent ends, AI scores it against your rubric and writes a scorecard: a score out of 100, pass or fail, a score for each rubric item with the quotes from the transcript it rests on, a compliance verdict, the call’s outcome and coaching tips. Insights sum up the scored calls of a period: by agent, by week, and which behaviour wins.
Over the API you keep the rubric in your own code, send scorecards to your coaching or HR tools, let supervisors’ corrections flow back, and feed the insights into your BI.
The rubric
Section titled “The rubric”The rubric is what every call is checked against. Each item is scored from 0 to 5 (or not applicable), and the call’s score is the weighted average of the applicable items on a 0–100 scale:
# Your organisation's rubric: three items on one 0–100 weight scale, a critical disclosure, pass at 75.curl -X PUT https://api.morevoice.ai/v1/qa/rubric \ -H "Authorization: Bearer $MOREVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Service calls", "pass_threshold": 75, "items": [ { "key": "disclosure", "label": "Recording notice", "description": "Says the call is recorded before asking for details.", "weight": 20, "type": "boolean", "critical": true }, { "key": "discovery", "label": "Understands the problem", "description": "Asks open questions and confirms the issue before solving it.", "weight": 50 }, { "key": "next_step", "label": "Clear next step", "description": "Ends with what happens next and when.", "weight": 30 } ] }'import MoreVoice from "@morevoice/sdk";
const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
// Your organisation's rubric: three items on one 0–100 weight scale, a critical disclosure, pass at 75.const rubric = await mv.qa.updateRubric({ name: "Service calls", pass_threshold: 75, items: [ { key: "disclosure", label: "Recording notice", description: "Says the call is recorded before asking for details.", weight: 20, type: "boolean", critical: true }, { key: "discovery", label: "Understands the problem", description: "Asks open questions and confirms the issue before solving it.", weight: 50 }, { key: "next_step", label: "Clear next step", description: "Ends with what happens next and when.", weight: 30 }, ],});console.log(rubric.name, rubric.items.length, "items, pass at", rubric.pass_threshold);import os
import requests
API = "https://api.morevoice.ai/v1"HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
# Your organisation's rubric: three items on one 0–100 weight scale, a critical disclosure, pass at 75.response = requests.put( f"{API}/qa/rubric", headers=HEADERS, json={ "name": "Service calls", "pass_threshold": 75, "items": [ {"key": "disclosure", "label": "Recording notice", "description": "Says the call is recorded before asking for details.", "weight": 20, "type": "boolean", "critical": True}, {"key": "discovery", "label": "Understands the problem", "description": "Asks open questions and confirms the issue before solving it.", "weight": 50}, {"key": "next_step", "label": "Clear next step", "description": "Ends with what happens next and when.", "weight": 30}, ], }, timeout=30,)response.raise_for_status()rubric = response.json()print(rubric["name"], len(rubric["items"]), "items, pass at", rubric["pass_threshold"])| Item field | What it does |
|---|---|
key |
Your stable name for the item (letters, digits, - and _). Overrides and scorecards refer to it. |
label, description |
What is checked, and what a good answer looks like: the scorer reads the description, so write it as you would for a new supervisor. |
weight |
The item’s importance, on one 0–100 scale. Only the ratios count: 60 and 40 weigh like 3 and 2. |
type |
scale (0–5) or boolean (done: 5, or not: 0). |
critical |
A compliance item: scoring 2 or less fails the call, whatever the total. |
The call passes at pass_threshold (0–100) or above, with no critical item failed. PUT /v1/qa/rubric replaces the whole rubric; calls scored from then on use it, and existing scorecards keep the rubric they were scored with. Until you save one, GET /v1/qa/rubric returns the built-in rubric (builtin: true).
Calls with a copilot profile that has its own qa_rubric are scored against that one instead, so a sales queue and a service queue can be judged differently. The scorecard’s rubric_source says which rubric applied.
Scorecards
Section titled “Scorecards”Calls handled by agents are scored automatically once they end, as your QA policy (in the app, under the supervisor’s Policy & QA) sets it. A call that is too short, or has no transcript, isn’t scored automatically: its scorecard is skipped, and error says why. Read a call’s scorecard when qa.scored arrives:
# A call's scorecard: the score, pass or fail, and each item with the quotes it rests on.curl https://api.morevoice.ai/v1/calls/$CALL_ID/scorecard \ -H "Authorization: Bearer $MOREVOICE_API_KEY"import MoreVoice from "@morevoice/sdk";
const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
// A call's scorecard: the score, pass or fail, and each item with the quotes it rests on.const card = await mv.qa.retrieveScorecard(process.env.CALL_ID!);console.log(card.status, card.score, card.passed ? "passed" : "failed", card.critical_failed);for (const item of card.items) { const quote = item.evidence[0]; console.log(`${item.label}: ${item.score ?? "n/a"}/5`, quote ? `“${quote.quote}” (${quote.speaker}, line ${quote.line})` : "");}import os
import requests
API = "https://api.morevoice.ai/v1"HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
# A call's scorecard: the score, pass or fail, and each item with the quotes it rests on.response = requests.get(f"{API}/calls/{os.environ['CALL_ID']}/scorecard", headers=HEADERS, timeout=30)response.raise_for_status()card = response.json()print(card["status"], card["score"], "passed" if card["passed"] else "failed", card["critical_failed"])for item in card["items"]: quote = item["evidence"][0] if item["evidence"] else None where = f"“{quote['quote']}” ({quote['speaker']}, line {quote['line']})" if quote else "" print(f"{item['label']}: {item['score']}/5", where)| Field | What it holds |
|---|---|
status |
pending, running, done, skipped (too short to score) or error (scoring failed). |
score, passed, critical_failed |
The 0–100 score after any overrides, pass or fail, and the keys of the critical items that failed the call. |
model_score |
The score as the model gave it, before any overrides. |
items |
One per rubric item: score (0–5, or null for not applicable), reasoning, and evidence: quotes, each verified against the transcript, with the speaker, the transcript line and the offset in the recording. |
compliance |
A pass, warn or fail verdict, with the issues found and their quotes. |
disposition, summary, customer_sentiment |
How the call ended for the business (sale, appointment, resolved, not_interested…), in a few sentences, and how the customer felt. |
strengths, coaching, next_steps, objections |
What went well, tips with better wording, follow-ups, and each objection with whether it was handled and how. |
GET answers 404 until the call has a scorecard.
Score a call now
Section titled “Score a call now”POST /v1/calls/{id}/scorecard/run scores a call on demand, even one too short for automatic scoring, and answers the scorecard (it can take a few seconds). A call that already has a finished scorecard is returned as it is unless you send force: true, which scores it again against today’s rubric. A call still in progress answers 409 call_in_progress.
Correct a score
Section titled “Correct a score”A supervisor who disagrees with an item sets its score, with a note. The call’s score and pass are recomputed, the change is kept in the scorecard’s history, and qa.overridden is sent:
# A supervisor corrects one item; the call's score and pass are recomputed.curl -X PATCH https://api.morevoice.ai/v1/calls/$CALL_ID/scorecard/items/discovery \ -H "Authorization: Bearer $MOREVOICE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "score": 4, "note": "Also confirmed the policy number before solving." }'import MoreVoice from "@morevoice/sdk";
const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
// A supervisor corrects one item; the call's score and pass are recomputed.const card = await mv.qa.overrideScorecardItem(process.env.CALL_ID!, "discovery", { score: 4, note: "Also confirmed the policy number before solving.",});console.log(`score ${card.model_score} → ${card.score}`, card.passed ? "passed" : "failed");import os
import requests
API = "https://api.morevoice.ai/v1"HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
# A supervisor corrects one item; the call's score and pass are recomputed.response = requests.patch( f"{API}/calls/{os.environ['CALL_ID']}/scorecard/items/discovery", headers=HEADERS, json={"score": 4, "note": "Also confirmed the policy number before solving."}, timeout=30,)response.raise_for_status()card = response.json()print(f"score {card['model_score']} → {card['score']}", "passed" if card["passed"] else "failed")Send score: null to mark the item not applicable, or clear: true to remove the override so the model’s score applies again. The item keeps both: score after the override, model_score as the model gave it, and override with who changed it and when. Only a finished scorecard (done) can be corrected; any other answers 409.
Insights
Section titled “Insights”GET /v1/insights sums up the scored calls of the last days days (1–365, 30 by default), for the whole organisation or one agent (agent_id):
# The last 30 days of scored calls: KPIs, the objections that cost deals and the answers that win, per-agent trends.curl "https://api.morevoice.ai/v1/insights?days=30" \ -H "Authorization: Bearer $MOREVOICE_API_KEY"import MoreVoice from "@morevoice/sdk";
const mv = new MoreVoice(); // reads MOREVOICE_API_KEY
// The last 30 days of scored calls: KPIs, the objections that cost deals and the answers that win, per-agent trends.const insights = await mv.insights.retrieve({ days: 30 });console.log(`${insights.kpis.analyzed} calls, average ${insights.kpis.average_score}, pass rate ${insights.kpis.pass_rate}`);for (const o of insights.top_objections) console.log(o.label, `handled ${o.handled_rate}`, `best answer: ${o.best_response ?? "—"}`);for (const a of insights.agents) console.log(a.name, a.calls, a.average_score, a.compliance_fails);import os
import requests
API = "https://api.morevoice.ai/v1"HEADERS = {"Authorization": f"Bearer {os.environ['MOREVOICE_API_KEY']}"}
# The last 30 days of scored calls: KPIs, the objections that cost deals and the answers that win, per-agent trends.response = requests.get(f"{API}/insights", headers=HEADERS, params={"days": 30}, timeout=30)response.raise_for_status()insights = response.json()kpis = insights["kpis"]print(f"{kpis['analyzed']} calls, average {kpis['average_score']}, pass rate {kpis['pass_rate']}")for o in insights["top_objections"]: print(o["label"], f"handled {o['handled_rate']}", f"best answer: {o['best_response'] or '—'}")for a in insights["agents"]: print(a["name"], a["calls"], a["average_score"], a["compliance_fails"])kpis: calls analysed, average score, pass rate, compliance-fail rate, win rate (calls with a positive outcome, such as a sale or an appointment) and average duration.top_objections: what customers object to most, how often agents handled it, the win rate when they did and when they didn’t, and the answer that won most often (best_response).behaviour: win rate and score by the agent’s share of talk time, by the number of questions asked, and by how much of the checklist they covered.agents: each agent’s calls, average score, pass rate, win rate and compliance fails, with a weekly trend.copilot: how often agents used the copilot’s cards, and their thumbs up and down.
The help centre explains the same screens to supervisors: scorecards, the rubric and insights.