# QA: scorecards, the rubric and insights

> Every agent call is scored against your rubric a few seconds after it ends, with quotes from the call as evidence. Set the rubric, read and correct scorecards, score a call on demand, and pull conversation insights over the API.

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

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:

**cURL**

```sh
# 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 }
    ]
  }'
```

**Node.js**

```ts title="set-rubric.ts"
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);
```

**Python**

```python title="set_rubric.py"
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](https://docs.morevoice.ai/guides/copilot/) 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

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`](https://docs.morevoice.ai/webhooks/events/#qa.scored) arrives:

**cURL**

```sh
# 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"
```

**Node.js**

```ts title="get-scorecard.ts"
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})` : "");
}
```

**Python**

```python title="get_scorecard.py"
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

`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

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`](https://docs.morevoice.ai/webhooks/events/#qa.overridden) is sent:

**cURL**

```sh
# 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." }'
```

**Node.js**

```ts title="override-item.ts"
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");
```

**Python**

```python title="override_item.py"
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

`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`):

**cURL**

```sh
# 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"
```

**Node.js**

```ts title="insights.ts"
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);
```

**Python**

```python title="insights.py"
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](https://docs.morevoice.ai/guides/copilot/)'s cards, and their thumbs up and down.

> **Note:** Objections are grouped by meaning in live mode. Test keys group them by their words (`semantic_clustering: false`).

The help centre explains the same screens to supervisors: [scorecards](https://help.morevoice.ai/en/qa/scorecards/), [the rubric](https://help.morevoice.ai/en/qa/rubric/) and [insights](https://help.morevoice.ai/en/qa/insights/).
