---
# System prepended metadata

title: Backend Push Notification API Requirements

---

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