Appearance
Academy
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.
In-app Academy training records — the auditable constancia (who completed what and when), append-only and per agent.
Endpoints
| Method | Path | Summary |
|---|---|---|
POST | /v1/academy/completions | Record ONE Academy milestone for an agent |
GET | /v1/academy/progress | An agent's DERIVED Academy state |
GET | /v1/academy/transcript | The auditable training record — who completed what and when |
Record ONE Academy milestone for an agent
http
POST /v1/academy/completionsAppends 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)
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string (uuid) | no | Required 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). |
kind | lesson · badge | yes | |
item_id | string | yes | Curriculum 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).
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | yes | |
agent_id | string (uuid) | yes | |
kind | lesson · badge | yes | |
item_id | string | yes | |
occurred_at | string (date-time) | yes | Stamped 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/progressFolds 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
agent_id | query | string (uuid) | no |
Responses
200
The derived progress document.
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string (uuid) | yes | |
lessons_done | array of string | yes | Lesson ids in completion order. |
badges | object | yes | Badge id -> the instant it was earned. |
updated_at | string,null (date-time) | yes | The 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/transcriptThe 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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string (date-time) | no | RFC 3339 lower bound (endpoint-specific field; default trailing 30 UTC days where noted). |
to | query | string (date-time) | no | RFC 3339 upper bound. |
agent_id | query | string (uuid) | no | |
format | query | csv | no | csv for the downloadable artifact (equivalent: Accept: text/csv). |
Responses
200
The transcript (JSON) or the CSV artifact.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (date-time) | yes | |
to | string (date-time) | yes | |
agent_id | string,null (uuid) | yes | The agent the report was filtered to, or null for the whole tenant. |
total | integer | yes | |
rows | array of AcademyTranscriptRow | yes | |
rows[].agent_id | string (uuid) | yes | |
rows[].agent_name | string | yes | |
rows[].agent_email | string | yes | |
rows[].agent_role | agent · supervisor · admin | yes | The agent's role TODAY (a join, not a snapshot) — someone who changed role has not un-completed their training. |
rows[].kind | lesson · badge | yes | |
rows[].item_id | string | yes | |
rows[].occurred_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 — 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"
}
}