---
title: Notification Support
slug: notification-support
docTags: 
createdAt: 2026-09-14T23:41:45.514Z
---

## Push Notifications in Handover

First provide the SDK with the Apple push notification token using:

```swift
KindlySDK.setAPNSDeviceToken(_ deviceToken: Data)
```

```swift
KindlySDK.setAPNSDeviceToken(_ deviceToken: String)
```

## When a Push Notification is Received

Forward the notification to SDK using

```swift
KindlySDK.notificationReceived(_ userInfo: [AnyHashable : Any])
```

```swift
KindlySDK.notificationReceived(_ notification: UNNotification)
```

The SDK will double-check if notification type is a Kindly notification before handling it.

## Check if a Notification is from Kindly

To determine if a notification should be handled by the Kindly SDK or another notification provider (like mParticle or Braze), use the following helper method:

```swift
let isKindlyNotification = KindlySDK.isKindlyNotification(_ userInfo: [AnyHashable : Any])
```

This can be particularly useful when integrating with multiple push notification providers. Example usage:

```swift
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    if KindlySDK.isKindlyNotification(userInfo) {
        // Handle with Kindly SDK
        KindlySDK.notificationReceived(userInfo)
    } else {
        // Handle with another notification provider (e.g., mParticle/Braze)
        otherNotificationProvider.handleNotification(userInfo)
    }

    completionHandler(.newData)
}
```

## When a Notification is Clicked

```swift
KindlySDK.notificationResponseReceived(_ response: UNNotificationResponse)
```

## When the SDK Displays the Notification

Kindly silent pushes are encrypted; the SDK decrypts each one and presents a local banner with the decrypted title/body — except when the user is already actively reading the chat conversation. The display rule is:

| App state  | Active screen                                             | SDK presents the banner?                    |
| ---------- | --------------------------------------------------------- | ------------------------------------------- |
| Background | any                                                       | ✅ Yes                                       |
| Foreground | Chat conversation                                         | ❌ No (the user can already see the message) |
| Foreground | Settings, language, image preview, host-app screens, etc. | ✅ Yes                                       |

:::BlockQuote
The SDK ships `KindlySDK.notificationDelegate`, which applies this rule; assign it to `UNUserNotificationCenter.current().delegate`. With your own delegate, call `KindlySDK.notificationWillPresent(_:completionHandler:)` from `willPresent` for Kindly notifications. With the Notification Service Extension below, killed and background states are handled by iOS itself with the decrypted text.
:::

## Intercepting Notifications (`shouldHandleNotification`)

Every Kindly silent push that the SDK successfully decrypts is forwarded to the host app via the `shouldHandleNotification(notification:)` delegate method — regardless of foreground/background state or which screen is on top. Use it to run side effects (analytics, logging, in-app indicators) or to take over the presentation entirely with your own UI.

| Scenario                                            | Result                                                            |
| --------------------------------------------------- | ----------------------------------------------------------------- |
| Delegate not set                                    | SDK presents the notification (subject to the display rule above) |
| `shouldHandleNotification` returns `true` (default) | SDK presents the notification (subject to the display rule above) |
| `shouldHandleNotification` returns `false`          | SDK does **not** present anything — your app handles it           |

The delegate receives an `ExternalNotification` containing the decrypted fields plus the original `userInfo` for any extra fields your backend attached:

```swift
public struct ExternalNotification {
    public let id: String                          // decrypted chat_id
    public let title: String                       // decrypted title
    public let body: String                        // decrypted body
    public let userInfo: [AnyHashable: Any]        // original APNs userInfo (aps, custom keys, …)
}
```

### Implementation

**1. Set the delegate:**

```swift
KindlySDK.delegate = self
```

**2. Conform to&#x20;**`KindlyChatClientDelegate`**:**

```swift
extension ViewController: KindlyChatClientDelegate {
    func shouldHandleNotification(notification: ExternalNotification) -> Bool {
        // Always log every Kindly notification, regardless of who presents it
        Analytics.track("kindly_notification_received", id: notification.id)

        // Example: take over presentation when the app is in a custom in-app inbox
        if isShowingCustomInbox {
            customInbox.append(notification)
            return false   // SDK does not present its banner
        }

        return true        // let the SDK present the banner
    }
}
```

### Notes

- The method has a default implementation that returns `true`, so you only need to implement it if you want to observe or intercept notifications.
- The callback fires for every successfully decrypted Kindly push — it is **not** gated by the SDK's foreground/chat-screen display rule. Use it for analytics that need to run even when the SDK is on screen.
- The polarity matches `shouldHandleLink`: returning `true` means "the SDK should handle this", returning `false` means "I'll handle it myself".
- Returning `false` only suppresses the local banner — the underlying message has already been delivered to the chat session via the websocket / `/latest` endpoint, so it will appear in the chat history when the user opens it.

## Notification Service Extension (recommended, SDK 3.0.10+)

With an extension in your app, Kindly sends a visible push (`aps.alert` + `mutable-content: 1`) instead of a silent one. iOS runs the extension before showing the banner, in every app state including when the app was killed, and the extension decrypts the Kindly payload so the banner shows the real title and text. Silent pushes are rate limited by iOS and never delivered to a force-quit app; visible pushes have neither problem.

The switch is per bot: once your app ships the extension, ask your Kindly contact to enable alert notifications for your bot. Kindly only sends the visible push to devices on SDK 3.0.10 or newer, so older app versions keep working exactly as before.

### Setup

1. File → New → Target → **Notification Service Extension**, embedded in your app. Replace the generated class with:

```swift
import Kindly

final class NotificationService: KindlyNotificationServiceExtension {}
```

2. Link `Kindly` into the extension target. The linker warning about extension safety is expected.
3. On both the app target and the extension target add the **Keychain Sharing** capability with the same group, your app's bundle id:

```xml
<key>keychain-access-groups</key>
<array>
    <string>$(AppIdentifierPrefix)ai.kindly.Example</string>
</array>
```

4. Tell Kindly the app version that ships the extension.

### What changes at runtime

| Push    | App state                            | User sees                                                     |
| ------- | ------------------------------------ | ------------------------------------------------------------- |
| visible | killed or background                 | one banner with the decrypted text, produced by the extension |
| visible | foreground, on the chat conversation | nothing                                                       |
| visible | foreground, elsewhere                | one banner with the decrypted text                            |
| silent  | any                                  | what it sees today                                            |

Without the extension a visible push shows the generic placeholder in the background and the SDK's own decrypted banner in the foreground. Ciphertext is never shown.

### Handling the tap

Tapping a Kindly banner opens the chat (`KindlySDK.openChatOnNotificationTap`, default `true`). A tap that launched the app opens the chat once `start()` has run. The remembered tap is replayed only when `start()` runs within 60 seconds of it; after that it is dropped, with a log line.

### Your own delegate

If you keep your own `UNUserNotificationCenterDelegate`, call `KindlySDK.notificationWillPresent(notification, completionHandler:)` for Kindly notifications from `willPresent` so the SDK's display rule applies.

### API

```swift
open class KindlyNotificationServiceExtension: UNNotificationServiceExtension {
    open func customize(_ content: UNMutableNotificationContent, payload: KindlyPushDecryption.DecryptedPayload?)
}

public enum KindlyPushDecryption {
    public struct DecryptedPayload { public let chatId: String; public let title: String; public let body: String }
    public static func isKindlyNotification(_ userInfo: [AnyHashable: Any]) -> Bool
    public static func decrypt(userInfo: [AnyHashable: Any]) -> DecryptedPayload?
    public static func apply(_ payload: DecryptedPayload, to content: UNMutableNotificationContent)
    public static func isDecrypted(_ userInfo: [AnyHashable: Any]) -> Bool
}
```

`decrypt` returns `nil` when the push is not Kindly's, a field is missing, the keychain has no key yet, or decryption fails; the extension then leaves the placeholder in place.

### See also

The [push notifications reference (PUSH.md)](https://kindly-ai.github.io/sdk-chat-ios-sources/PUSH.md) has the complete walkthrough, including hosts with an existing extension for another provider.
