Kulcare Docs
DeveloperTeleconsultation

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

  1. Overview
  2. User Flows
  3. Endpoint Reference
  4. Scenarios
  5. Frontend Integration

Overview

Base URL

/api/v2/clinical/encounters/:encounter_id/video_session

Authentication

All endpoints require JWT authentication with one of:

  • practitioner_id in token → Doctor role (role: host)
  • patient_id in 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 notifications

Endpoint 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 found
  • 403 - 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 encounter
  • 403 - 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:

  1. Creates/updates VideoSessionParticipant record
  2. Generates Vonage token (24-hour expiry)
  3. Session auto-transitions to "active" on first participant join

Error Responses:

  • 404 - No video session exists
  • 403 - 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 triggered

Doctor 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)
StatusMeaning
createdSession created, waiting for participants
activeCall in progress
completedDoctor ended call normally
failedTechnical error during call
timed_outNo one joined within 30 minutes
declinedPatient 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

EventPurpose
sessionConnectedSuccessfully connected to Vonage
sessionDisconnectedDisconnected from Vonage
sessionReconnectingNetwork lost, auto-reconnecting (handled by SDK)
sessionReconnectedReconnection successful
connectionFailedConnection failed (show error to user)
connectionCreatedNew participant connected
connectionDestroyedParticipant disconnected
streamCreatedParticipant started publishing
streamDestroyedParticipant stopped publishing
streamPropertyChangedAudio/video toggled
signalCustom 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:

SourcePurpose
API (React Query)Persisted state (session metadata, status)
Vonage EventsReal-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

JobTriggerTiming
TeleconsultationReminderJobCreateServicet-15, t-10, t=0
SlackNotificationJobCreateService+5s, +10min
VideoSessionTimeoutJobCreateService+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'

On this page