Virtual Care Service — API Contracts
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD
Global Conventions
| Convention | Value |
|---|---|
| Base path (Kong) | /api/v1/virtual-care |
| Auth | Authorization: Bearer <Keycloak RS256 JWT> (except join endpoint — HMAC token) |
| Content-Type | application/json |
| Tenant scoping | Extracted from JWT tenantId claim; cross-tenant = 403 |
| Timestamps | ISO-8601 UTC |
| ID format | ULID with prefix: vsn_ (VirtualSession), vsp_ (participant), avs_ (AsyncVisit) |
Error Code Reference
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body/params failed DTO validation |
| 401 | TOKEN_EXPIRED | Join token past expiry |
| 401 | TOKEN_INVALID | Token signature invalid or malformed |
| 401 | UNAUTHORIZED | Missing or invalid JWT |
| 403 | MODULE_NOT_LICENSED | Tenant does not have Virtual Care licensed |
| 403 | ACCESS_DENIED | Insufficient role/permission |
| 403 | CONSENT_REQUIRED | Patient telehealth consent not recorded |
| 404 | SESSION_NOT_FOUND | Session ID not found within tenant |
| 409 | INVALID_STATUS_TRANSITION | FSM state change not allowed from current state |
| 409 | OPTIMISTIC_LOCK_CONFLICT | Session version mismatch |
| 409 | PARTICIPANT_NOT_IN_WAITING_ROOM | Admit attempted but participant not in waiting status |
| 422 | MAX_PARTICIPANTS_EXCEEDED | Tenant max participant limit reached |
| 503 | VIDEO_PROVIDER_UNAVAILABLE | Video backend health check failed |
| 503 | PROVIDER_QUOTA_EXCEEDED | External provider API quota/rate limit hit |
Resource Group: Sessions
POST /api/v1/virtual-care/sessions
Creates a new virtual session. Video room is provisioned before session row is persisted.
Auth scopes: virtual_care:session:create
Roles: clinician, scheduler, nurse_practitioner, admin
Request body:
{
"patientId": "pat_01HX...",
"appointmentId": "appt_01HX...",
"scheduledStart": "2026-04-20T10:00:00Z",
"scheduledEnd": "2026-04-20T10:30:00Z",
"additionalParticipants": [
{ "userId": "usr_01HX...", "role": "interpreter" }
]
}
Response 201:
{
"id": "vsn_01HX...",
"tenantId": "ten_01HX...",
"patientId": "pat_01HX...",
"appointmentId": "appt_01HX...",
"encounterId": null,
"initiatorId": "usr_01HX...",
"videoBackend": "jitsi",
"roomName": "ghasi-abc123def456",
"roomUrl": "https://meet.example.com/ghasi-abc123def456",
"status": "scheduled",
"scheduledStart": "2026-04-20T10:00:00Z",
"scheduledEnd": "2026-04-20T10:30:00Z",
"actualStart": null,
"actualEnd": null,
"recordingEnabled": false,
"recordingRef": null,
"failureReason": null,
"messagingThreadId": null,
"participants": [],
"version": 1,
"createdAt": "2026-04-18T07:00:00Z",
"updatedAt": "2026-04-18T07:00:00Z"
}
Errors: 400 VALIDATION_ERROR, 403 MODULE_NOT_LICENSED | ACCESS_DENIED | CONSENT_REQUIRED, 503 VIDEO_PROVIDER_UNAVAILABLE
GET /api/v1/virtual-care/sessions
Lists virtual sessions for the authenticated user's tenant.
Auth scopes: virtual_care:session:read
Query parameters:
| Param | Type | Description |
|---|---|---|
patientId | ULID | Filter by patient |
status | string | scheduled|waiting|active|ended|cancelled|failed |
from | ISO-8601 | Scheduled start ≥ |
to | ISO-8601 | Scheduled start ≤ |
nodeId | ULID | Filter by hierarchy node |
page | integer | Default 1 |
pageSize | integer | Default 20, max 100 |
Response 200: { data: VirtualSessionDto[], total: number, page: number, pageSize: number }
GET /api/v1/virtual-care/sessions/:id
Get a single session by ID with full participants array.
Response 200: VirtualSessionDto with participants: VirtualSessionParticipantDto[]
Errors: 403 ACCESS_DENIED, 404 SESSION_NOT_FOUND
POST /api/v1/virtual-care/sessions/:id/end
End an active or waiting session.
Auth scopes: virtual_care:session:end
Roles: clinician, admin
Response 200: Updated VirtualSessionDto with status: ended.
Errors: 409 INVALID_STATUS_TRANSITION (if not active or waiting)
POST /api/v1/virtual-care/sessions/:id/cancel
Cancel a scheduled or waiting session.
Roles: clinician, scheduler, admin
Request body:
{ "reason": "Patient unable to join due to connectivity issues" }
Response 200: Updated VirtualSessionDto with status: cancelled.
Errors: 409 INVALID_STATUS_TRANSITION
POST /api/v1/virtual-care/sessions/:id/fallback-message
Initiate async messaging fallback when video is unavailable.
Roles: clinician, admin
Response 201:
{ "messagingThreadId": "thr_01HX..." }
Errors: 409 INVALID_STATUS_TRANSITION (session not in failed or active)
Resource Group: Participants and Waiting Room
GET /api/v1/virtual-care/sessions/:id/join-token
Issue or refresh a join token for the requesting user.
Auth: Bearer JWT (caller must be a participant of the session)
Response 200:
{
"joinToken": "eyJ...",
"expiresAt": "2026-04-20T10:15:00Z",
"waitingRoomUrl": "https://meet.example.com/waiting/ghasi-abc123"
}
Errors: 403 ACCESS_DENIED (not a participant), 404 SESSION_NOT_FOUND
GET /api/v1/virtual-care/sessions/join
Token-protected endpoint. Validates join token and returns destination URL.
Auth: HMAC join token in ?token= query param (no JWT required).
Query parameters:
| Param | Type | Description |
|---|---|---|
token | string | HMAC-signed join token |
Response 200:
{
"destination": "waiting_room",
"url": "https://meet.example.com/waiting/ghasi-abc123",
"sessionStatus": "scheduled"
}
Errors: 401 TOKEN_EXPIRED | TOKEN_INVALID, 409 INVALID_STATUS_TRANSITION (session already ended)
POST /api/v1/virtual-care/sessions/:id/admit/:participantId
Admit a patient from the waiting room. Provider only.
Auth scopes: virtual_care:session:admit
Roles: clinician, nurse_practitioner, admin
Response 200:
{ "videoJoinUrl": "https://meet.example.com/ghasi-abc123?jwt=..." }
Errors: 409 PARTICIPANT_NOT_IN_WAITING_ROOM, 404 SESSION_NOT_FOUND
POST /api/v1/virtual-care/sessions/:id/remove/:participantId
Remove a participant from an active session.
Roles: clinician, admin
Response 200: { "success": true }
Errors: 404 SESSION_NOT_FOUND
GET /api/v1/virtual-care/sessions/:id/participants
List all participants with status.
Response 200: VirtualSessionParticipantDto[]
Resource Group: Async Visits (Store-and-Forward)
POST /api/v1/virtual-care/async-visits
Submit an async visit (store-and-forward). May be authored offline.
Auth scopes: virtual_care:async_visit:create
Request body:
{
"patientId": "pat_01HX...",
"appointmentId": "appt_01HX...",
"chiefComplaint": "Headache for 3 days",
"attachments": [{ "mimeType": "image/jpeg", "url": "https://..." }],
"clientMutationId": "client-uuid-stable-across-retries"
}
Response 201: AsyncVisitDto
Idempotency: clientMutationId ensures duplicate offline-to-online submits are safe.
Resource Group: Configuration
GET /api/v1/virtual-care/config
Get current tenant virtual-care configuration (credentials redacted).
Roles: admin, tenant_admin
Response 200: TenantVirtualCareConfigDto with all credential fields as "****".
PUT /api/v1/virtual-care/config
Update tenant virtual-care configuration.
Roles: tenant_admin, platform_admin
Request body:
{
"defaultVideoBackend": "jitsi",
"jitsiServerUrl": "https://meet.example.com",
"brandingLogoUrl": "https://cdn.example.com/logo.png",
"brandingPrimaryColor": "#1E40AF",
"roomSlugPrefix": "clinic",
"recordingEnabled": false,
"sessionGraceMinutesBefore": 15,
"sessionGraceMinutesAfter": 30,
"maxParticipants": 8
}
Response 200: Updated TenantVirtualCareConfigDto.
Errors: 400 VALIDATION_ERROR, 403 ACCESS_DENIED
POST /api/v1/virtual-care/config/test-connection
Test connectivity to the configured video backend.
Roles: tenant_admin, platform_admin
Request body: { "backend": "jitsi" }
Response 200: { "healthy": true, "latencyMs": 45 }
Response 503: VIDEO_PROVIDER_UNAVAILABLE with diagnostic message.
FHIR REST Mappings (via interop-service)
| FHIR Operation | Notes |
|---|---|
GET /fhir/Appointment?patient=Patient/{id}&serviceType=VR | Virtual appointments for a patient |
GET /fhir/Encounter?appointment=Appointment/{id}&class=VR | Virtual encounter post-session |
POST /fhir/Encounter | Called internally by virtual-care-service at session end |
GET /fhir/AuditEvent?entity=VirtualSession/{id} | Session audit trail |
Kong Route Configuration
| Method | Path | Auth |
|---|---|---|
POST | /api/v1/virtual-care/sessions | JWT |
GET | /api/v1/virtual-care/sessions | JWT |
GET | /api/v1/virtual-care/sessions/:id | JWT |
POST | /api/v1/virtual-care/sessions/:id/end | JWT |
POST | /api/v1/virtual-care/sessions/:id/cancel | JWT |
POST | /api/v1/virtual-care/sessions/:id/admit/:participantId | JWT |
GET | /api/v1/virtual-care/sessions/join | HMAC token (JWT bypass on this route) |
GET/PUT | /api/v1/virtual-care/config | JWT |
POST | /api/v1/virtual-care/config/test-connection | JWT |
POST | /api/v1/virtual-care/async-visits | JWT |