Skip to content

Academy ​

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

In-app Academy training records — the auditable constancia (who completed what and when), append-only and per agent.

Endpoints ​

MethodPathSummary
POST/v1/academy/completionsRecord ONE Academy milestone for an agent
GET/v1/academy/progressAn agent's DERIVED Academy state
GET/v1/academy/transcriptThe auditable training record — who completed what and when

Record ONE Academy milestone for an agent ​

http
POST /v1/academy/completions

Appends one completion event (a lesson finished, a badge earned) to the tenant's append-only training stream. The row is the record a customer audits, so nothing here can be rewritten later: the table has no UPDATE or DELETE grant and a trigger refuses both.

Idempotent. Completing the same item twice is not a second event — the FIRST record is returned unchanged, so occurred_at keeps saying when the agent actually got there.

occurred_at is NOT an input: the server stamps it. A training record whose instant is chosen by the browser of the person being audited is not evidence.

Who may name whom (ADR #134). A ddw_ agent token writes its OWN record only (agent_id is taken from the token; naming another agent is 403). A dd_ machine key MUST name the agent — in the tenant lane there is no "me", and defaulting would attribute training to whoever happened to call (400). A ddu_ login session linked to an agent seat writes as THAT seat (agent_id optional or its own; another agent is 403 forbidden); a ddu_ with NO linked seat MUST name the agent (400) and may name another only with users.role = account_admin — a member is 403 role_forbidden — once the tenant's catalog key ddu_author_role_enforced is on (born off: until then it keeps the dd_ contract).

item_id is format-checked, not enumerated: the curriculum's source of truth is the dashboard, so a new lesson must not need a core migration. Lowercase alphanumerics plus ._-, up to 64 characters.

Request body (JSON, required)

FieldTypeRequiredDescription
agent_idstring (uuid)noRequired for a dd_ key and for a seatless ddu_ session; for a ddw_ token, and for a ddu_ session linked to a seat, it may only be their own agent (and may be omitted).
kindlesson · badgeyes
item_idstringyesCurriculum item id, e.g. ch1.dashboard.
json
{
  "agent_id": "8ac7f2f9-1f2c-4f7a-9d3b-1f0a6c5d4e3b",
  "kind": "lesson",
  "item_id": "ch1.dashboard"
}

Responses

201 ​

The milestone as recorded (the FIRST record, if it already existed).

FieldTypeRequiredDescription
idstring (uuid)yes
agent_idstring (uuid)yes
kindlesson · badgeyes
item_idstringyes
occurred_atstring (date-time)yesStamped by the server, never by the caller.

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"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.

json
{
  "error": {
    "code": "rate_limited",
    "message": "API key request quota exceeded; retry after the interval in Retry-After"
  }
}

An agent's DERIVED Academy state ​

http
GET /v1/academy/progress

Folds the agent's completion stream into the state the Academy renders from. Chapter and level are deliberately NOT computed here: which lesson belongs to which chapter is the client's curriculum, and duplicating it server-side would make every new chapter a core deploy.

A ddw_ token reads its OWN progress (agent_id optional, and only its own is accepted); so does a ddu_ login session linked to an agent seat (another agent is 403 forbidden). A dd_ key MUST pass agent_id; so must a ddu_ with NO linked seat, and it may read another agent's only with users.role = account_admin — a member is 403 role_forbidden — once the tenant's catalog key ddu_author_role_enforced is on (born off: until then it keeps the dd_ contract; ADR #134). An agent of another tenant reads as an EMPTY document — RLS returns zero rows, the handler compares no ids.

Parameters

NameInTypeRequiredDescription
agent_idquerystring (uuid)no

Responses

200 ​

The derived progress document.

FieldTypeRequiredDescription
agent_idstring (uuid)yes
lessons_donearray of stringyesLesson ids in completion order.
badgesobjectyesBadge id -> the instant it was earned.
updated_atstring,null (date-time)yesThe most recent milestone, or null when the agent has none yet.

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"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.

json
{
  "error": {
    "code": "rate_limited",
    "message": "API key request quota exceeded; retry after the interval in Retry-After"
  }
}

The auditable training record — who completed what and when ​

http
GET /v1/academy/transcript

The constancia itself: every milestone of every agent of the tenant, with the agent's name, email and role, over a window. This is the business deliverable of MT-CLI-20 — the report a customer that audits the training of its collectors asks for.

The window is half-open — from inclusive, to EXCLUSIVE — like the billing reports, so a milestone at a period cut belongs to exactly one side. from defaults to the epoch and not to a trailing window: the question an auditor asks is "everything this collector ever completed", and the stream is bounded by construction (one row per agent and item, never more).

Tenant scope only: this is the export of the whole tenant, asked for by whoever audits — not by the audited. A ddw_ agent token is denied before the handler (403).

?format=csv (or Accept: text/csv) returns the artifact with its own X-Artifact-SHA256, so an auditor who files it can later prove it is the one that was handed over.

Parameters

NameInTypeRequiredDescription
fromquerystring (date-time)noRFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted).
toquerystring (date-time)noRFC 3339 upper bound.
agent_idquerystring (uuid)no
formatquerycsvnocsv for the downloadable artifact (equivalent: Accept: text/csv).

Responses

200 ​

The transcript (JSON) or the CSV artifact.

FieldTypeRequiredDescription
fromstring (date-time)yes
tostring (date-time)yes
agent_idstring,null (uuid)yesThe agent the report was filtered to, or null for the whole tenant.
totalintegeryes
rowsarray of AcademyTranscriptRowyes
rows[].agent_idstring (uuid)yes
rows[].agent_namestringyes
rows[].agent_emailstringyes
rows[].agent_roleagent · supervisor · adminyesThe agent's role TODAY (a join, not a snapshot) — someone who changed role has not un-completed their training.
rows[].kindlesson · badgeyes
rows[].item_idstringyes
rows[].occurred_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 — 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"
  }
}

429 — ApiKeyRateLimited ​

This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.

json
{
  "error": {
    "code": "rate_limited",
    "message": "API key request quota exceeded; retry after the interval in Retry-After"
  }
}

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