Skip to content

Qm ​

Generated from DialerDigital/core commit 0fc18008b3c8 — spec openapi/dialer-v1.yaml v0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Español

Quality-management scoring forms (ADR #217), versioned append-only per tenant. Writes need the qm_enabled flag.

Endpoints ​

MethodPathSummary
GET/v1/me/evaluationsThe caller's OWN QM evaluations, newest first
GET/v1/me/evaluations/{id}One of the caller's OWN QM evaluations, with its criteria and dispute thread
POST/v1/me/evaluations/{id}/disputeOpen the dispute of one of the caller's OWN QM evaluations
GET/v1/qm/formsThe tenant's QM scoring forms, each with its current version
POST/v1/qm/formsCreate a QM form (version 1)
GET/v1/qm/forms/{id}One QM form, at its current version
PATCH/v1/qm/forms/{id}Edit a QM form into a NEW version
DELETE/v1/qm/forms/{id}Archive a QM form
GET/v1/qm/forms/{id}/versionsEvery version of a QM form, oldest first
GET/v1/qm/forms/{id}/versions/{version}One version of a QM form
GET/v1/recordings/{id}/evaluationsThe QM evaluations of a recording, newest first
POST/v1/recordings/{id}/evaluationsEvaluate a recording with a QM form version
GET/v1/recordings/{id}/evaluations/{evaluation_id}One QM evaluation of a recording
POST/v1/qm/evaluations/{id}/dispute/resolutionResolve the agent's dispute of an evaluation
GET/v1/qm/reports/agentsQM figures per agent and form over a window
GET/v1/qm/reports/campaignsQM figures per campaign and form over a window

The caller's OWN QM evaluations, newest first ​

http
GET /v1/me/evaluations

The 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.

FieldTypeRequiredDescription
evaluationsarray of QmAgentEvaluationyes
evaluations[].idstring (uuid)yes
evaluations[].recording_idstring (uuid)yes
evaluations[].call_attempt_idstring (uuid)yes
evaluations[].campaign_idstring,null (uuid)no
evaluations[].form_idstring (uuid)yes
evaluations[].form_versionintegeryes
evaluations[].answersobjectyes
evaluations[].score_bpintegeryesBasis points: 8750 is 87.5 %.
evaluations[].auto_failedbooleanyesA critical criterion was answered no; the score is 0.
evaluations[].passedbooleanyes
evaluations[].commentstring,nullyesThe evaluator's free text; the evaluated agent can read it.
evaluations[].supersedes_idstring,null (uuid)no
evaluations[].currentbooleanyesNo other evaluation replaces this one.
evaluations[].disputeQmAgentDispute | nullyes
evaluations[].dispute_untilstring,null (date-time)yesThe end of the dispute window while the dispute can still be opened; null otherwise.
evaluations[].created_atstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200 ​

The evaluation.

FieldTypeRequiredDescription
evaluationanyyesA 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}/dispute

The 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
reasonwrong_call · criterion_misjudged · otheryes
notestringnoOptional 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.

FieldTypeRequiredDescription
evaluationanyyesA 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/forms

By 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

NameInTypeRequiredDescription
include_archivedquerybooleanno

Responses

200 ​

The forms.

FieldTypeRequiredDescription
formsarray of QmFormyes
forms[].idstring (uuid)yes
forms[].tenant_idstring (uuid)no
forms[].namestringyes
forms[].archivedbooleanyes
forms[].versionintegeryesThe current version.
forms[].criteriaarray of QmCriterionyesORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
forms[].criteria[].keystringyes
forms[].criteria[].labelstringyesTrimmed, not blank.
forms[].criteria[].kindyes_no · scaleyes
forms[].criteria[].weightintegeryes
forms[].criteria[].maxintegernoRequired for scale, refused for yes_no.
forms[].criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
forms[].criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
forms[].pass_thresholdintegeryes
forms[].version_created_atstring (date-time)no
forms[].created_atstring (date-time)no
forms[].updated_atstring (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/forms

A 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)

FieldTypeRequiredDescription
namestringyes
criteriaarray of QmCriterionyesORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
criteria[].keystringyes
criteria[].labelstringyesTrimmed, not blank.
criteria[].kindyes_no · scaleyes
criteria[].weightintegeryes
criteria[].maxintegernoRequired for scale, refused for yes_no.
criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
pass_thresholdintegeryes
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.

FieldTypeRequiredDescription
formobject (QmForm)yesA form: its header and its CURRENT version.
form.idstring (uuid)yes
form.tenant_idstring (uuid)no
form.namestringyes
form.archivedbooleanyes
form.versionintegeryesThe current version.
form.criteriaarray of QmCriterionyesORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
form.criteria[].keystringyes
form.criteria[].labelstringyesTrimmed, not blank.
form.criteria[].kindyes_no · scaleyes
form.criteria[].weightintegeryes
form.criteria[].maxintegernoRequired for scale, refused for yes_no.
form.criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
form.criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
form.pass_thresholdintegeryes
form.version_created_atstring (date-time)no
form.created_atstring (date-time)no
form.updated_atstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200 ​

The form (archived ones too, flagged archived).

FieldTypeRequiredDescription
formobject (QmForm)yesA form: its header and its CURRENT version.
form.idstring (uuid)yes
form.tenant_idstring (uuid)no
form.namestringyes
form.archivedbooleanyes
form.versionintegeryesThe current version.
form.criteriaarray of QmCriterionyesORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
form.criteria[].keystringyes
form.criteria[].labelstringyesTrimmed, not blank.
form.criteria[].kindyes_no · scaleyes
form.criteria[].weightintegeryes
form.criteria[].maxintegernoRequired for scale, refused for yes_no.
form.criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
form.criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
form.pass_thresholdintegeryes
form.version_created_atstring (date-time)no
form.created_atstring (date-time)no
form.updated_atstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
criteriaarray of QmCriterionnoORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
criteria[].keystringyes
criteria[].labelstringyesTrimmed, not blank.
criteria[].kindyes_no · scaleyes
criteria[].weightintegeryes
criteria[].maxintegernoRequired for scale, refused for yes_no.
criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
pass_thresholdintegerno
base_versionintegerno
json
{
  "base_version": 1,
  "pass_threshold": 85
}

Responses

200 ​

The form, at its current version after the edit.

FieldTypeRequiredDescription
formobject (QmForm)yesA form: its header and its CURRENT version.
form.idstring (uuid)yes
form.tenant_idstring (uuid)no
form.namestringyes
form.archivedbooleanyes
form.versionintegeryesThe current version.
form.criteriaarray of QmCriterionyesORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
form.criteria[].keystringyes
form.criteria[].labelstringyesTrimmed, not blank.
form.criteria[].kindyes_no · scaleyes
form.criteria[].weightintegeryes
form.criteria[].maxintegernoRequired for scale, refused for yes_no.
form.criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
form.criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
form.pass_thresholdintegeryes
form.version_created_atstring (date-time)no
form.created_atstring (date-time)no
form.updated_atstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource 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}/versions

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200 ​

The versions (capped at 1 000).

FieldTypeRequiredDescription
versionsarray of QmFormVersionyes
versions[].form_idstring (uuid)yes
versions[].versionintegeryes
versions[].criteriaarray of QmCriterionyesORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
versions[].criteria[].keystringyes
versions[].criteria[].labelstringyesTrimmed, not blank.
versions[].criteria[].kindyes_no · scaleyes
versions[].criteria[].weightintegeryes
versions[].criteria[].maxintegernoRequired for scale, refused for yes_no.
versions[].criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
versions[].criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
versions[].pass_thresholdintegeryes
versions[].created_atstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
versionpathintegeryesThe version number (>= 1). Unknown, malformed or another tenant's reads as 404.

Responses

200 ​

The version, exactly as it was written.

FieldTypeRequiredDescription
versionobject (QmFormVersion)yesOne immutable version of a form.
version.form_idstring (uuid)yes
version.versionintegeryes
version.criteriaarray of QmCriterionyesORDERED list of scoring criteria. key is unique within the form; the score of an evaluation is computed from these, in basis points.
version.criteria[].keystringyes
version.criteria[].labelstringyesTrimmed, not blank.
version.criteria[].kindyes_no · scaleyes
version.criteria[].weightintegeryes
version.criteria[].maxintegernoRequired for scale, refused for yes_no.
version.criteria[].criticalbooleannoyes_no only: a failed critical criterion fails the whole evaluation.
version.criteria[].na_allowedbooleannoThe evaluator may mark the criterion not applicable.
version.pass_thresholdintegeryes
version.created_atstring (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}/evaluations

Every 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200 ​

The evaluations.

FieldTypeRequiredDescription
evaluationsarray of QmEvaluationyes
evaluations[].idstring (uuid)yes
evaluations[].recording_idstring (uuid)yes
evaluations[].call_attempt_idstring (uuid)yes
evaluations[].agent_idstring (uuid)yes
evaluations[].campaign_idstring,null (uuid)no
evaluations[].form_idstring (uuid)yes
evaluations[].form_versionintegeryes
evaluations[].evaluator_user_idstring,null (uuid)yesThe signing user; null when evaluator_kind is automatic.
evaluations[].evaluator_kindhuman · automaticyes
evaluations[].answersobjectyes
evaluations[].score_bpintegeryesBasis points: 8750 is 87.5 %.
evaluations[].auto_failedbooleanyesA critical criterion was answered no; the score is 0.
evaluations[].passedbooleanyes
evaluations[].supersedes_idstring,null (uuid)no
evaluations[].currentbooleanyesNo other evaluation replaces this one.
evaluations[].commentstring,nullnoPresent only for an eligible evaluator.
evaluations[].created_atstring (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}/evaluations

A 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
form_idstring (uuid)yesA live form of the tenant.
form_versionintegeryesThe version the answers are scored against.
answersobjectyescriterion 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.
commentstringnoOptional free text; blank reads as absent.
supersedes_idstring (uuid)noThe 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.

FieldTypeRequiredDescription
evaluationobject (QmEvaluation)yesOne 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.idstring (uuid)yes
evaluation.recording_idstring (uuid)yes
evaluation.call_attempt_idstring (uuid)yes
evaluation.agent_idstring (uuid)yes
evaluation.campaign_idstring,null (uuid)no
evaluation.form_idstring (uuid)yes
evaluation.form_versionintegeryes
evaluation.evaluator_user_idstring,null (uuid)yesThe signing user; null when evaluator_kind is automatic.
evaluation.evaluator_kindhuman · automaticyes
evaluation.answersobjectyes
evaluation.score_bpintegeryesBasis points: 8750 is 87.5 %.
evaluation.auto_failedbooleanyesA critical criterion was answered no; the score is 0.
evaluation.passedbooleanyes
evaluation.supersedes_idstring,null (uuid)no
evaluation.currentbooleanyesNo other evaluation replaces this one.
evaluation.commentstring,nullnoPresent only for an eligible evaluator.
evaluation.created_atstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.
evaluation_idpathstring (uuid)yesThe evaluation UUID. Malformed, unknown or another tenant's reads as 404.

Responses

200 ​

The evaluation.

FieldTypeRequiredDescription
evaluationobject (QmEvaluation)yesOne 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.idstring (uuid)yes
evaluation.recording_idstring (uuid)yes
evaluation.call_attempt_idstring (uuid)yes
evaluation.agent_idstring (uuid)yes
evaluation.campaign_idstring,null (uuid)no
evaluation.form_idstring (uuid)yes
evaluation.form_versionintegeryes
evaluation.evaluator_user_idstring,null (uuid)yesThe signing user; null when evaluator_kind is automatic.
evaluation.evaluator_kindhuman · automaticyes
evaluation.answersobjectyes
evaluation.score_bpintegeryesBasis points: 8750 is 87.5 %.
evaluation.auto_failedbooleanyesA critical criterion was answered no; the score is 0.
evaluation.passedbooleanyes
evaluation.supersedes_idstring,null (uuid)no
evaluation.currentbooleanyesNo other evaluation replaces this one.
evaluation.commentstring,nullnoPresent only for an eligible evaluator.
evaluation.created_atstring (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/resolution

An 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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Request body (JSON, required)

FieldTypeRequiredDescription
decisionupheld · adjustedyes
notestringnoOptional free text; blank reads as absent.
answersobjectnoOne 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.
commentstringnoOptional 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.

FieldTypeRequiredDescription
dispute_eventobject (QmDisputeResolution)yesThe event a resolution appended to the dispute thread.
dispute_event.idstring (uuid)yes
dispute_event.evaluation_idstring (uuid)yesThe disputed evaluation.
dispute_event.kindupheld · adjustedyes
dispute_event.notestring,nullyes
dispute_event.adjusted_evaluation_idstring,null (uuid)yesThe evaluation that replaces the disputed one; null when upheld.
dispute_event.created_atstring (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/agents

Figures 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

NameInTypeRequiredDescription
fromquerystring (date-time)yesRFC 3339 lower bound on the recording start (inclusive).
toquerystring (date-time)yesRFC 3339 upper bound on the recording start (exclusive); at most 31 days after from.
agent_idquerystring (uuid)noOnly this agent.
form_idquerystring (uuid)noOnly this form.
evaluator_kindqueryhuman · automaticnoOnly human, or only automatic, evaluations. Without it both appear, as separate rows.

Responses

200 ​

The rows, ordered by agent, form then evaluator kind.

FieldTypeRequiredDescription
fromstring (date-time)yes
tostring (date-time)yes
rowsarray of QmAgentReportRowyes
rows[].agent_idstring (uuid)yes
rows[].form_idstring,null (uuid)yes
rows[].evaluator_kindhuman · automatic · nullyesHuman 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[].evaluationsintegeryesThe current evaluations counted.
rows[].avg_score_bpinteger,nullyesMean score in basis points, rounded; null when there is no evaluation.
rows[].pass_rate_bpinteger,nullyesPassed over evaluations in basis points; null when there is no evaluation.
rows[].passedintegeryes
rows[].auto_failedintegeryesVoided by a critical criterion.
rows[].adjustedintegeryesCurrent evaluations that replaced a disputed one (the dispute was resolved as adjusted).
rows[].open_disputesintegeryesCurrent 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/campaigns

Figures 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

NameInTypeRequiredDescription
fromquerystring (date-time)yesRFC 3339 lower bound on the recording start (inclusive).
toquerystring (date-time)yesRFC 3339 upper bound on the recording start (exclusive); at most 31 days after from.
campaign_idquerystring (uuid)noOnly this campaign.
form_idquerystring (uuid)noOnly this form.
evaluator_kindqueryhuman · automaticnoOnly human, or only automatic, evaluations. Without it both appear, as separate rows.

Responses

200 ​

The rows, ordered by campaign, form then evaluator kind.

FieldTypeRequiredDescription
fromstring (date-time)yes
tostring (date-time)yes
rowsarray of QmCampaignReportRowyes
rows[].campaign_idstring,null (uuid)yesnull for recordings without a campaign.
rows[].form_idstring,null (uuid)yes
rows[].evaluator_kindhuman · automatic · nullyesHuman 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[].evaluationsintegeryesThe current evaluations counted.
rows[].avg_score_bpinteger,nullyesMean score in basis points, rounded; null when there is no evaluation.
rows[].pass_rate_bpinteger,nullyesPassed over evaluations in basis points; null when there is no evaluation.
rows[].passedintegeryes
rows[].auto_failedintegeryesVoided by a critical criterion.
rows[].adjustedintegeryesCurrent evaluations that replaced a disputed one (the dispute was resolved as adjusted).
rows[].open_disputesintegeryesCurrent 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"
  }
}

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.