---
title: Step 5. Implementing the Push client for iOS
description: Explains how to use the Push SDK to handle push notifications in your iOS app.
component: orchsdks
page_id: orchsdks:journey:use-cases/push/ios/05_implement_push_client
canonical_url: https://developer.pingidentity.com/orchsdks/journey/use-cases/push/ios/05_implement_push_client.html
revdate: Tue, 21 Mar 2026 11:57:00 +0100
keywords: ["Push", "MFA", "Strong Auth", "Integration", "SDK", "mobile", "authentication", "notification", "iOS", "APNs", "Xcode"]
section_ids:
  adding_core_dependencies: Adding core dependencies
  initializing_the_push_client: Initializing the Push Client
  managing_push_credentials: Managing Push credentials
  creating_push_credentials: Creating Push credentials
  getting_push_credentials: Getting Push credentials
  updating_push_credentials: Updating Push credentials
  deleting_push_credentials: Deleting Push credentials
  updating_device_tokens: Updating device tokens
  responding_to_push_notifications: Responding to push notifications
  responding_to_tap_to_accept_notifications: Responding to tap to accept notifications
  responding_to_display_challenge_code_notifications: Responding to display challenge code notifications
  responding_to_biometrics_to_accept_notifications: Responding to biometrics to accept notifications
  managing_stored_notifications: Managing stored notifications
  getting_pending_notifications: Getting pending notifications
  cleanup: Cleaning up pending notifications
  closing_the_push_client: Closing the Push client
  handling_errors: Handling errors
  storage: Customizing credential storage
  customizing_the_default_keychain_based_storage: Customizing the default keychain-based storage
  implementing_your_own_storage_mechanism: Implementing your own storage mechanism
  handler: Customizing Push notification handlers
---

# Step 5. Implementing the Push client for iOS

[icon: circle-check, set=far]PingOne Advanced Identity Cloud [icon: circle-check, set=far]PingAM [icon: apple, set=fab]iOS

This page guides you through configuring your iOS application to support Push-based multi-factor authentication (MFA) using the **Push** module.

It covers dependency setup, Push client initialization, credential management, handling different push notification types, and custom storage options.

## Adding core dependencies

You can use Swift Package Manager (SPM) or CocoaPods to add dependencies to your iOS project.

* SPM

* CocoaPods

1. With your project open in **Xcode**, select File > Add Package Dependencies.

2. In the search bar, enter the Orchestration SDK for iOS repository URL: `https://github.com/ForgeRock/ping-ios-sdk`.

3. Select the `ping-ios-sdk` package, and then click Add Package.

4. In the Choose Package Products dialog, ensure that the `PingPush` library is added to your target project.

5. Click Add Package.

6. In your project, import the relevant dependency:

   ```swift
   import PingPush
   ```

1) If you do not already have CocoaPods, install the [latest version](https://guides.cocoapods.org/using/getting-started.html).

2) If you do not already have a Podfile, in a terminal window, run the following command to create a new [Podfile](https://guides.cocoapods.org/syntax/podfile.html):

   ```
   pod init
   ```

3) Add the `PingPush` dependency to your Podfile:

   ```
   pod 'PingPush'
   ```

4) Run the following command to install pods:

   ```
   pod install
   ```

## Initializing the Push Client

To use the **Push** module, you must initialize the Push client in your application by calling the `createClient()` method:

Initializing the Push client with default config

```swift
// Create a Push client
let pushClient = try await PushClient.createClient { config in
    config.logger = LogManager.logger
    config.enableCredentialCache = false
}
```

The properties you can use to customize the Push client configuration are as follows:

* *enableCredentialCache*

  Whether to enable in-memory caching of credentials.

  By default, this is set to `false` for security reasons, as an attacker could potentially access cached credentials from memory dumps.

* *timeout*

  The timeout for network operations, in seconds.

  Default value is `15`.

* *storage*

  The storage implementation to use for Push credentials.

  If `nil`, the default `PushKeychainStorage` is used.

  Learn more in [Customizing credential storage](#storage).

* *policyEvaluator*

  The policy evaluator to use for credential policy validation.

  If `nil`, the default `MfaPolicyEvaluator` is used.

* *customPushHandlers*

  A map of custom push handlers that the module will use alongside the default handlers.

  Learn more in [Customizing Push notification handlers](#handler).

* *notificationCleanupConfig*

  Configuration options for the automatic cleanup of push notifications.

  Learn more in [Cleaning up pending notifications](#cleanup).

* *logger*

  The logger instance used for logging messages.

  Defaults to a global logger instance.

  Learn more in [Logging](../../../customization/logging/index.html).

## Managing Push credentials

The **Push** module relies on a set of credentials that you can create, retrieve, update, and delete.

The credentials contain details such as the service and user they relate to, and the cryptographic keys needed to validate push notifications.

### Creating Push credentials

The Push module lets the user register their device for Push-based multi-factor authentication (MFA).

The information required to register a device is contained in a specially-encoded URI, which your client application decodes to create the credentials.

This URI is often delivered by QR codes that the client can scan or is provided directly in the callback from the [Push Registration node](https://docs.pingidentity.com/auth-node-ref/latest/push-registration.html).

Use the `addCredentialFromUri()` method to create Push credentials and register an MFA device:

Creating Push credentials

```swift
let uri = "pushauth://push/issuer:user@example.com?key=ABCDEFGHIJK&c=https://example.com/push"

let credential = try await pushClient.addCredentialFromUri(uri)
```

### Getting Push credentials

You can get a list of all registered Push credentials or get an individual credential by passing its ID as a parameter.

* All Push credentials

* Specific Push credential

Getting all Push credentials

```swift
do {
    let credentials = try await Task.detached(priority: .userInitiated) {
        try await pushClient.getCredentials()
    }.value
} catch {
    throw AppError.pushError("Failed to load credentials: \(error.localizedDescription)")
}
```

Getting a specific Push credential

```swift
let credential = try await Task.detached(priority: .userInitiated) {
    try await pushClient.getCredential(credentialId)
}.value
```

### Updating Push credentials

You can update the properties of a stored credential with new values by using the `saveCredential()` method. Pass the updated credential object into the method as a parameter:

Updating a Push credential

```swift
for credential in credentials {
    var updated = credential
    updated.displayIssuer = "Example.com Checking Account"
    updated.displayAccountName = "Babs Jensen"
    _ = try await pushClient.saveCredential(updated)
}
```

### Deleting Push credentials

Use the `deleteCredential()` method to remove individual credentials from the client device. Pass the credential ID into the method as a parameter:

Deleting a Push credential

```swift
do {
    let removed = try await Task.detached(priority: .userInitiated) {
        try await pushClient.deleteCredential(credentialId)
    }.value
    return removed
} catch {
    throw AppError.pushError("Failed to remove credential: \(error.localizedDescription)")
}
```

## Updating device tokens

Under certain circumstances, the client operating system issues a new device token that your app needs to use for receiving push notifications.

> **Collapse: What can cause the device token for push messages to change?**
>
> The device token used to receive push messages can change due to a number of circumstances:
>
> * Uninstalling and reinstalling the client app
>
>   If the user uninstalls and then reinstalls the app, the OS regenerates the device token.
>
>   This is one of the most common reasons for a token change.
>
> * Clearing app data
>
>   Clearing the application's data by using the device settings causes the OS to issue a new device token upon next launch of the app.
>
> * Revoking and regranting Push permission
>
>   The OS might issue a new device token if a user revokes and then re-enables push notifications.
>
> * Push services expiring or invalidating tokens
>
>   The push services themselves, such as the Apple Push Notification service (APNs) or Google's Firebase Cloud Messaging (FCM) service might invalidate device tokens for various reasons.
>
>   The OS issues a new device token upon next launch of the app if the push service invalidates the existing tokens.
>
> * Updating the operating system
>
>   Occasionally, OS updates, especially major versions, might result in the push notification service issuing a new token.
>
>   Updating the OS can also clear app data, which would also mean the app requires a new device token on next launch.
>
> * Updating or migrating apps
>
>   If you change the package or bundle IDs of your client app that uses push notification, or alter the signing keys, the OS might invalidate existing device tokens and issue a new one.
>
>   Similarly, if the user restores the app from backup, or migrates the app to a different device, the OS might issue a new device token, even if restoring the app to the same physical device.

The **Push** module provides methods for updating the device token associated with accounts it has registered to receive Push notifications. These methods also contact the server that registered the device to update its copy of the device token.

Failing to update the device token on both the client and the server will prevent push messages from arriving, which will cause authentication to fail.

|   |                                                                                                                      |
| - | -------------------------------------------------------------------------------------------------------------------- |
|   | Updating existing accounts with a new device token is only supported by the following server:- PingAM 8.0.1 or later |

Updating the APNs device token

```swift
// Update the device token when it changes
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    pushClient.setDeviceToken(deviceToken)
}

// Update the device token for a specific credential
pushClient.setDeviceToken(deviceToken, credentialId: credentialId)
```

## Responding to push notifications

When your client app receives a push notification from APNs, you need to process it using the `processNotification()` method:

Processing incoming push notifications

```swift
func userNotificationCenter(
  _ center: UNUserNotificationCenter,
  didReceive response: UNNotificationResponse,
  withCompletionHandler completionHandler: @escaping @Sendable () -> Void
) {
  let userInfo = response.notification.request.content.userInfo

  // Process the notification through PushClient
  nonisolated(unsafe) let userInfoCopy = userInfo
  Task {
    do {
      // Process the notification - PushClient automatically extracts APNs payload
      if let notification = try await pushClient.processNotification(userInfo: userInfoCopy) {

        // Show notification to user based on type
        if let notification = notification {
          switch notification.pushType {
            case .default:
              showDefaultNotification(notification)
            case .challenge:
              showChallengeNotification(notification)
            case .biometric:
              showBiometricPrompt(notification)
          }
        }
      }
      else {
        print("Notification was not processed (may be unsupported type)")
      }
    }
    catch { print("Failed to process push notification: \(error.localizedDescription)") }
  }
  completionHandler()
}
```

The **Push** module supports three push notification types depending on your use case and provides methods for handling each one.

### Responding to tap to accept notifications

The **Tap to accept** notification type displays an **Accept** and a **Reject** button for the user to choose how to proceed. This is the default notification type.

Call the `approveNotification()` method to approve the push notification, or `denyNotification()` to reject it:

Approving or denying tap to accept notifications

```swift
// To approve
try await pushClient.approveNotification(notificationId)

// To deny
try await pushClient.denyNotification(notificationId)
```

### Responding to display challenge code notifications

The **Display Number Challenge** notification type displays a number that the user must match on their device, either by entering the value or selecting it from multiple options.

Use the `approveChallengeNotification(challengeResponse:)` method to return the response to the server:

Approving or denying display challenge code notifications

```swift
// The user sees matching numbers on both login screen and mobile device
// They enter or select the challenge response
let challengeResponse = userSelectedResponse // e.g., "80"

try await pushClient.approveChallengeNotification(notificationId, challengeResponse: challengeResponse)
```

### Responding to biometrics to accept notifications

The **Use Biometrics to Accept** notification type initially displays the same **Accept** and a **Reject** buttons for the user to choose how to proceed.

If the user selects to accept the notification, the client device presents its biometric options for the user to authenticate with.

Use the `approveBiometricNotification(authenticationMethod:)` method to respond to the notification:

Approving or denying biometrics to accept notifications

```swift
// After successful biometric authentication
let authMethod = "fingerprint" // or "face", "iris", etc.

try await pushClient.approveBiometricNotification(notificationId, authenticationMethod: authMethod)
```

## Managing stored notifications

The **Push** module stores all notifications, including any that are still pending a response.

### Getting pending notifications

You can get a list of pending notifications that have not yet been approved or rejected by using the `getPendingNotifications()` method:

Getting all pending notifications

```swift
// Get all pending notifications
let notifications = try await pushClient.getPendingNotifications()
if !notifications.isEmpty {
    // Display pending notifications to the user
    displayPendingNotifications(notifications)
} else {
    // No pending notifications
    showEmptyState()
}
```

You can use the `getNotification()` method to get an individual pending notification by passing its ID as a parameter:

Getting an individual pending notification

```swift
// Get a specific notification by ID
let notification = try await pushClient.getNotification(notificationId)
if let notification = notification {
    // Display the notification details
    showNotificationDetails(notification)
} else {
    // Notification not found
    showNotFoundMessage()
}
```

### Cleaning up pending notifications

The **Push** module provides automatic cleanup of push notifications by using the `NotificationCleanupConfig` class.

This helps prevent your app from accumulating too many push notification records, therefore improving performance and reducing storage usage.

You can customize notification cleanup by passing parameters to `NotificationCleanupConfig`:

Automating cleanup of stored notifications

```swift
// Create a client with custom notification cleanup configuration
let pushClient = try await PushClient.createClient { config in
    // Configure notification cleanup
    config.notificationCleanupConfig = NotificationCleanupConfig(
        // Choose a cleanup mode: .none, .countBased, .ageBased, or .hybrid
        cleanupMode: .hybrid,
        // Maximum notifications to keep when using .countBased or .hybrid mode
        maxStoredNotifications: 50,
        // Maximum age in days for notifications when using .ageBased or .hybrid mode
        maxNotificationAgeDays: 14
    )
}
```

The properties you can provide are as follows:

* **cleanupMode**

  The strategy the module uses to clean up notifications.

  Choose from one of the following:

  * `.none`

    The module does not perform automatic cleanup of notifications.

  * `.countBased`

    The module keeps a maximum number of notifications and deletes the oldest first when the limit is exceeded.

    This is the default.

  * `.ageBased`

    The module deletes notifications that are older than the specified number of days.

  * `.hybrid`

    The module applies both count and age limits to the stored notifications.

* **maxStoredNotifications**

  The maximum number of notifications to keep, if using the `.countBased` mode.

  If the number is exceeded, the module deletes the oldest notifications first to reach the threshold.

  The default is `100`.

* **maxNotificationAgeDays**

  The maximum number of days to keep a notification before removing it.

  Defaults to `30`.

You can choose to trigger notification cleanup on-demand rather than automatically by using the `cleanupNotifications()` method:

Manually cleaning up stored notifications

```swift
// Clean up notifications for all credentials
let count = try await pushClient.cleanupNotifications()
print("Removed \(count) old notifications")

// Clean up notifications for a specific credential
let countForCredential = try await pushClient.cleanupNotifications(credentialId: credentialId)
print("Removed \(countForCredential) old notifications for credential \(credentialId)")
```

## Closing the Push client

You can close the client, clean up any temporary files, and regain the memory used by calling the `close()` method:

Closing a Push client

```swift
// Close the Push client and clean up
try await pushClient.close()
```

## Handling errors

The **Push** module provides comprehensive error handling:

Handling errors

```swift
do {
    let credential = try await pushClient.addCredentialFromUri(uri)
} catch let error as PushError {
    switch error {
    case .invalidUri(let message):
        print("Invalid URI: \(message)")
    case .credentialNotFound(let id):
        print("Credential not found: \(id)")
    case .credentialLocked(let id):
        print("Credential is locked: \(id)")
    case .initializationFailed(let message, let underlying):
        print("Client initialization failed: \(message)")
    }
} catch let error as PushStorageError {
    switch error {
    case .storageFailure(let message, let underlying):
        print("Storage error: \(message)")
    }
}
```

## Customizing credential storage

The **Push** module needs to store the credentials it uses on the client device.

By default, it uses an iOS keychain services-based implementation, which you can customize.

You can also provide your own storage mechanism by implementing the `PushStorage` interface.

### Customizing the default keychain-based storage

The **Push** module uses the `PushKeychainStorage` implementation for storing Push credentials by default.

You can customize this keychain-based default as follows:

Customizing the `PushKeychainStorage` implementation

```swift
// Create a custom storage instance with specific parameters
let customStorage = PushKeychainStorage(
  credentialService: String = "com.myapp.push.credentials",
  notificationService: String = "com.myapp.push.notifications",
  tokenService: String = "com.myapp.push.tokens",
  accessGroup: "group.myapp",
  accessibility: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
)

// Create the client with the custom storage
let pushClient = try await PushClient.createClient { config in
  config.storage = customStorage
  config.enableCredentialCache = false
}
```

The properties you can customize are as follows:

* `credentialService`

  The keychain service identifier to use for storing credentials.

  Defaults to `com.pingidentity.push.credentials`.

* `notificationService`

  The keychain service identifier to use for storing notifications.

  Defaults to com.pingidentity.push.notifications\`.

* `tokenService`

  The keychain service identifier to use for storing device tokens.

  Defaults to `com.pingidentity.push.tokens`.

* `accessGroup`

  Optional keychain access group for shared access.

* `accessibility`

  Keychain accessibility level.

  Defaults to `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`.

### Implementing your own storage mechanism

You can implement a custom storage solution as an alternative to the default `PushKeychainStorage` by implementing the `PushStorage` interface.

Push credential storage interface

```swift
public protocol PushStorage: Sendable {

    /// Store a push credential.
    /// - Parameter credential: The Push credential to be stored.
    /// - Throws: `PushStorageError.storageFailure` if the credential cannot be stored.
    /// - Throws: `PushStorageError.duplicateCredential` if a credential with the same ID already exists.
    func storePushCredential(_ credential: PushCredential) async throws

    /// Retrieve all stored push credentials.
    /// - Returns: A list of all Push credentials.
    /// - Throws: `PushStorageError.storageFailure` if the credentials cannot be retrieved.
    func getAllPushCredentials() async throws -> [PushCredential]

    /// Retrieve a specific push credential by ID.
    /// - Parameter credentialId: The ID of the credential to retrieve.
    /// - Returns: The Push credential, or nil if not found.
    /// - Throws: `PushStorageError.storageFailure` if the credential cannot be retrieved.
    func retrievePushCredential(credentialId: String) async throws -> PushCredential?

    /// Remove a push credential by its ID.
    /// - Parameter credentialId: The ID of the credential to remove.
    /// - Returns: true if the credential was successfully removed, false if it didn't exist.
    /// - Throws: `PushStorageError.storageFailure` if the credential cannot be removed.
    func removePushCredential(credentialId: String) async throws -> Bool

    /// Clear all Push credentials from the storage.
    /// - Throws: `PushStorageError.storageFailure` if the credentials cannot be cleared.
    func clearPushCredentials() async throws

    /// Retrieve a push credential by issuer and account name.
    /// Used for duplicate detection during credential registration.
    /// - Parameters:
    ///   - issuer: The issuer of the credential.
    ///   - accountName: The account name of the credential.
    /// - Returns: The Push credential if found, nil otherwise.
    /// - Throws: `PushStorageError.storageFailure` if the credential cannot be retrieved.
    func getCredentialByIssuerAndAccount(issuer: String, accountName: String) async throws -> PushCredential?

    /// Store a push notification.
    /// - Parameter notification: The Push notification to be stored.
    /// - Throws: `PushStorageError.storageFailure` if the notification cannot be stored.
    func storePushNotification(_ notification: PushNotification) async throws

    /// Update a push notification.
    /// - Parameter notification: The Push notification to update.
    /// - Throws: `PushStorageError.storageFailure` if the notification cannot be updated.
    func updatePushNotification(_ notification: PushNotification) async throws

    /// Retrieve all stored push notifications.
    /// - Returns: A list of all Push notifications.
    /// - Throws: `PushStorageError.storageFailure` if the notifications cannot be retrieved.
    func getAllPushNotifications() async throws -> [PushNotification]

    /// Retrieve all pending push notifications.
    /// - Returns: A list of pending Push notifications.
    /// - Throws: `PushStorageError.storageFailure` if the notifications cannot be retrieved.
    func getPendingPushNotifications() async throws -> [PushNotification]

    /// Retrieve a specific push notification by ID.
    /// - Parameter notificationId: The ID of the notification to retrieve.
    /// - Returns: The Push notification, or nil if not found.
    /// - Throws: `PushStorageError.storageFailure` if the notification cannot be retrieved.
    func retrievePushNotification(notificationId: String) async throws -> PushNotification?

    /// Retrieve a push notification by message ID.
    /// - Parameter messageId: The message ID of the notification to retrieve.
    /// - Returns: The Push notification, or nil if not found.
    /// - Throws: `PushStorageError.storageFailure` if the notification cannot be retrieved.
    func getNotificationByMessageId(messageId: String) async throws -> PushNotification?

    /// Remove a push notification by its ID.
    /// - Parameter notificationId: The ID of the notification to remove.
    /// - Returns: true if the notification was successfully removed, false if it didn't exist.
    /// - Throws: `PushStorageError.storageFailure` if the notification cannot be removed.
    func removePushNotification(notificationId: String) async throws -> Bool

    /// Remove all push notifications associated with a credential.
    /// - Parameter credentialId: The ID of the credential.
    /// - Returns: The number of notifications removed.
    /// - Throws: `PushStorageError.storageFailure` if the notifications cannot be removed.
    func removePushNotificationsForCredential(credentialId: String) async throws -> Int

    /// Clear all Push notifications from the storage.
    /// - Throws: `PushStorageError.storageFailure` if the notifications cannot be cleared.
    func clearPushNotifications() async throws

    /// Store a push device token.
    /// - Parameter token: The Push device token to be stored.
    /// - Throws: `PushStorageError.storageFailure` if the token cannot be stored.
    func storePushDeviceToken(_ token: PushDeviceToken) async throws

    /// Retrieve the current push device token.
    /// - Returns: The current Push device token, or nil if not found.
    /// - Throws: `PushStorageError.storageFailure` if the token cannot be retrieved.
    func getCurrentPushDeviceToken() async throws -> PushDeviceToken?

    /// Clear all Push device tokens from the storage.
    /// - Throws: `PushStorageError.storageFailure` if the tokens cannot be cleared.
    func clearPushDeviceTokens() async throws

    /// Count the number of push notifications.
    /// - Parameter credentialId: Optional ID of a specific credential to count notifications for.
    /// - Returns: The count of push notifications.
    /// - Throws: `PushStorageError.storageFailure` if the count cannot be retrieved.
    func countPushNotifications(credentialId: String?) async throws -> Int

    /// Retrieve the oldest push notifications.
    /// - Parameters:
    ///   - limit: The maximum number of notifications to retrieve.
    ///   - credentialId: Optional ID of a specific credential to retrieve notifications for.
    /// - Returns: A list of the oldest push notifications.
    /// - Throws: `PushStorageError.storageFailure` if the notifications cannot be retrieved.
    func getOldestPushNotifications(limit: Int, credentialId: String?) async throws -> [PushNotification]

    /// Purge push notifications by age.
    /// - Parameters:
    ///   - maxAgeDays: The maximum age in days for notifications to keep.
    ///   - credentialId: Optional ID of a specific credential to purge notifications for.
    /// - Returns: The number of notifications removed.
    /// - Throws: `PushStorageError.storageFailure` if the notifications cannot be purged.
    func purgePushNotificationsByAge(maxAgeDays: Int, credentialId: String?) async throws -> Int

    /// Purge push notifications by count (removes oldest notifications when count exceeds the limit).
    /// - Parameters:
    ///   - maxCount: The maximum number of notifications to keep.
    ///   - credentialId: Optional ID of a specific credential to purge notifications for.
    /// - Returns: The number of notifications removed.
    /// - Throws: `PushStorageError.storageFailure` if the notifications cannot be purged.
    func purgePushNotificationsByCount(maxCount: Int, credentialId: String?) async throws -> Int
}
```

## Customizing Push notification handlers

You can implement the `PushHandler` interface to support a custom push notification formats, if required.

Push handler interface

```swift
public protocol PushHandler: AnyObject, Sendable {

    /// Check if this handler can process the given message data.
    /// This method should inspect the map data (typically the push notification payload)
    /// to determine if it can be handled by this handler.
    ///
    /// - Parameter messageData: The message data as a map, usually received from UNNotification userInfo.
    /// - Returns: True if this handler can process the message data, false otherwise.
    func canHandle(messageData: [String: Any]) -> Bool

    /// Check if this handler can process the given message in string format.
    /// This method should inspect the message string to determine if it can be handled by this
    /// handler. It should return true if the handler can process the message, and false otherwise.
    ///
    /// - Parameter message: The message data as a string, typically a JWT or JSON string.
    /// - Returns: True if this handler can process the message, false otherwise.
    func canHandle(message: String) -> Bool

    /// Parse the message data received from the push service.
    /// It should extract relevant information such as notification type, message content, and any
    /// additional parameters. It should return a map of parsed data that maps to the expected
    /// structure for the PushNotification.
    ///
    /// - Parameter messageData: The message data to parse. Usually a map containing the raw data
    ///   from the push service. On iOS, this would typically come from UNNotificationRequest userInfo.
    /// - Returns: A map of parsed data.
    /// - Throws: `PushError.messageParsingFailed` if parsing fails.
    func parseMessage(messageData: [String: Any]) throws -> [String: Any]

    /// Parse the message received as a string.
    /// It should extract relevant information from the string message (typically a JWT or JSON string)
    /// and return a map of parsed data that maps to the expected structure for the PushNotification.
    ///
    /// - Parameter message: The message data as a string to parse.
    /// - Returns: A map of parsed data.
    /// - Throws: `PushError.messageParsingFailed` if parsing fails.
    func parseMessage(message: String) throws -> [String: Any]

    /// Send to the server an approval response for a notification that was received.
    /// How the approval is sent depends on the platform and the implementation of this handler.
    ///
    /// - Parameters:
    ///   - credential: The credential to use for the response.
    ///   - notification: The notification to approve.
    ///   - params: Additional parameters.
    /// - Returns: True if the approval was sent successfully, false otherwise.
    /// - Throws: `PushError.networkFailure` if the network request fails.
    func sendApproval(
        credential: PushCredential,
        notification: PushNotification,
        params: [String: Any]
    ) async throws -> Bool

    /// Send a denial response for a notification.
    /// How the denial is sent depends on the platform and the implementation of this handler.
    ///
    /// - Parameters:
    ///   - credential: The credential to use for the response.
    ///   - notification: The notification to deny.
    ///   - params: Additional parameters.
    /// - Returns: True if the denial was sent successfully, false otherwise.
    /// - Throws: `PushError.networkFailure` if the network request fails.
    func sendDenial(
        credential: PushCredential,
        notification: PushNotification,
        params: [String: Any]
    ) async throws -> Bool

    /// Register or update the device token.
    /// This is typically called when the device token changes.
    ///
    /// - Parameters:
    ///   - credential: The credential to register or update.
    ///   - deviceToken: The device token.
    ///   - params: Additional parameters.
    /// - Returns: True if was successful, false otherwise.
    /// - Throws: `PushError.networkFailure` if the network request fails.
    func setDeviceToken(
        credential: PushCredential,
        deviceToken: String,
        params: [String: Any]
    ) async throws -> Bool

    /// Register a new push credential with the server, if this is required by the platform.
    /// The parameters may include additional information or any other relevant data that the
    /// server needs to process the registration.
    ///
    /// - Parameters:
    ///   - credential: The credential to register.
    ///   - params: Additional parameters for registration including messageId.
    /// - Returns: True if the registration was successful, false otherwise.
    /// - Throws: `PushError.networkFailure` if the network request fails.
    func register(
        credential: PushCredential,
        params: [String: Any]
    ) async throws -> Bool
}
```

Register your custom handler when initializing the client:

Registering custom push handlers

```swift
let pushClient = try await PushClient.createClient { config in
    config.customPushHandlers = [CustomPushHandler()]
}
```
