# 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>