Skip to content

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.

RouteScopeDoes
GET /api/public/v1/qa/rubricsqa:readA page of rubrics, newest first
POST /api/public/v1/qa/rubricsqa:writeCreates a rubric
GET /api/public/v1/qa/rubrics/{id}qa:readOne rubric
PATCH /api/public/v1/qa/rubrics/{id}qa:writeChanges its details, criteria, status, default
DELETE /api/public/v1/qa/rubrics/{id}qa:writeArchives it
PropertyValue
Org kindSeller organizations only
Cost1 token per request
BodyJSON, 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 ​

FieldTypeNullableExample
idstring (uuid)No"6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d"
namestringNo"Outbound sales"
descriptionstringYes"Every campaign selling cover"
ai_instructionsstringYes"Treat a missing disclosure as a fail."
statusstringNo"active"
is_defaultbooleanNotrue
criteriaarrayNosee below
created_atstring (ISO)No"2026-11-01T09:00:00.000Z"
updated_atstring (ISO)No"2026-11-02T09:00:00.000Z"
  • status is active or archived. Only an active rubric can score a call, be a campaign's rubric or be the default.
  • is_default marks 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_instructions is appended to the AI reviewer's prompt on every review against this rubric, so it costs input tokens each time.

Each criterion:

FieldTypeExample
idstring (uuid)"77777777-7777-4777-8777-777777777777"
namestring"Disclosure"
descriptionstring"Reads the recording notice before the pitch."
scalestring"binary"
weightnumber3
criticalbooleantrue
allow_nabooleanfalse
  • description is handed to the AI reviewer verbatim: it is what the criterion checks.
  • scale is binary (pass or fail) or scale_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.

ParameterMeaning
limitRows per page, 1–100, default 50.
cursorThe previous page's next_cursor.
statusactive 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
    }
  ]
}
FieldRequiredBounds
nameYes1–120 characters
descriptionNoUp to 500 characters
ai_instructionsNoUp to 2 000 characters
criteriaNo1–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.

FieldEffect
nameRenames it.
descriptionReplaces it; null clears it.
ai_instructionsReplaces it; null clears it.
criteriaReplaces the whole list, in the order sent. Send id to keep a criterion.
statusarchived archives it; active restores it.
is_defaultOnly 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 status the rubric already has is not an error, so a retried request answers 200.
  • Archiving clears the rubric from every campaign that named it, and drops its default flag; status: "archived" with is_default: true is a 400.
  • 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.