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:
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:
{
"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:
exp := appointment.EndedAt.Unix() // UNIX timestampExample: If appointment ends at 2025-01-20T10:30:00Z, room expires at that exact time.
Update (Reschedule)
When: Appointment is rescheduled
API Call:
PATCH https://api.daily.co/v1/rooms/{roomName}
Authorization: Bearer {API_KEY}
Content-Type: application/json
{
"properties": {
"exp": 1706098200
}
}Logic:
- Calculate new
ended_atfrom rescheduled time - Update room expiration via Daily.co API
- Update appointment record
Deletion
When:
- Appointment cancelled
- Appointment marked no-show
- Admin hard-deletes appointment
API Call:
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:
- Check if room still exists (GET
/v1/rooms/{roomName}) - If exists and valid → keep it
- If not exists or expired → create new room
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:
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:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Join URL:
https://restartix.daily.co/restartix-1-102?t=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Token properties:
is_owner: true— Can start/stop recording, remove participantsenable_recording: "cloud"— Can record to Daily.co clouduser_name— Pre-filled display name- Token expires after appointment
ended_attime
Patient Join URL (with token)
Patients receive a meeting token with guest privileges:
API Call:
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 participantsuser_name— Pre-filled with patient name- Token expires after appointment
ended_attime
Room Configuration
Privacy Settings
- Privacy:
private— Room requires authentication (join via token only) - No public access — Cannot join without token
Features Enabled
| Feature | Enabled | Notes |
|---|---|---|
| Screen share | Yes | Both specialist and patient |
| Chat | Yes | Text chat during call |
| Knocking | Yes | Patient waits in lobby until specialist admits |
| Prejoin UI | Yes | Camera/mic test before joining |
| Recording | Yes (cloud) | Specialist can start/stop recording |
| Video on by default | Yes | |
| Audio on by default | Yes | |
| Owner-only broadcast | No | Both can speak |
| Max participants | 2 | One specialist, one patient |
API Integration (Go)
Service Interface
// 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
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
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
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:
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:
- Click "Join Videocall" button on appointment page
- Backend generates specialist token
- Frontend receives join URL with token
- Embed Daily.co iframe with URL
Patient:
- Click "Join Videocall" link in email or appointment page
- Backend generates patient token
- Frontend receives join URL with token
- 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:
{
"type": "recording.started",
"room": "restartix-1-102",
"startedBy": "Dr. Smith",
"timestamp": "2025-01-20T10:05:00Z"
}Handled events:
recording.started— Log to auditrecording.stopped— Log to auditrecording.ready-to-download— Notify specialist, store download URL
Error Handling
Room Creation Failure
Scenario: Daily.co API is down or rate limited
Fallback:
- Appointment still created with status
upcoming videocall_room_name= NULL- Background job retries room creation every 5 minutes
- Admin dashboard shows warning: "Videocall room pending"
Token Generation Failure
Scenario: Cannot generate join token
Fallback:
- Show error message: "Videocall temporarily unavailable"
- Provide manual room URL (user enters without token)
- Log error for monitoring
Room Deletion Failure
Scenario: Daily.co API unavailable when cancelling
Fallback:
- Appointment still marked
cancelled - Background job retries deletion
- Room expires naturally at
ended_attime
Configuration
Daily.co API Key
Stored in organization_integrations table:
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:
- Create subdomain
- Set branding (logo, colors)
- 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
| Event | Details |
|---|---|
videocall.room_created | Appointment ID, room name |
videocall.room_deleted | Appointment ID, reason (cancel/noshow) |
videocall.recording_started | Appointment ID, started by |
videocall.recording_stopped | Appointment ID, duration |
videocall.token_generated | Appointment ID, role (specialist/patient) |
Daily.co API Reference
Base URL: https://api.daily.co/v1
Authentication: Authorization: Bearer {API_KEY}
| Endpoint | Method | Purpose |
|---|---|---|
/rooms | POST | Create room |
/rooms/{name} | GET | Get room details |
/rooms/{name} | PATCH | Update room config |
/rooms/{name} | DELETE | Delete room |
/meeting-tokens | POST | Generate 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 Event | Trigger |
|---|---|
videocall.room_created | Room created during appointment onboarding |
videocall.room_deleted | Room deleted on cancel/noshow |
videocall.recording_started | Specialist starts recording |
videocall.recording_stopped | Recording ends |
videocall.token_generated | Join token generated (specialist or patient) |
Error Reporting (Frontend)
When videocall initialization or connection fails, the frontend should report errors to Telemetry for support troubleshooting:
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.
Related Documentation
- Lifecycle - Room creation/deletion triggers
- API Endpoints - Appointment videocall fields
- Schema - Appointment table structure