Agents & seats
Generated from
DialerDigital/corecommit6e9f026f0a72— specopenapi/dialer-v1.yamlv0.36.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Seat roster, presence, softphone checkin/checkout.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/agents | Roster + live seat presence |
POST | /v1/agents | Create a seat |
GET | /v1/agents/{id} | Fetch one agent + presence |
POST | /v1/agents/{id}/credential | Enable or rotate an agent's widget-login password |
DELETE | /v1/agents/{id}/credential | Disable an agent's widget login |
POST | /v1/agents/checkin | Seat online + softphone bootstrap |
POST | /v1/agents/checkout | Seat offline (revokes the ephemeral SIP credential) |
POST | /v1/agents/{id}/pause | Take an available seat out of rotation (paused) |
POST | /v1/agents/{id}/resume | Return a paused seat to rotation (available) |
POST | /v1/agents/{id}/wrap-up | Finish after-call work (wrap_up -> available) |
GET | /v1/planned-shifts | List planned shifts for the tenant |
POST | /v1/planned-shifts | Create a planned shift |
Roster + live seat presence
GET /v1/agentsDurable roster merged with live presence (offline|available|ringing|on_call).
Responses
200
All agents with presence.
| Field | Type | Required | Description |
|---|---|---|---|
agents | array of Agent | yes | |
agents[].id | string (uuid) | no | |
agents[].tenant_id | string (uuid) | no | |
agents[].name | string | no | |
agents[].email | string | no | |
agents[].role | agent · supervisor · admin | no | |
agents[].status | active · inactive | no | Roster flag — NOT presence. |
agents[].sip_extension | string | null | no | |
agents[].device_mode | browser · external | no | |
agents[].login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agents[].presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agents[].created_at | string (date-time) | no | |
agents[].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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}Create a seat
POST /v1/agentsRequest body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
email | string (email) | yes | |
role | agent · supervisor · admin | no | |
sip_extension | string | no | The SIP registration identity calls bridge to. Only inert characters (digits, letters, dot, underscore, hyphen): the value is both the SIP identity and half of the media dialstring, so anything else is refused with 422 rather than diverging between the two. |
{
"name": "Postman Seat",
"email": "[email protected]",
"role": "agent",
"sip_extension": "1009"
}Responses
201
Created seat.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Fetch one agent + presence
GET /v1/agents/{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 agent.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}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).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Enable or rotate an agent's widget-login password
POST /v1/agents/{id}/credentialSets (or rotates) the agent's widget-login credential so they can sign into the widget with email + password (POST /v1/auth/agent-login). Tenant scope (a dd_ key or ddu_ session; a ddw_ is 403). The plaintext password is body-only — never persisted (PBKDF2-SHA512 at rest) or echoed back. A ROTATION revokes the agent's live ddw_ (the re-key eviction) AND tears down the SIP endpoint minted under that session (NR-02), reported in sip_teardown.
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 |
|---|---|---|---|
password | string (password) | yes |
Responses
200
The agent (now login_enabled true), plus the SIP teardown outcome.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.updated_at | string (date-time) | no | |
sip_teardown | ok · skipped · failed | yes | Outcome of the SIP teardown that accompanies a revocation (NR-02): the deletion of the agent's ephemeral agentcred credential and its live usrloc binding in Kamailio. * ok — credential and binding removed; the softphone can no longer register. * skipped — nothing to remove: the agent has no SIP extension, SBC provisioning is not configured in this environment, or the agent still holds a VIGENT session (see below) so this revocation left no access open. * failed — the DB revocation stands, but the SIP credential is STILL LIVE and will remain so until its 24h autoexpire. Treat as an open access path: retry, or evict the agent by other means. It is a field and not an error status on purpose: the revocation itself is durable and did happen, so a 5xx would be a lie in the other direction. What must never happen is a silent 200 implying an eviction that did not occur. The teardown follows the TRANSITION, not the agent. Revoking a token id that was already revoked — an id a client can legitimately hold, since GET /v1/widget_tokens lists revoked rows, and since minting supersedes the previous token — must NOT evict, because the agent's SUCCESSOR session would be the one cut. The teardown therefore runs only when the agent is left with no vigent token; otherwise it reports skipped. This is what keeps failed's retry advice usable: an already-revoked token with no live successor still evicts on a retried DELETE, so retrying is a real remedy and not a no-op. |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"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).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Disable an agent's widget login
DELETE /v1/agents/{id}/credentialClears the agent's widget-login password (back to login_enabled: false) and revokes any live ddw_ the agent holds, so the disable takes effect at once — INCLUDING the SIP endpoint (NR-02), reported in sip_teardown. Without that half, a disabled agent kept dialling over SIP until the credential's 24h autoexpire. The seat itself is untouched. Idempotent. Tenant scope (a ddw_ is 403).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The agent (now login_enabled false), plus the SIP teardown outcome.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.updated_at | string (date-time) | no | |
sip_teardown | ok · skipped · failed | yes | Outcome of the SIP teardown that accompanies a revocation (NR-02): the deletion of the agent's ephemeral agentcred credential and its live usrloc binding in Kamailio. * ok — credential and binding removed; the softphone can no longer register. * skipped — nothing to remove: the agent has no SIP extension, SBC provisioning is not configured in this environment, or the agent still holds a VIGENT session (see below) so this revocation left no access open. * failed — the DB revocation stands, but the SIP credential is STILL LIVE and will remain so until its 24h autoexpire. Treat as an open access path: retry, or evict the agent by other means. It is a field and not an error status on purpose: the revocation itself is durable and did happen, so a 5xx would be a lie in the other direction. What must never happen is a silent 200 implying an eviction that did not occur. The teardown follows the TRANSITION, not the agent. Revoking a token id that was already revoked — an id a client can legitimately hold, since GET /v1/widget_tokens lists revoked rows, and since minting supersedes the previous token — must NOT evict, because the agent's SUCCESSOR session would be the one cut. The teardown therefore runs only when the agent is left with no vigent token; otherwise it reports skipped. This is what keeps failed's retry advice usable: an already-revoked token with no live successor still evicts on a retried DELETE, so retrying is a real remedy and not a no-op. |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"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).
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Seat online + softphone bootstrap
POST /v1/agents/checkinSeat goes available. Optional device_mode (browser|external) persists on the seat and refines the campaign bridge hunt; omitted keeps the stored value (re-checkin stays idempotent). When SBC provisioning is enabled (KAMAILIO_RPC_URL; always on in prod) the response carries the FULL softphone bootstrap — an ephemeral SIP password minted per checkin, provisioned into Kamailio's agentcred htable (the ONLY place it exists; 24h autoexpire). Re-checkin reissues. With provisioning disabled the sip key is simply absent.
Two failures AFTER the seat is opened answer 503 and revert it, so the seat is never left claiming an agent the caller cannot reach. Branch on code: sbc_unavailable when the SBC RPC fails — credentials that cannot register are never returned — and presence_unavailable when the durable seat-presence write fails (core#557).
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
sip_extension | string | yes | |
device_mode | browser · external | no | browser = kamailio-first WSS softphone; external = straight to the FS registrar. |
{
"sip_extension": "1001",
"device_mode": "external"
}Responses
200
Seat online (+ softphone bootstrap when SBC provisioning is on).
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.updated_at | string (date-time) | no | |
sip | object (SipBootstrap) | no | Softphone bootstrap issued at checkin when SBC provisioning is enabled. The ephemeral password lives ONLY in Kamailio's agentcred htable — never in Postgres, never logged. Absent entirely when provisioning is disabled. |
sip.extension | string | no | Digest username (bare AOR). |
sip.password | string | no | Ephemeral, minted per checkin, NEVER persisted. |
sip.wss_url | string | no | SOFTPHONE_WSS_URL. |
sip.domain | string | no | SOFTPHONE_SIP_DOMAIN (From URI). |
sip.expires_at | string (date-time) | no | When this credential stops working. The htable autoexpire horizon (24h) CAPPED by the agent's widget session, when one STILL governs it: that session's absolute cap is at most 12h and the server evicts the credential the moment it lapses, so the earlier of the two is the only horizon a client can plan a renewal against. Always in the future — a session whose cap has already lapsed reports the flat horizon, never a past instant for a credential just issued. |
{
"agent": {
"id": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"name": "Postman Seat",
"email": "[email protected]",
"role": "agent",
"status": "active",
"sip_extension": "1001",
"device_mode": "external",
"presence": "available",
"created_at": "2026-06-01T12:00:00Z",
"updated_at": "2026-07-05T10:00:00Z"
},
"sip": {
"extension": "1001",
"password": "ephemeral-example-not-real",
"wss_url": "wss://127.0.0.1:8443",
"domain": "sip.usa.dialerdigital.com",
"expires_at": "2026-07-06T10:00:00Z"
}
}400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}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).
{
"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.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
Shape is fine, semantics are not (broken domain rule).
{
"error": {
"code": "unprocessable",
"message": "scheduled_at must be at least 10 minutes out"
}
}503 — CheckinUnavailable
Checkin refused fail-closed AFTER the seat was opened, so the seat was reverted and nothing durable is left claiming the agent is available. Retry. TWO causes, told apart by code (ErrorEnvelope says to branch on it, and a client that only knows one of them will silently do nothing for the other):
sbc_unavailable— the SBC RPC failed. Credentials that cannot register are never returned.presence_unavailable— the durable seat-presence write failed (core#557). It is not cosmetic: that column is whatSessionCapSweeperreads to tell this session apart from the one it is tearing down, so a checkin that answered 200 without it hands out a live SIP credential over a seat the sweeper may take back.
This response replaced the former SbcUnavailable, whose name promised the single cause it had when the checkin only had one.
sbc_unavailable
{
"error": {
"code": "sbc_unavailable",
"message": "softphone provisioning unavailable, retry"
}
}presence_unavailable
{
"error": {
"code": "presence_unavailable",
"message": "seat presence could not be recorded, retry"
}
}Seat offline (revokes the ephemeral SIP credential)
POST /v1/agents/checkoutOnly an available seat may leave (409 otherwise, fail-closed — a seat on a call cannot vanish). Revokes the ephemeral SIP credential (idempotent htable delete; the 24h autoexpire is the backstop for a missed revoke).
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
sip_extension | string | yes |
{
"sip_extension": "1001"
}Responses
200
Seat offline.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}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).
{
"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.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Take an available seat out of rotation (paused)
POST /v1/agents/{id}/pauseMoves an available (or wrap_up) seat to paused with a reason from a CLOSED set; a paused seat is never handed a call and may still check out. Tenant scope only (a ddw_ agent token is 403 by the widget allowlist). 409 when the agent is not checked in, is reserved / on a call, or is already paused (fail-closed: nothing is coerced). The seat is in-memory state; whether the seat stays out of rotation across a restart depends on the tenant's agent_session_journal_enabled flag (ADR #101 D10).
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 | manual · break · lunch · training · meeting | yes |
{
"reason": "lunch"
}Responses
200
Seat paused; agent.presence is read back from the seat.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.updated_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"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).
{
"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.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Return a paused seat to rotation (available)
POST /v1/agents/{id}/resumeMoves a paused seat back to available, at the BACK of the reservation queue. Tenant scope only. 409 when the agent is not checked in or the seat is not paused.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
Seat available again; agent.presence is read back from the seat.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"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).
{
"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.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}Finish after-call work (wrap_up -> available)
POST /v1/agents/{id}/wrap-upThe agent is done with after-call work: moves a wrap_up seat back to available, at the BACK of the reservation queue, and drops the reservation token the call left on the seat. Tenant scope only. 409 when the agent is not checked in or the seat is not in wrap_up.
There is NO verb that ENTERS wrap_up: the seat FSM enters it only on the hangup of an ANSWERED call, and only when the tenant's agent_wrap_up_seconds cap is above 0 (ADR #101 D5/D8). The cap is the other exit — the seat returns to available on its own when it expires — and POST /v1/agents/{id}/pause is the third (an agent may close its ACW with a pause).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
After-call work finished; agent.presence is read back from the seat.
| Field | Type | Required | Description |
|---|---|---|---|
agent | object (Agent) | yes | |
agent.id | string (uuid) | no | |
agent.tenant_id | string (uuid) | no | |
agent.name | string | no | |
agent.email | string | no | |
agent.role | agent · supervisor · admin | no | |
agent.status | active · inactive | no | Roster flag — NOT presence. |
agent.sip_extension | string | null | no | |
agent.device_mode | browser · external | no | |
agent.login_enabled | boolean | no | Widget self-login state — true iff a password credential is set. The password hash itself is never exposed. |
agent.presence | offline · available · ringing · on_call · wrap_up · paused | no | Live seat presence from the in-memory AgentFSM. wrap_up is after-call work (entered on hangup when the tenant's agent_wrap_up_seconds cap is above 0); paused is a seat taken out of rotation with a reason (ADR #101 D1, MT-ROAD-R11). |
agent.created_at | string (date-time) | no | |
agent.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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"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).
{
"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.
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}List planned shifts for the tenant
GET /v1/planned-shiftsResponses
200
List of planned shifts
| Field | Type | Required | Description |
|---|---|---|---|
shifts | array of PlannedShift | yes | |
shifts[].id | string (uuid) | yes | |
shifts[].agent_id | string (uuid) | yes | |
shifts[].start_time | string (date-time) | yes | |
shifts[].end_time | string (date-time) | yes | |
shifts[].planned_state | available · ringing · on_call · wrap_up · paused | yes |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}Create a planned shift
POST /v1/planned-shiftsRequest body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string (uuid) | yes | |
start_time | string (date-time) | yes | |
end_time | string (date-time) | yes | |
planned_state | available · ringing · on_call · wrap_up · paused | no |
Responses
201
Planned shift created.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
{
"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).
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended.
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}