Teleconsultation API Guide
Complete API reference with user flows, payloads, and response examples for doctor and patient scenarios. Last Updated: 2026-03-30
Table of Contents
Overview
Base URL
/api/v2/clinical/encounters/:encounter_id/video_sessionAuthentication
All endpoints require JWT authentication with one of:
practitioner_idin token → Doctor role (role:host)patient_idin token → Patient role (role:participant)
Prerequisites
- Encounter must have
encounter_class: 'virtual'(telemedicine) - Encounter must not be in terminal state (
cancelled,entered_in_error,finished)
User Flows
Doctor-Initiated Flow (Production Ready ✅)
1. Patient books telemedicine appointment
2. Appointment time arrives
3. Doctor opens virtual encounter in CMS
4. Doctor clicks "Start Video Call" → POST create
├── Creates VideoSession (status: "created")
├── Creates Vonage session
├── Schedules 30-min timeout job
└── Sends SMS/WhatsApp to patient
5. Patient joins via link → POST join
├── Backend returns Vonage token + session_id
└── Patient connects to Vonage
6. Doctor joins → POST join → connects to Vonage
7. Doctor clicks "Start Session" → POST start
├── Status changes to "active"
└── Video call begins
8. Consultation via Vonage WebRTC
9. Doctor clicks "End Call" → POST complete
├── Status changes to "completed"
└── Triggers notificationsEndpoint Reference
1. Create Session
When: Doctor clicks "Start Video Call" from virtual encounter
URL: POST /api/v2/clinical/encounters/:encounter_id/video_session
Auth: Doctor JWT (must be practitioner on encounter)
Request:
{
"provider": "vonage" // optional, defaults to vonage
}Response (201):
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"encounter_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"provider": "vonage",
"status": "created",
"started_at": null,
"completed_at": null,
"provider_session_id": "2_MX40NzIwMDAwMDB7...",
"created_at": "2026-03-27T10:00:00Z",
"updated_at": "2026-03-27T10:00:00Z"
}
}Note: If an active session already exists, returns the existing session (idempotent).
Side Effects:
- Creates Vonage session via adapter
- Schedules background jobs:
TeleconsultationReminderJob(doctor_15min, patient_10min, at_time)SlackNotificationJob(new session alert)VideoSessionTimeoutJob(30-min timeout)
- Sends SMS/WhatsApp to patient with join link
Error Responses:
404- Encounter not found403- Not authorized (not practitioner on encounter)422- Encounter not eligible (not virtual class or already finished)
2. Get Session Status
When: Check current session state (polling, page load)
URL: GET /api/v2/clinical/encounters/:encounter_id/video_session
Auth: Doctor or Patient JWT
Response (200):
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"encounter_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"provider": "vonage",
"status": "active",
"started_at": "2026-03-27T10:05:00Z",
"completed_at": null,
"provider_session_id": "2_MX40NzIwMDAwMDB7...",
"created_at": "2026-03-27T10:00:00Z",
"updated_at": "2026-03-27T10:05:00Z",
"active_participant_count": 2,
"participants": [
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"participant_type": "practitioner",
"participant_id": "d4e5f6a7-b8c9-0123-def0-123456789012",
"role": "host",
"joined_at": "2026-03-27T10:05:00Z",
"declined_at": null,
"left_at": null,
"active": true
}
]
}
}Error Responses:
404- No video session exists for this encounter403- Not authorized
3. Join Session
When: Patient clicks join link OR Doctor clicks join in CMS
URL: POST /api/v2/clinical/encounters/:encounter_id/video_session/join
Auth:
- Doctor JWT → participant_type:
practitioner, role:host - Patient JWT → participant_type:
patient, role:participant
Request: No body (auth determines identity)
Response (200):
{
"data": {
"participant": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"participant_type": "patient",
"participant_id": "e5f6a7b8-c9d0-1234-ef01-234567890123",
"role": "participant",
"joined_at": "2026-03-27T10:00:00Z",
"active": true
},
"token_data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"session_id": "2_MX40NzIwMDAwMDB7...",
"api_key": "12345678",
"token_expires_at": "2026-03-28T10:00:00Z"
},
"video_session": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "created"
}
},
"meta": {
"request_id": "req_abc123"
}
}What Happens:
- Creates/updates
VideoSessionParticipantrecord - Generates Vonage token (24-hour expiry)
- Session auto-transitions to "active" on first participant join
Error Responses:
404- No video session exists403- Not authorized (not patient/practitioner on encounter)422- Session completed or not joinable
4. Get/Refresh Token
When: Need new token (expired, refresh before call)
URL: POST /api/v2/clinical/encounters/:encounter_id/video_session/token
Auth: Doctor or Patient JWT
Response (200):
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"session_id": "2_MX40NzIwMDAwMDB7...",
"api_key": "12345678",
"expires_at": "2026-03-28T10:00:00Z"
},
"meta": {
"request_id": "req_abc123"
}
}5. Start Session
When: Doctor marks call as officially started
URL: POST /api/v2/clinical/encounters/:encounter_id/video_session/start
Auth: Doctor JWT only
Response (200):
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "active",
"started_at": "2026-03-27T10:05:00Z",
"completed_at": null
}
}Note: Session auto-transitions to "active" when first participant joins. This endpoint is for explicit start confirmation.
6. Complete Session
When: Doctor ends the consultation
URL: POST /api/v2/clinical/encounters/:encounter_id/video_session/complete
Auth: Doctor JWT only
Request:
{
"connection_id": "2_MX40NzIwMDAwMDB7" // optional
}Response (200):
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "completed",
"started_at": "2026-03-27T10:05:00Z",
"completed_at": "2026-03-27T10:35:00Z",
"duration_seconds": 1800
}
}7. Decline Session
When: Patient declines the call
URL: POST /api/v2/clinical/encounters/:encounter_id/video_session/decline
Auth: Patient JWT only
Response (200):
{
"data": {
"declined": true
},
"meta": {
"request_id": "req_abc123"
}
}Scenarios
Happy Path - Doctor Starts, Patient Joins
1. Doctor creates session
POST /video_session
→ 201 Created, status: "created"
2. Patient receives SMS, clicks link, joins
POST /video_session/join
→ 200 OK, token + session_id
3. Doctor joins from CMS
POST /video_session/join
→ 200 OK, token + session_id
4. Session auto-transitions to "active"
5. Doctor completes
POST /video_session/complete
→ 200 OK, status: "completed"Patient No-Show (30-min timeout)
1. Doctor creates session
POST /video_session
→ 201 Created
2. No patient joins within 30 minutes
3. VideoSessionTimeoutJob fires
→ status: "timed_out"
→ patient refund triggeredDoctor Clicks "Start" Twice
1. Doctor clicks "Start Video"
POST /video_session
→ 201 Created, session: "abc123"
2. Doctor clicks "Start Video" again
POST /video_session
→ 200 OK, session: "abc123" (existing)Session Status Reference
created ──> active ──> completed
│ │
│ +--> failed
│
+--> timed_out (30-min no-show)
+--> declined (patient declined)| Status | Meaning |
|---|---|
created | Session created, waiting for participants |
active | Call in progress |
completed | Doctor ended call normally |
failed | Technical error during call |
timed_out | No one joined within 30 minutes |
declined | Patient declined the invitation |
Frontend Integration
File Structure
frontend/apps/cms/src/features/video-sessions/
├── types.ts # Vonage SDK + web component type definitions
├── hooks/
│ ├── index.ts # Hook exports
│ └── useVideoSessionState.ts # SDK state machine (connect, events, reconnect)
├── components/
│ └── PreCallSetup.tsx # Camera/mic permissions & preview
└── pages/
└── VideoSessionPage.tsx # Main video session page (UI + integration)
frontend/packages/shared-services/src/domains/clinical/
├── api.ts # Video session API functions (lines 1524-1611)
├── hooks.ts # React Query hooks (useVideoSession, etc.)
├── types.ts # VideoSession, VideoSessionTokenData types
└── keys.ts # Query key factory (videoSessions.detail)Vonage SDK Events Handled
| Event | Purpose |
|---|---|
sessionConnected | Successfully connected to Vonage |
sessionDisconnected | Disconnected from Vonage |
sessionReconnecting | Network lost, auto-reconnecting (handled by SDK) |
sessionReconnected | Reconnection successful |
connectionFailed | Connection failed (show error to user) |
connectionCreated | New participant connected |
connectionDestroyed | Participant disconnected |
streamCreated | Participant started publishing |
streamDestroyed | Participant stopped publishing |
streamPropertyChanged | Audio/video toggled |
signal | Custom messages |
Key: Getting Session ID for SDK
Important: The Vonage session ID comes from token_data.session_id, NOT from video_session.id:
// ✅ CORRECT - session_id from token_data
const sessionId = joinResponse.token_data.session_id;
const token = joinResponse.token_data.token;
// ❌ WRONG - video_session.id is the DB UUID
const wrongId = joinResponse.video_session.id;State Management
Two sources of truth:
| Source | Purpose |
|---|---|
| API (React Query) | Persisted state (session metadata, status) |
| Vonage Events | Real-time state (participants, streams) |
Error Handling
try {
await joinVideoSession(encounterId);
toast.success('Joined session');
} catch (error) {
if (isValidationError(error)) {
toast.error('Cannot join: Session may have ended');
} else {
toast.error(getAPIErrorMessage(error, 'Failed to join session'));
}
}Background Jobs
| Job | Trigger | Timing |
|---|---|---|
TeleconsultationReminderJob | CreateService | t-15, t-10, t=0 |
SlackNotificationJob | CreateService | +5s, +10min |
VideoSessionTimeoutJob | CreateService | +30 min |
Testing
TOKEN="your-jwt-token"
ENCOUNTER_ID="encounter-uuid"
BASE="/api/v2/clinical/encounters/$ENCOUNTER_ID/video_session"
# 1. Create session
curl -X POST "http://localhost:3000$BASE" \
-H "Authorization: Bearer $TOKEN" | jq
# 2. Join (get token)
curl -X POST "http://localhost:3000$BASE/join" \
-H "Authorization: Bearer $TOKEN" | jq
# 3. Verify session_id format (should be "2_MX...")
jq '.data.token_data.session_id'