Appearance
Qm
Generated from
DialerDigital/corecommit0fc18008b3c8— specopenapi/dialer-v1.yamlv0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Quality-management scoring forms (ADR #217), versioned append-only per tenant. Writes need the qm_enabled flag.
Endpoints
The caller's OWN QM evaluations, newest first
http
GET /v1/me/evaluationsThe evaluations of the seat the BEARER identifies, corrections included, capped at 200. No parameter may name an agent: agent_id, seat_id, user_id and sip_extension are refused by name with 400, never ignored, so a client can never believe it read a colleague's evaluations. The check comes AFTER the 403, so a caller with no seat learns nothing about which parameters are read.
Who gets a seat: a ddu_ dashboard login linked to an active seat (403 seat_not_linked otherwise). A dd_ machine key has no person behind it and is 403. A ddw_ widget token is 403 before the handler: this route is not on the widget allowlist.
Each row carries the criteria answers, the score, the form version, the comment written for the agent, whether it is the current one, the state of the dispute (dispute, null when there is none) and dispute_until, the end of the qm_dispute_window_days window while the dispute can still be opened (null otherwise: replaced evaluation, dispute already open or resolved, fruit of a resolved dispute, window closed). The row does NOT say who evaluated, and it never carries the agent's own id. Reads work whether or not qm_enabled is on.
Responses
200
The caller's evaluations.
| Field | Type | Required | Description |
|---|---|---|---|
evaluations | array of QmAgentEvaluation | yes | |
evaluations[].id | string (uuid) | yes | |
evaluations[].recording_id | string (uuid) | yes | |
evaluations[].call_attempt_id | string (uuid) | yes | |
evaluations[].campaign_id | string,null (uuid) | no | |
evaluations[].form_id | string (uuid) | yes | |
evaluations[].form_version | integer | yes | |
evaluations[].answers | object | yes | |
evaluations[].score_bp | integer | yes | Basis points: 8750 is 87.5 %. |
evaluations[].auto_failed | boolean | yes | A critical criterion was answered no; the score is 0. |
evaluations[].passed | boolean | yes | |
evaluations[].comment | string,null | yes | The evaluator's free text; the evaluated agent can read it. |
evaluations[].supersedes_id | string,null (uuid) | no | |
evaluations[].current | boolean | yes | No other evaluation replaces this one. |
evaluations[].dispute | QmAgentDispute | null | yes | |
evaluations[].dispute_until | string,null (date-time) | yes | The end of the dispute window while the dispute can still be opened; null otherwise. |
evaluations[].created_at | string (date-time) | yes |
400
A parameter that names an agent (agent_id, seat_id, user_id, sip_extension), refused by name.
json
{
"error": {
"code": "bad_request",
"message": "agent_id not accepted here: these evaluations are always the caller's own, derived from the token"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}One of the caller's OWN QM evaluations, with its criteria and dispute thread
http
GET /v1/me/evaluations/{id}Same identity contract as GET /v1/me/evaluations: the seat comes from the token and the parameters that name an agent are 400. Adds the criteria and pass_threshold of the form version the evaluation was scored against, and the dispute_events thread (kind, reason, note, time; no actor ids). An evaluation of another agent, of another tenant or that does not exist is the same 404.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The evaluation.
| Field | Type | Required | Description |
|---|---|---|---|
evaluation | any | yes | A QmAgentEvaluation plus the criteria it was scored against and its dispute thread. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Open the dispute of one of the caller's OWN QM evaluations
http
POST /v1/me/evaluations/{id}/disputeThe evaluated agent contests the evaluation: ONE dispute per evaluation, opened inside the qm_dispute_window_days window (1 to 90 days, 7 by default) counted from when the evaluation was written. Only the ddu_ login of the agent the evaluation is about can open it, with an ACTIVE seat. A dd_ key is a service, not a person: 403 role_forbidden. A ddw_ widget token is 403 before the handler: this route is not on the widget allowlist.
The checks run in this order and the first one that fails writes nothing: tenant scope, qm_enabled (403 qm_not_enabled: reads continue with the flag off, writes do not), the person (403 role_forbidden), 404 if the id is not a UUID, 400 (the body: reason is one of wrong_call, criterion_misjudged, other, note is optional text, unknown fields are refused), 404 (the evaluation is not the caller's own, belongs to another tenant or does not exist: one answer for all three), then 409 when the evaluation was replaced by a newer one (dispute the current), already has its dispute or is itself the result of a resolved dispute (a resolved dispute is not reopened), or the window has closed.
Answers with the evaluation as GET /v1/me/evaluations/{id} shows it, now with its dispute and the opened event in dispute_events. While the dispute is open the evaluation keeps counting: nothing hides or excludes it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
reason | wrong_call · criterion_misjudged · other | yes | |
note | string | no | Optional free text; blank reads as absent. |
json
{
"reason": "criterion_misjudged",
"note": "Dije el aviso de grabacion en el minuto 1:10 y el criterio lo marca como omitido."
}Responses
201
The evaluation with its dispute open.
| Field | Type | Required | Description |
|---|---|---|---|
evaluation | any | yes | A QmAgentEvaluation plus the criteria it was scored against and its dispute thread. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
code qm_not_enabled: the tenant's qm_enabled flag is off. coderole_forbidden: the caller is a dd_ key or a login with no active agent seat. code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "qm_not_enabled",
"message": "QM is not enabled for this tenant"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}The tenant's QM scoring forms, each with its current version
http
GET /v1/qm/formsBy name, the live ones; ?include_archived=true adds the archived. Each form comes with its CURRENT version. Capped at 1 000. Reads work whether or not qm_enabled is on.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
include_archived | query | boolean | no |
Responses
200
The forms.
| Field | Type | Required | Description |
|---|---|---|---|
forms | array of QmForm | yes | |
forms[].id | string (uuid) | yes | |
forms[].tenant_id | string (uuid) | no | |
forms[].name | string | yes | |
forms[].archived | boolean | yes | |
forms[].version | integer | yes | The current version. |
forms[].criteria | array of QmCriterion | yes | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
forms[].criteria[].key | string | yes | |
forms[].criteria[].label | string | yes | Trimmed, not blank. |
forms[].criteria[].kind | yes_no · scale | yes | |
forms[].criteria[].weight | integer | yes | |
forms[].criteria[].max | integer | no | Required for scale, refused for yes_no. |
forms[].criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
forms[].criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
forms[].pass_threshold | integer | yes | |
forms[].version_created_at | string (date-time) | no | |
forms[].created_at | string (date-time) | no | |
forms[].updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}Create a QM form (version 1)
http
POST /v1/qm/formsA form is an ORDERED list of criteria plus the pass threshold (0 to 100). name is trimmed, not blank, at most 200 characters and unique among the tenant's LIVE forms (409). A tenant holds at most 200 live forms (422). The body is checked as a whole: the first criterion that does not fit is named by its position in the 400 message. Unknown fields are refused.
Writes need the qm_enabled flag (403 qm_not_enabled while it is off) and a PERSON: a dashboard login with the account_admin role. A dd_ API key, a member login or a widget token is 403 role_forbidden.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
criteria | array of QmCriterion | yes | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
criteria[].key | string | yes | |
criteria[].label | string | yes | Trimmed, not blank. |
criteria[].kind | yes_no · scale | yes | |
criteria[].weight | integer | yes | |
criteria[].max | integer | no | Required for scale, refused for yes_no. |
criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
pass_threshold | integer | yes |
json
{
"name": "Cobranza — llamada saliente",
"pass_threshold": 80,
"criteria": [
{
"key": "identificacion",
"label": "Identifica a la empresa y al agente",
"kind": "yes_no",
"weight": 2
},
{
"key": "mini_miranda",
"label": "Lee el aviso legal completo",
"kind": "yes_no",
"weight": 5,
"critical": true
},
{
"key": "empatia",
"label": "Empatía con el deudor",
"kind": "scale",
"max": 5,
"weight": 3,
"na_allowed": true
}
]
}Responses
201
The form, at version 1.
| Field | Type | Required | Description |
|---|---|---|---|
form | object (QmForm) | yes | A form: its header and its CURRENT version. |
form.id | string (uuid) | yes | |
form.tenant_id | string (uuid) | no | |
form.name | string | yes | |
form.archived | boolean | yes | |
form.version | integer | yes | The current version. |
form.criteria | array of QmCriterion | yes | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
form.criteria[].key | string | yes | |
form.criteria[].label | string | yes | Trimmed, not blank. |
form.criteria[].kind | yes_no · scale | yes | |
form.criteria[].weight | integer | yes | |
form.criteria[].max | integer | no | Required for scale, refused for yes_no. |
form.criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
form.criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
form.pass_threshold | integer | yes | |
form.version_created_at | string (date-time) | no | |
form.created_at | string (date-time) | no | |
form.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — QmWriteForbidden
A QM write was refused. code qm_not_enabled: the tenant's qm_enabled flag is off (it is off by default; a flag that cannot be read counts as off). code role_forbidden: the caller is not a dashboard login with the account_admin role (a dd_ key, a member). code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "qm_not_enabled",
"message": "QM is not enabled for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}One QM form, at its current version
http
GET /v1/qm/forms/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The form (archived ones too, flagged archived).
| Field | Type | Required | Description |
|---|---|---|---|
form | object (QmForm) | yes | A form: its header and its CURRENT version. |
form.id | string (uuid) | yes | |
form.tenant_id | string (uuid) | no | |
form.name | string | yes | |
form.archived | boolean | yes | |
form.version | integer | yes | The current version. |
form.criteria | array of QmCriterion | yes | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
form.criteria[].key | string | yes | |
form.criteria[].label | string | yes | Trimmed, not blank. |
form.criteria[].kind | yes_no · scale | yes | |
form.criteria[].weight | integer | yes | |
form.criteria[].max | integer | no | Required for scale, refused for yes_no. |
form.criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
form.criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
form.pass_threshold | integer | yes | |
form.version_created_at | string (date-time) | no | |
form.created_at | string (date-time) | no | |
form.updated_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Edit a QM form into a NEW version
http
PATCH /v1/qm/forms/{id}Send at least one of criteria, pass_threshold; the one omitted is carried over from the current version. This appends version n+1 and leaves version n exactly as it was: an evaluation may have cited it. An edit that changes nothing writes no version and returns the current one. base_version is optional and, when sent, must be the current version — otherwise 409 and nothing is written, so two people editing the same form do not overwrite each other unnoticed. An archived form cannot be edited (409); a form holds at most 1 000 versions (422). Needs qm_enabled and an account_admin login.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
criteria | array of QmCriterion | no | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
criteria[].key | string | yes | |
criteria[].label | string | yes | Trimmed, not blank. |
criteria[].kind | yes_no · scale | yes | |
criteria[].weight | integer | yes | |
criteria[].max | integer | no | Required for scale, refused for yes_no. |
criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
pass_threshold | integer | no | |
base_version | integer | no |
json
{
"base_version": 1,
"pass_threshold": 85
}Responses
200
The form, at its current version after the edit.
| Field | Type | Required | Description |
|---|---|---|---|
form | object (QmForm) | yes | A form: its header and its CURRENT version. |
form.id | string (uuid) | yes | |
form.tenant_id | string (uuid) | no | |
form.name | string | yes | |
form.archived | boolean | yes | |
form.version | integer | yes | The current version. |
form.criteria | array of QmCriterion | yes | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
form.criteria[].key | string | yes | |
form.criteria[].label | string | yes | Trimmed, not blank. |
form.criteria[].kind | yes_no · scale | yes | |
form.criteria[].weight | integer | yes | |
form.criteria[].max | integer | no | Required for scale, refused for yes_no. |
form.criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
form.criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
form.pass_threshold | integer | yes | |
form.version_created_at | string (date-time) | no | |
form.created_at | string (date-time) | no | |
form.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — QmWriteForbidden
A QM write was refused. code qm_not_enabled: the tenant's qm_enabled flag is off (it is off by default; a flag that cannot be read counts as off). code role_forbidden: the caller is not a dashboard login with the account_admin role (a dd_ key, a member). code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "qm_not_enabled",
"message": "QM is not enabled for this tenant"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}Archive a QM form
http
DELETE /v1/qm/forms/{id}Forms are never deleted: an evaluation may have cited one of their versions. Archiving frees the name for a new form and takes the form out of the default list; it can no longer be edited. Idempotent. Needs qm_enabled and an account_admin login, like the other writes.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
204
The form is archived.
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — QmWriteForbidden
A QM write was refused. code qm_not_enabled: the tenant's qm_enabled flag is off (it is off by default; a flag that cannot be read counts as off). code role_forbidden: the caller is not a dashboard login with the account_admin role (a dd_ key, a member). code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "qm_not_enabled",
"message": "QM is not enabled for this tenant"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Every version of a QM form, oldest first
http
GET /v1/qm/forms/{id}/versionsParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The versions (capped at 1 000).
| Field | Type | Required | Description |
|---|---|---|---|
versions | array of QmFormVersion | yes | |
versions[].form_id | string (uuid) | yes | |
versions[].version | integer | yes | |
versions[].criteria | array of QmCriterion | yes | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
versions[].criteria[].key | string | yes | |
versions[].criteria[].label | string | yes | Trimmed, not blank. |
versions[].criteria[].kind | yes_no · scale | yes | |
versions[].criteria[].weight | integer | yes | |
versions[].criteria[].max | integer | no | Required for scale, refused for yes_no. |
versions[].criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
versions[].criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
versions[].pass_threshold | integer | yes | |
versions[].created_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}One version of a QM form
http
GET /v1/qm/forms/{id}/versions/{version}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
version | path | integer | yes | The version number (>= 1). Unknown, malformed or another tenant's reads as 404. |
Responses
200
The version, exactly as it was written.
| Field | Type | Required | Description |
|---|---|---|---|
version | object (QmFormVersion) | yes | One immutable version of a form. |
version.form_id | string (uuid) | yes | |
version.version | integer | yes | |
version.criteria | array of QmCriterion | yes | ORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points. |
version.criteria[].key | string | yes | |
version.criteria[].label | string | yes | Trimmed, not blank. |
version.criteria[].kind | yes_no · scale | yes | |
version.criteria[].weight | integer | yes | |
version.criteria[].max | integer | no | Required for scale, refused for yes_no. |
version.criteria[].critical | boolean | no | yes_no only: a failed critical criterion fails the whole evaluation. |
version.criteria[].na_allowed | boolean | no | The evaluator may mark the criterion not applicable. |
version.pass_threshold | integer | yes | |
version.created_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}The QM evaluations of a recording, newest first
http
GET /v1/recordings/{id}/evaluationsEvery evaluation written for the recording, corrections included: each row says whether it is the current one (no other row replaces it) and which row it supersedes_id. Capped at 200. Reads work whether or not qm_enabled is on. A dashboard login that may not evaluate (an agent seat, a member) is 403 role_forbidden: the rows say who evaluated. A dd_ key reads. The comment is only present for an eligible evaluator (a supervisor or admin seat, or an account_admin login without a seat); for anyone else the key is absent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The evaluations.
| Field | Type | Required | Description |
|---|---|---|---|
evaluations | array of QmEvaluation | yes | |
evaluations[].id | string (uuid) | yes | |
evaluations[].recording_id | string (uuid) | yes | |
evaluations[].call_attempt_id | string (uuid) | yes | |
evaluations[].agent_id | string (uuid) | yes | |
evaluations[].campaign_id | string,null (uuid) | no | |
evaluations[].form_id | string (uuid) | yes | |
evaluations[].form_version | integer | yes | |
evaluations[].evaluator_user_id | string,null (uuid) | yes | The signing user; null when evaluator_kind is automatic. |
evaluations[].evaluator_kind | human · automatic | yes | |
evaluations[].answers | object | yes | |
evaluations[].score_bp | integer | yes | Basis points: 8750 is 87.5 %. |
evaluations[].auto_failed | boolean | yes | A critical criterion was answered no; the score is 0. |
evaluations[].passed | boolean | yes | |
evaluations[].supersedes_id | string,null (uuid) | no | |
evaluations[].current | boolean | yes | No other evaluation replaces this one. |
evaluations[].comment | string,null | no | Present only for an eligible evaluator. |
evaluations[].created_at | string (date-time) | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Evaluate a recording with a QM form version
http
POST /v1/recordings/{id}/evaluationsA supervisor scores ONE available recording against a version of a live form and the row is frozen: it is never edited. To correct it, send the same body with supersedes_id set to the CURRENT evaluation of that recording and form; that appends a new row and leaves the old one as it was. Without supersedes_id a recording holds one evaluation per form (409 once there is one).
The score is computed here, in basis points (8750 is 87.5 %), from the version you cite and your answers: yes_no takes true or false, scale an integer from 0 to max, and "na" where the criterion allows it. An answer is required for every criterion. agent_id, call_attempt_id and campaign_id come from the recording, never from the body. Unknown fields are refused.
The checks run in this order and the first one that fails writes nothing: qm_enabled (403 qm_not_enabled), the person (403 role_forbidden), 400 (body shape), 404 (recording), 403 self_evaluation_forbidden (the recording is the evaluator's own), 409 (recording not available), 422 (no agent, AI leg, unknown form or version, answers that do not fit), 409 (archived form, or the chain). A dd_ API key is a service, not a person: 403 role_forbidden.
A correction is refused while the evaluation it corrects has an OPEN dispute (the agent opened it and nobody resolved it): 409 with error.codeevaluation_under_dispute, nothing is written, and the evaluation keeps counting in the QM reports with its open dispute. That evaluation is corrected only by resolving the dispute as adjusted. Once the dispute is resolved upheld, a correction is accepted again.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
form_id | string (uuid) | yes | A live form of the tenant. |
form_version | integer | yes | The version the answers are scored against. |
answers | object | yes | criterion key -> answer. true/false for yes_no, an integer from 0 to max for scale, the string na where na_allowed. One entry per criterion. |
comment | string | no | Optional free text; blank reads as absent. |
supersedes_id | string (uuid) | no | The CURRENT evaluation of this recording and form, to correct it. |
json
{
"form_id": "5d3b0c8e-4f3a-4a0e-9d1c-0a5f9a9b7e11",
"form_version": 1,
"answers": {
"identificacion": true,
"mini_miranda": true,
"empatia": 4
},
"comment": "Buena apertura; falto cerrar con el aviso de grabacion."
}Responses
201
The evaluation, with its score and as the current one.
| Field | Type | Required | Description |
|---|---|---|---|
evaluation | object (QmEvaluation) | yes | One frozen QM evaluation. Corrections are other rows that point here through supersedes_id. evaluator_kind is human (a person signed it, evaluator_user_id is that user) or automatic (the post-call AI pass over the form's criteria: nobody signed it and evaluator_user_id is null). The score is computed by core in both cases, never by the model. A human evaluation of the same recording and form replaces an automatic one through the same supersedes_id chain. |
evaluation.id | string (uuid) | yes | |
evaluation.recording_id | string (uuid) | yes | |
evaluation.call_attempt_id | string (uuid) | yes | |
evaluation.agent_id | string (uuid) | yes | |
evaluation.campaign_id | string,null (uuid) | no | |
evaluation.form_id | string (uuid) | yes | |
evaluation.form_version | integer | yes | |
evaluation.evaluator_user_id | string,null (uuid) | yes | The signing user; null when evaluator_kind is automatic. |
evaluation.evaluator_kind | human · automatic | yes | |
evaluation.answers | object | yes | |
evaluation.score_bp | integer | yes | Basis points: 8750 is 87.5 %. |
evaluation.auto_failed | boolean | yes | A critical criterion was answered no; the score is 0. |
evaluation.passed | boolean | yes | |
evaluation.supersedes_id | string,null (uuid) | no | |
evaluation.current | boolean | yes | No other evaluation replaces this one. |
evaluation.comment | string,null | no | Present only for an eligible evaluator. |
evaluation.created_at | string (date-time) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — QmEvaluateForbidden
Evaluating a recording was refused. code qm_not_enabled: the tenant's qm_enabled flag is off. code role_forbidden: the caller is not an eligible evaluator (a dd_ key, a member, a seat that is not supervisor or admin, an inactive seat). code self_evaluation_forbidden: the recording is the caller's own. code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "role_forbidden",
"message": "evaluating a recording takes a supervisor or admin seat, or an account_admin login"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}One QM evaluation of a recording
http
GET /v1/recordings/{id}/evaluations/{evaluation_id}The row exactly as it was written, plus whether it is still the current one. An id that belongs to another recording or another tenant reads as 404. The comment and the 403 for a login that may not evaluate follow the same rule as in the list.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
evaluation_id | path | string (uuid) | yes | The evaluation UUID. Malformed, unknown or another tenant's reads as 404. |
Responses
200
The evaluation.
| Field | Type | Required | Description |
|---|---|---|---|
evaluation | object (QmEvaluation) | yes | One frozen QM evaluation. Corrections are other rows that point here through supersedes_id. evaluator_kind is human (a person signed it, evaluator_user_id is that user) or automatic (the post-call AI pass over the form's criteria: nobody signed it and evaluator_user_id is null). The score is computed by core in both cases, never by the model. A human evaluation of the same recording and form replaces an automatic one through the same supersedes_id chain. |
evaluation.id | string (uuid) | yes | |
evaluation.recording_id | string (uuid) | yes | |
evaluation.call_attempt_id | string (uuid) | yes | |
evaluation.agent_id | string (uuid) | yes | |
evaluation.campaign_id | string,null (uuid) | no | |
evaluation.form_id | string (uuid) | yes | |
evaluation.form_version | integer | yes | |
evaluation.evaluator_user_id | string,null (uuid) | yes | The signing user; null when evaluator_kind is automatic. |
evaluation.evaluator_kind | human · automatic | yes | |
evaluation.answers | object | yes | |
evaluation.score_bp | integer | yes | Basis points: 8750 is 87.5 %. |
evaluation.auto_failed | boolean | yes | A critical criterion was answered no; the score is 0. |
evaluation.passed | boolean | yes | |
evaluation.supersedes_id | string,null (uuid) | no | |
evaluation.current | boolean | yes | No other evaluation replaces this one. |
evaluation.comment | string,null | no | Present only for an eligible evaluator. |
evaluation.created_at | string (date-time) | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Resolve the agent's dispute of an evaluation
http
POST /v1/qm/evaluations/{id}/dispute/resolutionAn eligible evaluator (a supervisor or admin seat, or an account_admin login without a seat) closes the dispute that the evaluated agent opened. The {id} is the DISPUTED evaluation. decision is upheld (the evaluation stands) or adjusted (it is replaced). The evaluator cannot be the agent of the evaluation. A dd_ key is a service, not a person: 403 role_forbidden.
adjusted takes answers (required, the same shape as when evaluating) and an optional comment for the new evaluation, which repeats the form and the version of the disputed one and is scored here from your answers. The new evaluation and the event that cites it (adjusted_evaluation_id) are written in ONE transaction: either both rows exist or neither. upheld takes neither answers nor comment. The new evaluation is read with GET /v1/recordings/{id}/evaluations/{evaluation_id}.
The checks run in this order and the first one that fails writes nothing: qm_enabled (403 qm_not_enabled), the person (403 role_forbidden), 404 (an id that is not a UUID), 400 (the body shape), 404 (evaluation), 403 self_evaluation_forbidden, 409 (the evaluation has no dispute, or it is already resolved), 422 (answers that do not fit the form version), 409 (the evaluation is no longer the current one).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
decision | upheld · adjusted | yes | |
note | string | no | Optional free text; blank reads as absent. |
answers | object | no | One answer per criterion of the form version of the disputed evaluation: a boolean for yes_no, an integer from 0 to max for scale, the string na where the criterion allows it. |
comment | string | no | Optional free text of the new evaluation. |
json
{
"decision": "adjusted",
"note": "Tiene razon: el aviso se dijo en el minuto 1:10.",
"answers": {
"identificacion": true,
"mini_miranda": true,
"empatia": 4
}
}Responses
201
The event the resolution appended to the dispute thread.
| Field | Type | Required | Description |
|---|---|---|---|
dispute_event | object (QmDisputeResolution) | yes | The event a resolution appended to the dispute thread. |
dispute_event.id | string (uuid) | yes | |
dispute_event.evaluation_id | string (uuid) | yes | The disputed evaluation. |
dispute_event.kind | upheld · adjusted | yes | |
dispute_event.note | string,null | yes | |
dispute_event.adjusted_evaluation_id | string,null (uuid) | yes | The evaluation that replaces the disputed one; null when upheld. |
dispute_event.created_at | string (date-time) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — QmEvaluateForbidden
Evaluating a recording was refused. code qm_not_enabled: the tenant's qm_enabled flag is off. code role_forbidden: the caller is not an eligible evaluator (a dd_ key, a member, a seat that is not supervisor or admin, an inactive seat). code self_evaluation_forbidden: the recording is the caller's own. code forbidden: the tenant is suspended or the token is a widget token.
json
{
"error": {
"code": "role_forbidden",
"message": "evaluating a recording takes a supervisor or admin seat, or an account_admin login"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}QM figures per agent and form over a window
http
GET /v1/qm/reports/agentsFigures derived on every call from the CURRENT evaluations (a correction replaces the row it supersedes; the old one no longer counts) whose recording started in [from, to). One row per agent, form AND evaluator_kind: two forms are two scales, and a person's score and the automatic pass are never averaged together. Per row: evaluations, avg_score_bp (mean score in basis points, rounded; null when there is nothing to average, never 0), pass_rate_bp (passed over evaluations in basis points, null likewise), passed and auto_failed (voided by a critical criterion). No comment, phone number or other free text is ever returned.
from and to are mandatory, from must be before to and the window may not exceed 31 days (400 otherwise). Without agent_id only agents with data appear; with an existing one that has no evaluation in the window the answer is ONE row with evaluations: 0 and the averages null; an unknown one, or another tenant's, is 404. Reads work whether or not qm_enabled is on. A dashboard login that may not evaluate (an agent seat, a member) is 403 role_forbidden; a dd_ key reads.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | yes | RFC 3339 lower bound on the recording start (inclusive). |
to | query | string (date-time) | yes | RFC 3339 upper bound on the recording start (exclusive); at most 31 days after from. |
agent_id | query | string (uuid) | no | Only this agent. |
form_id | query | string (uuid) | no | Only this form. |
evaluator_kind | query | human · automatic | no | Only human, or only automatic, evaluations. Without it both appear, as separate rows. |
Responses
200
The rows, ordered by agent, form then evaluator kind.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (date-time) | yes | |
to | string (date-time) | yes | |
rows | array of QmAgentReportRow | yes | |
rows[].agent_id | string (uuid) | yes | |
rows[].form_id | string,null (uuid) | yes | |
rows[].evaluator_kind | human · automatic · null | yes | Human and automatic evaluations are never averaged together: each has its own row. Null only on the empty row of an entity asked for without an evaluator_kind filter. |
rows[].evaluations | integer | yes | The current evaluations counted. |
rows[].avg_score_bp | integer,null | yes | Mean score in basis points, rounded; null when there is no evaluation. |
rows[].pass_rate_bp | integer,null | yes | Passed over evaluations in basis points; null when there is no evaluation. |
rows[].passed | integer | yes | |
rows[].auto_failed | integer | yes | Voided by a critical criterion. |
rows[].adjusted | integer | yes | Current evaluations that replaced a disputed one (the dispute was resolved as adjusted). |
rows[].open_disputes | integer | yes | Current evaluations with a dispute opened and not yet resolved. They keep counting in every other figure. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}QM figures per campaign and form over a window
http
GET /v1/qm/reports/campaignsFigures derived on every call from the CURRENT evaluations (a correction replaces the row it supersedes; the old one no longer counts) whose recording started in [from, to). One row per campaign, form AND evaluator_kind: two forms are two scales, and a person's score and the automatic pass are never averaged together. Per row: evaluations, avg_score_bp (mean score in basis points, rounded; null when there is nothing to average, never 0), pass_rate_bp (passed over evaluations in basis points, null likewise), passed and auto_failed (voided by a critical criterion). No comment, phone number or other free text is ever returned.
from and to are mandatory, from must be before to and the window may not exceed 31 days (400 otherwise). Without campaign_id only campaigns with data appear; with an existing one that has no evaluation in the window the answer is ONE row with evaluations: 0 and the averages null; an unknown one, or another tenant's, is 404. Reads work whether or not qm_enabled is on. A dashboard login that may not evaluate (an agent seat, a member) is 403 role_forbidden; a dd_ key reads.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | yes | RFC 3339 lower bound on the recording start (inclusive). |
to | query | string (date-time) | yes | RFC 3339 upper bound on the recording start (exclusive); at most 31 days after from. |
campaign_id | query | string (uuid) | no | Only this campaign. |
form_id | query | string (uuid) | no | Only this form. |
evaluator_kind | query | human · automatic | no | Only human, or only automatic, evaluations. Without it both appear, as separate rows. |
Responses
200
The rows, ordered by campaign, form then evaluator kind.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (date-time) | yes | |
to | string (date-time) | yes | |
rows | array of QmCampaignReportRow | yes | |
rows[].campaign_id | string,null (uuid) | yes | null for recordings without a campaign. |
rows[].form_id | string,null (uuid) | yes | |
rows[].evaluator_kind | human · automatic · null | yes | Human and automatic evaluations are never averaged together: each has its own row. Null only on the empty row of an entity asked for without an evaluator_kind filter. |
rows[].evaluations | integer | yes | The current evaluations counted. |
rows[].avg_score_bp | integer,null | yes | Mean score in basis points, rounded; null when there is no evaluation. |
rows[].pass_rate_bp | integer,null | yes | Passed over evaluations in basis points; null when there is no evaluation. |
rows[].passed | integer | yes | |
rows[].auto_failed | integer | yes | Voided by a critical criterion. |
rows[].adjusted | integer | yes | Current evaluations that replaced a disputed one (the dispute was resolved as adjusted). |
rows[].open_disputes | integer | yes | Current evaluations with a dispute opened and not yet resolved. They keep counting in every other figure. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}