QA rubrics
A rubric is what a good call looks like: an ordered list of weighted criteria that the AI reviewer and your managers both score calls against. These routes manage your organization's rubrics, the same ones Manage → QA edits; a change made here shows there at once, and the other way round.
| Route | Scope | Does |
|---|---|---|
GET /api/public/v1/qa/rubrics | qa:read | A page of rubrics, newest first |
POST /api/public/v1/qa/rubrics | qa:write | Creates a rubric |
GET /api/public/v1/qa/rubrics/{id} | qa:read | One rubric |
PATCH /api/public/v1/qa/rubrics/{id} | qa:write | Changes its details, criteria, status, default |
DELETE /api/public/v1/qa/rubrics/{id} | qa:write | Archives it |
| Property | Value |
|---|---|
| Org kind | Seller organizations only |
| Cost | 1 token per request |
| Body | JSON, at most 96 KiB, on POST and PATCH |
qa:read is in the default scope set; qa:write must be granted explicitly. Rubrics belong to the organization whose calls are reviewed — the seller side, as in the dashboard — so a buyer-only organization gets 403.
The rubric object
| Field | Type | Nullable | Example |
|---|---|---|---|
id | string (uuid) | No | "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d" |
name | string | No | "Outbound sales" |
description | string | Yes | "Every campaign selling cover" |
ai_instructions | string | Yes | "Treat a missing disclosure as a fail." |
status | string | No | "active" |
is_default | boolean | No | true |
criteria | array | No | see below |
created_at | string (ISO) | No | "2026-11-01T09:00:00.000Z" |
updated_at | string (ISO) | No | "2026-11-02T09:00:00.000Z" |
statusisactiveorarchived. Only an active rubric can score a call, be a campaign's rubric or be the default.is_defaultmarks the rubric a review uses when nobody named one and the call's campaign has none. At most one rubric is the default; your first rubric becomes it.ai_instructionsis appended to the AI reviewer's prompt on every review against this rubric, so it costs input tokens each time.
Each criterion:
| Field | Type | Example |
|---|---|---|
id | string (uuid) | "77777777-7777-4777-8777-777777777777" |
name | string | "Disclosure" |
description | string | "Reads the recording notice before the pitch." |
scale | string | "binary" |
weight | number | 3 |
critical | boolean | true |
allow_na | boolean | false |
descriptionis handed to the AI reviewer verbatim: it is what the criterion checks.scaleisbinary(pass or fail) orscale_5(1 to 5).weight, a whole number from 1 to 10, is the criterion's share of the score.critical: failing it fails the whole call, whatever the other scores.allow_na: the criterion may be marked not applicable, and is then left out of the score.
A criterion's id is its identity across every review. Scores are reported per criterion id, so a criterion sent back with its id keeps its history; one sent without an id is a new criterion, even if its name is unchanged.
The fields are ApiQaRubricSchema in src/lib/schema/api/api_qa_rubric.schema.ts; the bounds below are QA.LIMITS in src/lib/const/qa.const.ts.
GET /api/public/v1/qa/rubrics
Paged as every list is — see Pagination.
| Parameter | Meaning |
|---|---|
limit | Rows per page, 1–100, default 50. |
cursor | The previous page's next_cursor. |
status | active or archived; repeat it for both. Omitted lists every rubric. |
An organization holds at most 20 rubrics, archived ones included, so one page usually holds them all.
GET /api/public/v1/qa/rubrics/{id}
One rubric, archived or not. Another organization's id, or one that is not a rubric id at all, is a 404.
POST /api/public/v1/qa/rubrics
json
{
"name": "Outbound sales",
"description": "Every campaign selling cover",
"ai_instructions": "Treat a missing disclosure as a fail.",
"criteria": [
{
"name": "Disclosure",
"description": "Reads the recording notice before the pitch.",
"scale": "binary",
"weight": 3,
"critical": true
},
{
"name": "Rapport",
"description": "Uses the customer's name and listens before answering.",
"scale": "scale_5",
"weight": 1,
"allow_na": true
}
]
}| Field | Required | Bounds |
|---|---|---|
name | Yes | 1–120 characters |
description | No | Up to 500 characters |
ai_instructions | No | Up to 2 000 characters |
criteria | No | 1–25; each name up to 120, description up to 1 000 |
Leave out criteria to start from the starter set the dashboard starts from, then edit it with PATCH. Ids are always assigned by us: an id on a criterion you create is ignored.
201 with the rubric. A 409 CONFLICT means the organization already holds 20 rubrics; archive one first.
PATCH /api/public/v1/qa/rubrics/{id}
Send only what changes. Every field is optional, and at least one is required.
| Field | Effect |
|---|---|
name | Renames it. |
description | Replaces it; null clears it. |
ai_instructions | Replaces it; null clears it. |
criteria | Replaces the whole list, in the order sent. Send id to keep a criterion. |
status | archived archives it; active restores it. |
is_default | Only true: makes it the default, taking the flag from the rubric that had it. |
The change is one unit of work: if any part is refused — a criteria list out of bounds, another rubric made the default at the same moment — nothing is changed. 200 with the rubric as it now stands.
- An archived rubric takes no other change until it is restored. Send
"status": "active"in the same request and the rest applies after the restore. - A
statusthe rubric already has is not an error, so a retried request answers200. - Archiving clears the rubric from every campaign that named it, and drops its default flag;
status: "archived"withis_default: trueis a400. - There is no
is_default: false. A default is moved, never left empty: make another rubric the default instead.
DELETE /api/public/v1/qa/rubrics/{id}
Archives the rubric; it is never deleted, because every call already scored against it keeps pointing at it. 200 with the archived rubric, which PATCH with "status": "active" restores. Deleting an archived rubric answers 200 again and changes nothing.
Unknown fields
A POST or PATCH body with a field these pages do not name is a 400, not ignored — a misspelt descripton would otherwise answer 200 having changed nothing:
json
{
"error": {
"code": "INVALID_INPUT",
"message": "Unrecognized key: \"descripton\""
}
}Rate limits
Beside the per-key request budget, every rubric write spends one token from the organization's QA editing budget — 60 a minute (QA.WRITE_RATE_LIMIT), shared with the dashboard's editor. A spent budget is a 429 TOO_MANY_REQUESTS naming it.