Skip to main content

Virtual Care Service — API Contracts

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD

Global Conventions

ConventionValue
Base path (Kong)/api/v1/virtual-care
AuthAuthorization: Bearer <Keycloak RS256 JWT> (except join endpoint — HMAC token)
Content-Typeapplication/json
Tenant scopingExtracted from JWT tenantId claim; cross-tenant = 403
TimestampsISO-8601 UTC
ID formatULID with prefix: vsn_ (VirtualSession), vsp_ (participant), avs_ (AsyncVisit)

Error Code Reference

HTTPCodeMeaning
400VALIDATION_ERRORRequest body/params failed DTO validation
401TOKEN_EXPIREDJoin token past expiry
401TOKEN_INVALIDToken signature invalid or malformed
401UNAUTHORIZEDMissing or invalid JWT
403MODULE_NOT_LICENSEDTenant does not have Virtual Care licensed
403ACCESS_DENIEDInsufficient role/permission
403CONSENT_REQUIREDPatient telehealth consent not recorded
404SESSION_NOT_FOUNDSession ID not found within tenant
409INVALID_STATUS_TRANSITIONFSM state change not allowed from current state
409OPTIMISTIC_LOCK_CONFLICTSession version mismatch
409PARTICIPANT_NOT_IN_WAITING_ROOMAdmit attempted but participant not in waiting status
422MAX_PARTICIPANTS_EXCEEDEDTenant max participant limit reached
503VIDEO_PROVIDER_UNAVAILABLEVideo backend health check failed
503PROVIDER_QUOTA_EXCEEDEDExternal 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:

ParamTypeDescription
patientIdULIDFilter by patient
statusstringscheduled|waiting|active|ended|cancelled|failed
fromISO-8601Scheduled start ≥
toISO-8601Scheduled start ≤
nodeIdULIDFilter by hierarchy node
pageintegerDefault 1
pageSizeintegerDefault 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:

ParamTypeDescription
tokenstringHMAC-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 OperationNotes
GET /fhir/Appointment?patient=Patient/{id}&serviceType=VRVirtual appointments for a patient
GET /fhir/Encounter?appointment=Appointment/{id}&class=VRVirtual encounter post-session
POST /fhir/EncounterCalled internally by virtual-care-service at session end
GET /fhir/AuditEvent?entity=VirtualSession/{id}Session audit trail

Kong Route Configuration

MethodPathAuth
POST/api/v1/virtual-care/sessionsJWT
GET/api/v1/virtual-care/sessionsJWT
GET/api/v1/virtual-care/sessions/:idJWT
POST/api/v1/virtual-care/sessions/:id/endJWT
POST/api/v1/virtual-care/sessions/:id/cancelJWT
POST/api/v1/virtual-care/sessions/:id/admit/:participantIdJWT
GET/api/v1/virtual-care/sessions/joinHMAC token (JWT bypass on this route)
GET/PUT/api/v1/virtual-care/configJWT
POST/api/v1/virtual-care/config/test-connectionJWT
POST/api/v1/virtual-care/async-visitsJWT