# Push notifications

> Reach users after they leave the app. APNs on iOS, FCM on Android.

Source: https://conciergesdk.com/docs/push/

Every team or AI reply is pushed to each of the customer's devices that has a push token. Concierge never asks for notification permission; you do that, when it makes sense in your app, and forward the token.

## Upload your credentials

In the dashboard, open your app's **Push** settings:

- **iOS:** your APNs auth key (`.p8`), key ID and team ID. Concierge picks the sandbox or production gateway per device automatically.
- **Android:** a Firebase service account JSON for FCM HTTP v1.

Without credentials, local dev logs each push instead of sending it, so you can build without them.

## iOS

```swift
func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    Concierge.registerPushToken(token)   // sandbox or production is detected for you
}

// UNUserNotificationCenterDelegate on a @MainActor class. Don't mark these nonisolated.
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
    let userInfo = notification.request.content.userInfo
    if Concierge.handleNotification(userInfo, openChat: false) {
        return Concierge.shared.isChatVisible ? [] : [.banner, .sound]
    }
    return [.banner, .sound]
}

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
    Concierge.handleNotification(response.notification.request.content.userInfo)   // opens the chat
}
```

## Android

Concierge doesn't depend on Firebase. Forward from your own `FirebaseMessagingService`:

```kotlin
override fun onNewToken(token: String) = Concierge.registerPushToken(token)

override fun onMessageReceived(message: RemoteMessage) {
    if (Concierge.handleNotification(message.data)) {
        // Ours: show a notification that calls Concierge.open(context) when tapped.
        return
    }
    // Yours.
}
```

`handleNotification` returns true for Concierge payloads and updates the unread count.

## Payload

Every push carries the same data fields:

| Field | Value |
|---|---|
| `source` | `"concierge"` |
| `conversationId` | `"cnv_..."` |
| `messageId` | `"msg_..."` |
| `unread` | Messages this customer hasn't read yet |

**APNs.** The alert lives in `aps`: the title is the team member's name when **Show team names** is on, otherwise your app's name; the body is the message preview; `badge` is the unread count; `thread-id` is the conversation id. Data fields sit in a top-level `concierge` dictionary:

```json
{
  "aps": { "alert": { "title": "Kirill", "body": "Fixed in 2.4, update when you can" }, "badge": 1, "thread-id": "cnv_..." },
  "concierge": { "source": "concierge", "conversationId": "cnv_...", "messageId": "msg_...", "unread": 1 }
}
```

**FCM.** A `notification` block (title, body) plus a `data` map where every value is a string:

```json
{ "source": "concierge", "conversationId": "cnv_...", "messageId": "msg_...", "unread": "1", "title": "Kirill", "body": "Fixed in 2.4, update when you can" }
```

Tokens the gateway reports as invalid are cleared from the device automatically.
