# Backend Push Notification API Requirements This document outlines the backend API endpoints that need to be implemented to support push notifications for LiveKit 1-to-1 audio calls in the Cynoia mobile app. ## Overview The mobile app expects specific API endpoints to send push notifications when calls are initiated, declined, or ended. These endpoints should integrate with OneSignal to deliver notifications to mobile devices. ## Required API Endpoints ### 1. Send Call Notification **Endpoint**: `POST /api/v1/users/{calleeId}/call-notification` **Purpose**: Send a push notification to inform a user about an incoming call. **Path Parameters**: - `calleeId` (string, required): The ID of the user receiving the call **Request Body**: ```json { "callerName": "John Doe", "callerAvatar": "https://cdn.example.com/avatars/john-doe.jpg", "roomId": "room-uuid-12345", "callType": "audio" } ``` **Request Body Schema**: ```typescript interface SendCallNotificationRequest { callerName: string; // Display name of the caller callerAvatar?: string; // Optional URL to caller's avatar image roomId: string; // LiveKit room ID for the call callType: "audio" | "video"; // Type of call (currently only "audio" supported) } ``` **Response**: ```json { "success": true, "message": "Call notification sent successfully" } ``` **Error Responses**: ```json // User not found { "success": false, "message": "User not found", "statusCode": 404 } // User has notifications disabled { "success": false, "message": "User has push notifications disabled", "statusCode": 403 } // OneSignal delivery failed { "success": false, "message": "Failed to deliver notification", "statusCode": 500 } ``` ### 2. Send Call End Notification **Endpoint**: `POST /api/v1/users/{calleeId}/call-end-notification` **Purpose**: Send a push notification when a call is ended, declined, or cancelled. **Path Parameters**: - `calleeId` (string, required): The ID of the user to notify about the call ending **Request Body**: ```json { "roomId": "room-uuid-12345", "reason": "declined" } ``` **Request Body Schema**: ```typescript interface SendCallEndNotificationRequest { roomId: string; // LiveKit room ID for the call reason: "cancelled" | "declined" | "ended"; // Reason for call ending } ``` **Response**: ```json { "success": true, "message": "Call end notification sent successfully" } ``` ### 3. Send Call Accept Notification **Endpoint**: `POST /api/v1/users/{callerId}/call-accept-notification` **Purpose**: Send a push notification to inform the caller that their call has been accepted. **Path Parameters**: - `callerId` (string, required): The ID of the user who initiated the call **Request Body**: ```json { "roomId": "room-uuid-12345", "accepterName": "Jane Smith", "accepterAvatar": "https://cdn.example.com/avatars/jane-smith.jpg" } ``` **Request Body Schema**: ```typescript interface SendCallAcceptNotificationRequest { roomId: string; // LiveKit room ID for the call accepterName: string; // Display name of the user who accepted accepterAvatar?: string; // Optional URL to accepter's avatar image } ``` **Response**: ```json { "success": true, "message": "Call accept notification sent successfully" } ``` ## OneSignal Payload Structure The backend should send these specific payloads to OneSignal to ensure proper mobile app handling. ### Incoming Call Notification Payload ```json { "app_id": "your-onesignal-app-id", "include_external_user_ids": ["user-123"], "headings": { "en": "Incoming Call" }, "contents": { "en": "John Doe is calling you" }, "data": { "type": "incoming_call", "callerId": "caller-user-id", "callerName": "John Doe", "callerAvatar": "https://cdn.example.com/avatars/john-doe.jpg", "roomId": "room-uuid-12345", "callType": "audio", "timestamp": 1640995200000 }, "priority": 10, "ttl": 30, "android_channel_id": "call_notifications", "ios_sound": "call_sound.wav", "ios_category": "call" } ``` ### Call End Notification Payload ```json { "app_id": "your-onesignal-app-id", "include_external_user_ids": ["user-123"], "headings": { "en": "Call Ended" }, "contents": { "en": "Call was declined" }, "data": { "type": "call_ended", "roomId": "room-uuid-12345", "reason": "declined" }, "priority": 10, "ttl": 15 } ``` ### Call Accept Notification Payload ```json { "app_id": "your-onesignal-app-id", "include_external_user_ids": ["caller-user-id"], "headings": { "en": "Call Accepted" }, "contents": { "en": "Jane Smith accepted your call" }, "data": { "type": "call_accepted", "roomId": "room-uuid-12345", "accepterId": "accepter-user-id", "accepterName": "Jane Smith", "accepterAvatar": "https://cdn.example.com/avatars/jane-smith.jpg", "timestamp": 1640995200000 }, "priority": 10, "ttl": 15 } ``` ## Implementation Requirements ### Authentication & Authorization - All endpoints require valid JWT authentication - User must be authenticated and have valid session - Rate limiting should be applied to prevent spam calls ### User Device Management - Only send notifications to users who have: - Push notifications enabled in their settings - Active OneSignal device registration - Valid OneSignal player ID ### Error Handling - Return appropriate HTTP status codes - Include meaningful error messages - Log notification delivery status for debugging ### Database Considerations **Recommended data to store**: ```sql -- Call notifications log table CREATE TABLE call_notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), caller_id UUID NOT NULL REFERENCES users(id), callee_id UUID NOT NULL REFERENCES users(id), room_id VARCHAR(255) NOT NULL, notification_type VARCHAR(50) NOT NULL, -- 'incoming_call', 'call_ended', 'call_accepted' status VARCHAR(50) NOT NULL, -- 'sent', 'delivered', 'failed' reason VARCHAR(50), -- For call_ended notifications onesignal_notification_id VARCHAR(255), created_at TIMESTAMP DEFAULT NOW(), delivered_at TIMESTAMP ); ``` ## ๐Ÿ”ง Backend Implementation Example (Node.js/Express) ### Send Call Notification Endpoint ```javascript app.post('/api/v1/users/:calleeId/call-notification', authenticateUser, async (req, res) => { try { const { calleeId } = req.params; const { callerName, callerAvatar, roomId, callType } = req.body; const callerId = req.user.id; // Validate request if (!callerName || !roomId || !callType) { return res.status(400).json({ success: false, message: 'Missing required fields' }); } // Get callee user and check notification settings const callee = await getUserById(calleeId); if (!callee) { return res.status(404).json({ success: false, message: 'User not found' }); } if (!callee.pushNotificationsEnabled) { return res.status(403).json({ success: false, message: 'User has push notifications disabled' }); } // Get user's OneSignal player ID const device = await getUserDevice(calleeId); if (!device?.onesignalPlayerId) { return res.status(404).json({ success: false, message: 'User device not found' }); } // Send OneSignal notification const notificationPayload = { app_id: process.env.ONESIGNAL_APP_ID, include_external_user_ids: [calleeId], headings: { en: 'Incoming Call' }, contents: { en: `${callerName} is calling you` }, data: { type: 'incoming_call', callerId, callerName, callerAvatar, roomId, callType, timestamp: Date.now() }, priority: 10, ttl: 30, android_channel_id: 'call_notifications', ios_sound: 'call_sound.wav', ios_category: 'call' }; const oneSignalResponse = await sendOneSignalNotification(notificationPayload); // Log notification in database await logCallNotification({ callerId, calleeId, roomId, notificationType: 'incoming_call', status: oneSignalResponse.success ? 'sent' : 'failed', onesignalNotificationId: oneSignalResponse.id }); res.json({ success: true, message: 'Call notification sent successfully' }); } catch (error) { console.error('Error sending call notification:', error); res.status(500).json({ success: false, message: 'Failed to send notification' }); } }); ``` ### Send Call End Notification Endpoint ```javascript app.post('/api/v1/users/:calleeId/call-end-notification', authenticateUser, async (req, res) => { try { const { calleeId } = req.params; const { roomId, reason } = req.body; const callerId = req.user.id; // Validate request if (!roomId || !reason) { return res.status(400).json({ success: false, message: 'Missing required fields' }); } // Get callee user const callee = await getUserById(calleeId); if (!callee) { return res.status(404).json({ success: false, message: 'User not found' }); } // Send OneSignal notification const notificationPayload = { app_id: process.env.ONESIGNAL_APP_ID, include_external_user_ids: [calleeId], headings: { en: 'Call Ended' }, contents: { en: getCallEndMessage(reason) }, data: { type: 'call_ended', roomId, reason }, priority: 10, ttl: 15 }; const oneSignalResponse = await sendOneSignalNotification(notificationPayload); // Log notification in database await logCallNotification({ callerId, calleeId, roomId, notificationType: 'call_ended', status: oneSignalResponse.success ? 'sent' : 'failed', reason, onesignalNotificationId: oneSignalResponse.id }); res.json({ success: true, message: 'Call end notification sent successfully' }); } catch (error) { console.error('Error sending call end notification:', error); res.status(500).json({ success: false, message: 'Failed to send notification' }); } }); function getCallEndMessage(reason) { switch (reason) { case 'declined': return 'Call was declined'; case 'cancelled': return 'Call was cancelled'; case 'ended': return 'Call ended'; default: return 'Call ended'; } } ``` ### Send Call Accept Notification Endpoint ```javascript app.post('/api/v1/users/:callerId/call-accept-notification', authenticateUser, async (req, res) => { try { const { callerId } = req.params; const { roomId, accepterName, accepterAvatar } = req.body; const accepterId = req.user.id; // Validate request if (!roomId || !accepterName) { return res.status(400).json({ success: false, message: 'Missing required fields' }); } // Get caller user const caller = await getUserById(callerId); if (!caller) { return res.status(404).json({ success: false, message: 'Caller not found' }); } // Send OneSignal notification const notificationPayload = { app_id: process.env.ONESIGNAL_APP_ID, include_external_user_ids: [callerId], headings: { en: 'Call Accepted' }, contents: { en: `${accepterName} accepted your call` }, data: { type: 'call_accepted', roomId, accepterId, accepterName, accepterAvatar, timestamp: Date.now() }, priority: 10, ttl: 15 }; const oneSignalResponse = await sendOneSignalNotification(notificationPayload); // Log notification in database await logCallNotification({ callerId, calleeId: accepterId, roomId, notificationType: 'call_accepted', status: oneSignalResponse.success ? 'sent' : 'failed', onesignalNotificationId: oneSignalResponse.id }); res.json({ success: true, message: 'Call accept notification sent successfully' }); } catch (error) { console.error('Error sending call accept notification:', error); res.status(500).json({ success: false, message: 'Failed to send notification' }); } }); ``` ## ๐Ÿ”ง OneSignal Helper Function ```javascript async function sendOneSignalNotification(payload) { try { const response = await fetch('https://onesignal.com/api/v1/notifications', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${process.env.ONESIGNAL_REST_API_KEY}` }, body: JSON.stringify(payload) }); const data = await response.json(); if (response.ok) { return { success: true, id: data.id, recipients: data.recipients }; } else { console.error('OneSignal error:', data); return { success: false, error: data.errors || 'Unknown error' }; } } catch (error) { console.error('OneSignal request failed:', error); return { success: false, error: error.message }; } } ``` ## ๐Ÿงช Testing the Implementation ### Test Call Notification ```bash curl -X POST "https://api.cynoia.app/api/v1/users/123/call-notification" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "callerName": "John Doe", "callerAvatar": "https://example.com/avatar.jpg", "roomId": "test-room-123", "callType": "audio" }' ``` ### Test Call End Notification ```bash curl -X POST "https://api.cynoia.app/api/v1/users/123/call-end-notification" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "roomId": "test-room-123", "reason": "declined" }' ``` ## ๐Ÿ” Monitoring & Analytics ### Recommended Metrics to Track - Notification delivery success rate - Call notification response time - Call acceptance rate from notifications - Failed notification reasons - Device registration status ### Logging ```javascript // Log all notification events logger.info('Call notification sent', { callerId, calleeId, roomId, notificationType: 'incoming_call', deliveryStatus: 'sent', timestamp: new Date().toISOString() }); ``` ## ๐Ÿšจ Error Handling Best Practices 1. **Graceful Degradation**: If notifications fail, don't fail the call creation 2. **Retry Logic**: Implement retry for transient OneSignal failures 3. **Rate Limiting**: Prevent spam calls with rate limiting 4. **User Preferences**: Respect user notification settings 5. **Device Status**: Check if user's device is active/registered ## ๐Ÿ” Security Considerations 1. **Authentication**: Verify caller is authenticated 2. **Authorization**: Ensure caller can contact the callee 3. **Rate Limiting**: Prevent notification spam 4. **Input Validation**: Sanitize all input data 5. **Audit Logging**: Log all notification activities This implementation will provide a robust foundation for call notifications in the Cynoia mobile app, ensuring users receive timely notifications for incoming calls while maintaining security and reliability. ## ๏ฟฝ Sequence Diagrams ### 1. Incoming Call Notification Flow ```mermaid sequenceDiagram participant CA as Caller App participant BE as Backend API participant DB as Database participant OS as OneSignal participant CE as Callee App CA->>BE: POST /api/v1/users/{calleeId}/call-notification Note over CA,BE: { callerName, callerAvatar, roomId, callType } BE->>DB: Check callee user exists DB-->>BE: User data BE->>DB: Check push notification settings DB-->>BE: push_enabled: true BE->>DB: Get OneSignal player_id DB-->>BE: onesignal_player_id BE->>OS: Send notification payload Note over BE,OS: { type: "incoming_call", roomId, callerData } OS-->>BE: Notification sent successfully BE->>DB: Log notification event BE-->>CA: { success: true, message: "sent" } OS->>CE: Push notification delivered CE->>CE: Handle incoming call notification CE->>CE: Navigate to AudioMeet screen CE->>CE: Show incoming call UI ``` ### 2. Call End Notification Flow ```mermaid sequenceDiagram participant CA as Caller App participant BE as Backend API participant DB as Database participant OS as OneSignal participant CE as Callee App Note over CA: User declines/ends call CA->>BE: POST /api/v1/users/{calleeId}/call-end-notification Note over CA,BE: { roomId, reason: "declined" } BE->>DB: Validate callee user DB-->>BE: User exists BE->>OS: Send call end notification Note over BE,OS: { type: "call_ended", roomId, reason } OS-->>BE: Notification sent BE->>DB: Log call end event BE-->>CA: { success: true } OS->>CE: Push notification delivered CE->>CE: Handle call end notification CE->>CE: End current call (if matching roomId) CE->>CE: Update UI state ``` ### 3. Complete Call Flow with Error Handling ```mermaid sequenceDiagram participant CA as Caller App participant BE as Backend API participant DB as Database participant OS as OneSignal participant CE as Callee App Note over CA,CE: Complete call lifecycle with notifications CA->>BE: Initiate call notification alt User has push notifications disabled BE->>DB: Check push settings DB-->>BE: push_enabled: false BE-->>CA: 403 - Push notifications disabled else User not found BE->>DB: Find user DB-->>BE: User not found BE-->>CA: 404 - User not found else Success path BE->>DB: Get OneSignal player_id DB-->>BE: Valid player_id BE->>OS: Send incoming call notification alt OneSignal delivery fails OS-->>BE: Delivery failed BE->>DB: Log failed notification BE-->>CA: 500 - Notification failed else OneSignal success OS-->>BE: Notification sent BE->>DB: Log successful notification BE-->>CA: 200 - Success OS->>CE: Deliver push notification CE->>CE: Process incoming call Note over CE: User can accept or decline alt User declines call CE->>CA: Decline call (via LiveKit) CA->>BE: Send call end notification BE->>OS: Call ended notification OS->>CE: Deliver call end notification CE->>CE: Clean up call state else User accepts call CE->>CA: Accept call (via LiveKit) Note over CA,CE: LiveKit audio call proceeds Note over CA,CE: Call ends naturally CA->>BE: Send call end notification BE->>OS: Call ended notification OS->>CE: Deliver call end notification CE->>CE: Clean up call state end end end ``` ### 4. Device Registration and Organization Context ```mermaid sequenceDiagram participant MA as Mobile App participant BE as Backend API participant DB as Database participant OS as OneSignal Note over MA: User logs in MA->>OS: Initialize OneSignal OS-->>MA: Generate player_id MA->>BE: POST /api/v1/users/{userId}/devices Note over MA,BE: { onesignal_player_id, device_type } BE->>DB: Find existing device record DB-->>BE: Device found/not found alt Device exists BE->>DB: Update player_id BE->>OS: Update player tags Note over BE,OS: { user_id, organization_id, push_enabled } else New device BE->>DB: Create device record BE->>OS: Register player with tags end BE-->>MA: { push_enabled: true } Note over MA,BE: Now ready to receive notifications Note over MA: When call notification arrives OS->>MA: Push notification Note over OS,MA: Filtered by organization_id tag MA->>MA: Process call notification MA->>MA: Navigate to call screen ``` ## ๏ฟฝ๐Ÿ“ฑ Mobile App Call Notification Handler The mobile app includes a call notification handler that processes the push notifications sent by the backend. Here's the implementation for reference: ### Call Notification Handler (`callNotificationHandler.ts`) ```typescript /** * Call Notification Handler * * Handles incoming call notifications and call-related push notifications */ import { audioCallService } from "@/services/call/audio-call-service" import { CallNotificationData } from "@/services/api/notification/callNotificationTypes" import { navigationRef } from "@/navigators/navigationUtilities" export interface CallEndNotificationData { type: "call_ended" roomId: string reason: "cancelled" | "declined" | "ended" } /** * Handle incoming call notification */ export function handleIncomingCallNotification(callData: CallNotificationData) { try { console.log("๐Ÿ“ž Handling incoming call notification:", callData) // Check if we're already in a call if (audioCallService.isInCall()) { console.warn("โš ๏ธ Already in a call, ignoring incoming call notification") return } // Set up the incoming call in the audio service audioCallService.receiveIncomingCall( callData.roomId, callData.callerId, callData.callerName, callData.callerAvatar ) // Navigate to the call screen if (navigationRef.isReady()) { navigationRef.navigate("MainApp", { screen: "Chats", params: { screen: "AudioMeet", params: {}, }, } as any) } } catch (error) { console.error("โŒ Error handling incoming call notification:", error) } } /** * Handle call end notification */ export function handleCallEndNotification(callEndData: CallEndNotificationData) { try { console.log("๐Ÿ“ž Handling call end notification:", callEndData) // Check if this call end notification is for the current call const currentCall = audioCallService.getCurrentCallState() if (currentCall.callId === callEndData.roomId) { console.log("๐Ÿ“ž Call end notification is for current call - ending call") // End the call based on the reason switch (callEndData.reason) { case "declined": case "cancelled": audioCallService.endCallAsMissed() break case "ended": audioCallService.endCall() break default: audioCallService.endCall() break } } else { console.log("๐Ÿ“ž Call end notification is not for current call - ignoring") } } catch (error) { console.error("โŒ Error handling call end notification:", error) } } /** * Main notification handler for all call-related notifications */ export function handleCallNotification(notificationData: any) { try { const additionalData = notificationData.additionalData as any if (!additionalData || !additionalData.type) { return } switch (additionalData.type) { case "incoming_call": handleIncomingCallNotification(additionalData as CallNotificationData) break case "call_ended": handleCallEndNotification(additionalData as CallEndNotificationData) break default: console.log("๐Ÿ“ž Unknown call notification type:", additionalData.type) break } } catch (error) { console.error("โŒ Error handling call notification:", error) } } ``` ### How the Mobile App Processes Notifications 1. **OneSignal Integration**: The `AppNavigator.tsx` sets up OneSignal handlers that call `handleCallNotification()` 2. **Notification Routing**: The main handler routes notifications based on the `type` field in `additionalData` 3. **Incoming Calls**: Sets up the call state and navigates to the AudioMeet screen 4. **Call End Events**: Properly ends the current call based on the notification reason 5. **State Management**: Integrates with the existing `audioCallService` for consistent state management This shows exactly how the mobile app expects to receive and process the notification data that the backend APIs should send. <style> section#notes-recommendations, section#notes-recommendations hr { display: none !important; } </style>