Skip to content

Videocall Integration (Daily.co)

Overview

All appointments in upcoming, confirmed, and inprogress states automatically have a Daily.co videocall room created. Rooms are managed via the Daily.co REST API.

Room Naming Convention

restartix-{orgID}-{appointmentID}

Examples:

  • restartix-1-102 (Organization 1, Appointment 102)
  • restartix-3-458 (Organization 3, Appointment 458)

Why this pattern?

  • Globally unique across all organizations
  • Easily traceable to appointment record
  • No collisions between orgs

Room Lifecycle

Creation

When: Appointment transitions from booked to upcoming (onboarding)

API Call:

http
POST https://api.daily.co/v1/rooms
Authorization: Bearer {API_KEY}
Content-Type: application/json

{
  "name": "restartix-1-102",
  "privacy": "private",
  "properties": {
    "exp": 1706094600,
    "enable_screenshare": true,
    "enable_chat": true,
    "enable_knocking": true,
    "enable_prejoin_ui": true,
    "start_video_off": false,
    "start_audio_off": false,
    "owner_only_broadcast": false,
    "enable_recording": "cloud",
    "max_participants": 2
  }
}

Response:

json
{
  "id": "d61cd7b2-a273-42b1-a1e6-657a7b8f4c8e",
  "name": "restartix-1-102",
  "api_created": true,
  "privacy": "private",
  "url": "https://restartix.daily.co/restartix-1-102",
  "created_at": "2025-01-15T10:00:00Z",
  "config": {
    "exp": 1706094600
  }
}

Stored in appointment record:

  • videocall_room_name = "restartix-1-102"
  • videocall_room_url = "https://restartix.daily.co/restartix-1-102"

Expiration

When: Set to appointment's ended_at timestamp

Daily.co automatically deletes the room after expiration. This ensures:

  • No lingering rooms after appointment ends
  • Automatic cleanup (no manual deletion needed after completion)

Calculation:

go
exp := appointment.EndedAt.Unix()  // UNIX timestamp

Example: If appointment ends at 2025-01-20T10:30:00Z, room expires at that exact time.


Update (Reschedule)

When: Appointment is rescheduled

API Call:

http
PATCH https://api.daily.co/v1/rooms/{roomName}
Authorization: Bearer {API_KEY}
Content-Type: application/json

{
  "properties": {
    "exp": 1706098200
  }
}

Logic:

  1. Calculate new ended_at from rescheduled time
  2. Update room expiration via Daily.co API
  3. Update appointment record

Deletion

When:

  • Appointment cancelled
  • Appointment marked no-show
  • Admin hard-deletes appointment

API Call:

http
DELETE https://api.daily.co/v1/rooms/{roomName}
Authorization: Bearer {API_KEY}

Response: 200 OK (room deleted) or 404 (room already expired/deleted)

Error handling: If room doesn't exist (404), treat as success (idempotent).


Recreation (Reinstate)

When:

  • Cancelled appointment reinstated to upcoming
  • No-show appointment reinstated to upcoming

Logic:

  1. Check if room still exists (GET /v1/rooms/{roomName})
  2. If exists and valid → keep it
  3. If not exists or expired → create new room
go
func (s *VideocallService) EnsureRoom(ctx context.Context, appt *Appointment) error {
    roomName := fmt.Sprintf("restartix-%d-%d", appt.OrganizationID, appt.ID)

    // Try to get existing room
    room, err := s.dailyClient.GetRoom(roomName)
    if err == nil && room.Config.Exp > time.Now().Unix() {
        // Room exists and is still valid
        return nil
    }

    // Create new room
    return s.CreateRoom(ctx, appt)
}

Join URLs

Specialist Join URL (with token)

Specialists receive a meeting token with host privileges:

API Call:

http
POST https://api.daily.co/v1/meeting-tokens
Authorization: Bearer {API_KEY}
Content-Type: application/json

{
  "properties": {
    "room_name": "restartix-1-102",
    "is_owner": true,
    "user_name": "Dr. Smith",
    "enable_recording": "cloud",
    "start_cloud_recording": true
  }
}

Response:

json
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Join URL:

https://restartix.daily.co/restartix-1-102?t=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token properties:

  • is_owner: true — Can start/stop recording, remove participants
  • enable_recording: "cloud" — Can record to Daily.co cloud
  • user_name — Pre-filled display name
  • Token expires after appointment ended_at time

Patient Join URL (with token)

Patients receive a meeting token with guest privileges:

API Call:

http
POST https://api.daily.co/v1/meeting-tokens
Authorization: Bearer {API_KEY}
Content-Type: application/json

{
  "properties": {
    "room_name": "restartix-1-102",
    "is_owner": false,
    "user_name": "John Doe",
    "enable_recording": "cloud",
    "start_cloud_recording": false
  }
}

Join URL:

https://restartix.daily.co/restartix-1-102?t=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token properties:

  • is_owner: false — Cannot control recording or remove participants
  • user_name — Pre-filled with patient name
  • Token expires after appointment ended_at time

Room Configuration

Privacy Settings

  • Privacy: private — Room requires authentication (join via token only)
  • No public access — Cannot join without token

Features Enabled

FeatureEnabledNotes
Screen shareYesBoth specialist and patient
ChatYesText chat during call
KnockingYesPatient waits in lobby until specialist admits
Prejoin UIYesCamera/mic test before joining
RecordingYes (cloud)Specialist can start/stop recording
Video on by defaultYes
Audio on by defaultYes
Owner-only broadcastNoBoth can speak
Max participants2One specialist, one patient

API Integration (Go)

Service Interface

go
// internal/services/videocall/service.go

type VideocallService interface {
    CreateRoom(ctx context.Context, appt *Appointment) error
    DeleteRoom(ctx context.Context, orgID, appointmentID int64) error
    UpdateRoomExpiration(ctx context.Context, orgID, appointmentID int64, newEndTime time.Time) error
    GenerateSpecialistToken(ctx context.Context, roomName, specialistName string, endTime time.Time) (string, error)
    GeneratePatientToken(ctx context.Context, roomName, patientName string, endTime time.Time) (string, error)
}

Create Room Implementation

go
func (s *DailyService) CreateRoom(ctx context.Context, appt *Appointment) error {
    roomName := fmt.Sprintf("restartix-%d-%d", appt.OrganizationID, appt.ID)

    req := &DailyCreateRoomRequest{
        Name:    roomName,
        Privacy: "private",
        Properties: DailyRoomProperties{
            Exp:               appt.EndedAt.Unix(),
            EnableScreenshare: true,
            EnableChat:        true,
            EnableKnocking:    true,
            EnablePrejoinUI:   true,
            StartVideoOff:     false,
            StartAudioOff:     false,
            EnableRecording:   "cloud",
            MaxParticipants:   2,
        },
    }

    resp, err := s.client.CreateRoom(ctx, req)
    if err != nil {
        return fmt.Errorf("daily.co create room: %w", err)
    }

    // Store room info in appointment
    appt.VideocallRoomName = roomName
    appt.VideocallRoomURL = resp.URL

    return nil
}

Delete Room Implementation

go
func (s *DailyService) DeleteRoom(ctx context.Context, orgID, appointmentID int64) error {
    roomName := fmt.Sprintf("restartix-%d-%d", orgID, appointmentID)

    err := s.client.DeleteRoom(ctx, roomName)
    if err != nil {
        // If room doesn't exist (404), treat as success
        if errors.Is(err, ErrRoomNotFound) {
            return nil
        }
        return fmt.Errorf("daily.co delete room: %w", err)
    }

    return nil
}

Generate Join Token

go
func (s *DailyService) GenerateSpecialistToken(ctx context.Context, roomName, specialistName string, endTime time.Time) (string, error) {
    req := &DailyMeetingTokenRequest{
        Properties: DailyTokenProperties{
            RoomName:           roomName,
            IsOwner:            true,
            UserName:           specialistName,
            EnableRecording:    "cloud",
            StartCloudRecording: true,
            Exp:                endTime.Unix(),
        },
    }

    resp, err := s.client.CreateMeetingToken(ctx, req)
    if err != nil {
        return "", fmt.Errorf("daily.co create token: %w", err)
    }

    return resp.Token, nil
}

Frontend Integration

Embedding Daily.co

React component example:

tsx
import DailyIframe from '@daily-co/daily-js';
import { useEffect, useRef } from 'react';

function VideocallRoom({ joinUrl }: { joinUrl: string }) {
  const callRef = useRef<DailyIframe | null>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const callFrame = DailyIframe.createFrame(containerRef.current, {
      iframeStyle: {
        width: '100%',
        height: '100%',
        border: 'none'
      },
      showLeaveButton: true,
      showFullscreenButton: true
    });

    callFrame.join({ url: joinUrl });
    callRef.current = callFrame;

    return () => {
      callFrame.destroy();
    };
  }, [joinUrl]);

  return (
    <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
  );
}

Join Flow

Specialist:

  1. Click "Join Videocall" button on appointment page
  2. Backend generates specialist token
  3. Frontend receives join URL with token
  4. Embed Daily.co iframe with URL

Patient:

  1. Click "Join Videocall" link in email or appointment page
  2. Backend generates patient token
  3. Frontend receives join URL with token
  4. Embed Daily.co iframe with URL

Recording

Cloud Recording

  • Controlled by specialist (host)
  • Stored in Daily.co cloud storage
  • Accessible via Daily.co dashboard
  • Can be downloaded as MP4

Webhook Events

Daily.co sends webhooks for recording events:

json
{
  "type": "recording.started",
  "room": "restartix-1-102",
  "startedBy": "Dr. Smith",
  "timestamp": "2025-01-20T10:05:00Z"
}

Handled events:

  • recording.started — Log to audit
  • recording.stopped — Log to audit
  • recording.ready-to-download — Notify specialist, store download URL

Error Handling

Room Creation Failure

Scenario: Daily.co API is down or rate limited

Fallback:

  1. Appointment still created with status upcoming
  2. videocall_room_name = NULL
  3. Background job retries room creation every 5 minutes
  4. Admin dashboard shows warning: "Videocall room pending"

Token Generation Failure

Scenario: Cannot generate join token

Fallback:

  1. Show error message: "Videocall temporarily unavailable"
  2. Provide manual room URL (user enters without token)
  3. Log error for monitoring

Room Deletion Failure

Scenario: Daily.co API unavailable when cancelling

Fallback:

  1. Appointment still marked cancelled
  2. Background job retries deletion
  3. Room expires naturally at ended_at time

Configuration

Daily.co API Key

Stored in organization_integrations table:

sql
INSERT INTO organization_integrations (organization_id, service, api_key_encrypted)
VALUES (1, 'daily', encrypt('sk_live_abc123...', '...'));

Encryption: AES-256-GCM (app-level)

Access: Admin-only via /v1/organizations/{id}/api-keys

Domain Configuration

Daily.co subdomain: restartix.daily.co

Configured in Daily.co dashboard:

  1. Create subdomain
  2. Set branding (logo, colors)
  3. Configure default room settings

Monitoring

Metrics to Track

  • Room creation success rate
  • Token generation success rate
  • Average room duration
  • Recordings created per day
  • Failed API calls (alerts)

Audit Log Events

EventDetails
videocall.room_createdAppointment ID, room name
videocall.room_deletedAppointment ID, reason (cancel/noshow)
videocall.recording_startedAppointment ID, started by
videocall.recording_stoppedAppointment ID, duration
videocall.token_generatedAppointment ID, role (specialist/patient)

Daily.co API Reference

Base URL: https://api.daily.co/v1

Authentication: Authorization: Bearer {API_KEY}

EndpointMethodPurpose
/roomsPOSTCreate room
/rooms/{name}GETGet room details
/rooms/{name}PATCHUpdate room config
/rooms/{name}DELETEDelete room
/meeting-tokensPOSTGenerate join token

Documentation: https://docs.daily.co/reference/rest-api


Telemetry Integration

Videocalls generate telemetry data through two channels:

Audit (Automatic)

All videocall actions are automatically captured by the audit middleware — no additional code needed:

Audit EventTrigger
videocall.room_createdRoom created during appointment onboarding
videocall.room_deletedRoom deleted on cancel/noshow
videocall.recording_startedSpecialist starts recording
videocall.recording_stoppedRecording ends
videocall.token_generatedJoin token generated (specialist or patient)

Error Reporting (Frontend)

When videocall initialization or connection fails, the frontend should report errors to Telemetry for support troubleshooting:

json
POST /v1/errors/report
{
  "error_type": "videocall_error",
  "error_code": "ROOM_JOIN_FAILED",
  "error_message": "Failed to connect to Daily.co room",
  "feature_name": "videocall",
  "stack_trace": "Error: WebSocket connection failed...",
  "response_time_ms": 30000
}

Error codes: ROOM_JOIN_FAILED, TOKEN_EXPIRED, MEDIA_DEVICE_ERROR, NETWORK_DISCONNECT, RECORDING_FAILED

Consent requirement: Level 1+ (legitimate interest for customer support)

Note: Videocalls are NOT tracked via POST /v1/media/events (which is for exercise video playback). Videocall quality metrics come from Daily.co's own analytics dashboard. The Telemetry media events system tracks pre-recorded exercise videos only.