---
title: Step 5. Implementing the Push client for Android
description: Explains how to use the Push SDK to handle push notifications in your Android app.
component: orchsdks
page_id: orchsdks:journey:use-cases/push/android/05_implement_push_client
canonical_url: https://developer.pingidentity.com/orchsdks/journey/use-cases/push/android/05_implement_push_client.html
revdate: Mon, 23 Mar 2026 16:18:00 +0100
keywords: ["Push", "MFA", "Strong Auth", "Integration", "SDK", "mobile", "authentication", "notification", "Android", "FCM", "Android Studio"]
section_ids:
  adding_core_dependencies: Adding core dependencies
  initializing_the_push_client: Initializing the Push Client
  default_push_client_configuration: Default Push client configuration
  custom_push_client_configuration: Custom Push client Configuration
  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_sqlite_based_storage: Customizing the default SQLite-based storage
  implementing_your_own_storage_mechanism: Implementing your own storage mechanism
  handler: Customizing Push notification handlers
---

# Step 5. Implementing the Push client for Android

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

This page guides you through implementing the **Push** client in your Android application to support Push-based Multi-Factor Authentication (MFA).

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

## Adding core dependencies

To add the core dependencies for Push MFA:

1. In the **Project** tree view of your Android Studio project, open the `build.gradle.kts` file.

2. In the `dependencies` section, add the following:

   ```gradle
   implementation("com.pingidentity.sdks:mfa:push:2.0.0")

   // Firebase dependencies for push notifications
   implementation("com.google.firebase:firebase-messaging:25.0.1")
   ```

## Initializing the Push Client

To use the **Push** module, you must initialize the Push client in your application.

You can use the default Push client configuration or provide your own configuration using a DSL-style builder.

### Default Push client configuration

To use the default Push client configuration, call the `PushClient()` method with no additional parameters.

When using the default configuration, you also need to initialize the client by using the `initialize()` method:

Initializing the Push client with default config

```kotlin
val pushClient = PushClient()

// Initialize the client
pushClient.initialize()
```

### Custom Push client Configuration

To customize the Push client configuration, call the `PushClient()` method and pass the custom configuration as parameters.

When you provide a custom configuration, the `PushClient()` method also initializes the client, so you don't need to call `initialize()` manually:

Initializing the Push client with custom config

```kotlin
// Create with custom configuration using DSL-style builder
val pushClient = PushClient {
    enableCredentialCache = true
    timeout = 30
    // Any other configuration options
}
```

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.

  The default value is `15` seconds.

* *storage*

  The storage implementation to use for Push credentials.

  If `null`, the default `SQLPushStorage` is used.

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

* *policyEvaluator*

  The policy evaluator to use for credential policy validation.

  If `null`, 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 the 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:

* onSuccess

* getOrThrow

Creating Push credentials using onSuccess

```kotlin
// Obtain Push URI from journey callback or QR code
val uri = "pushauth://push/issuer:user@example.com?key=ABCDEFGHIJK&c=https://example.com/push"

// Create Push credentials using onSuccess
pushClient.addCredentialFromUri(uri).onSuccess { credential ->
    // Handle the successfully created credential
    println("Created credential: ${credential.issuer}")
}.onFailure { exception ->
    // Handle error
    println("Failed to add credential: ${exception.message}")
}
```

Creating Push credentials using getOrThrow

```kotlin
// Obtain Push URI from journey callback or QR code
val uri = "pushauth://push/issuer:user@example.com?key=ABCDEFGHIJK&c=https://example.com/push"

// Create Push credentials using getOrThrow
try {
    val credential = pushClient.addCredentialFromUri(uri).getOrThrow()
    // Use credential
} catch (e: Exception) {
    // Handle exception
}
```

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

```kotlin
pushClient.getCredentials().onSuccess { credentials ->
    if (credentials.isEmpty()) {
        showMessage("No credentials found")
    } else {
        displayCredentials(credentials)
    }
}
```

Getting a specific Push credential

```kotlin
pushClient.getCredential(credentialId).onSuccess { credential ->
    if (credential != null) {
        // Credential found, use it
        displayCredential(credential)
    } else {
        // Credential not found
        showMessage("Credential not found")
    }
}
```

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

```kotlin
// Change display properties
credential.displayAccountName = "Babs Jensen"
credential.displayIssuer = "Example.com Checking Account"

pushClient.saveCredential(credential).onSuccess { updatedCredential ->
    // Handle successful update
    showMessage("Credential updated")
}.onFailure { exception ->
    // Handle failure
    showError("Failed to update credential: ${exception.message}")
}
```

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

```kotlin
// Remove a credential by ID
pushClient.deleteCredential(credentialId).onSuccess { isDeleted ->
    if (isDeleted) {
        showMessage("Credential deleted")
    } else {
        showMessage("Credential not found")
    }
}.onFailure { exception ->
    showError("Failed to delete credential: ${exception.message}")
}
```

## 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 it's 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 Firebase device token

```kotlin
// Update the device token when it changes
val deviceToken = firebaseMessaging.getToken().await()
pushClient.setDeviceToken(deviceToken)

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

## Responding to push notifications

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

Processing incoming push notifications

```kotlin
// When a push notification is received from Firebase
val messageData = remoteMessage.data
val notification = pushClient.processNotification(messageData)

// Show notification to user based on type
if (notification != null) {
    when (notification.pushType) {
        PushType.DEFAULT -> showDefaultNotification(notification)
        PushType.CHALLENGE -> showChallengeNotification(notification)
        PushType.BIOMETRIC -> showBiometricPrompt(notification)
    }
}
```

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

```kotlin
// To approve
pushClient.approveNotification(notificationId)
    .onSuccess { success ->
        if (success) {
            // Notify user of successful authentication
        } else {
            // Handle failure
        }
    }
    .onFailure { error ->
        // Handle error
    }

// To deny
pushClient.denyNotification(notificationId)
    .onSuccess { success ->
        if (success) {
            // Notify user of successful denial
        } else {
            // Handle failure
        }
    }
    .onFailure { error ->
        // Handle error
    }
```

### 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()` method to return the response to the server:

Approving or denying display challenge code notifications

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

pushClient.approveChallengeNotification(notificationId, challengeResponse)
    .onSuccess { success ->
        if (success) {
            // Notify user of successful authentication
        } else {
            // Handle failure (possibly wrong challenge response)
        }
    }
    .onFailure { error ->
        // Handle error
    }
```

### 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()` method to respond to the notification:

Approving or denying biometrics to accept notifications

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

pushClient.approveBiometricNotification(notificationId, authMethod)
    .onSuccess { success ->
        if (success) {
            // Notify user of successful authentication
        } else {
            // Handle failure
        }
    }
    .onFailure { error ->
        // Handle error
    }
```

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

```kotlin
// Get all pending notifications
pushClient.getPendingNotifications().onSuccess { notifications ->
    if (notifications.isNotEmpty()) {
        // Display pending notifications to the user
        displayPendingNotifications(notifications)
    } else {
        // No pending notifications
        showEmptyState()
    }
}.onFailure { error ->
    // Handle error
    showError("Failed to retrieve notifications: ${error.message}")
}
```

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

Getting an individual pending notification

```kotlin
// Get a specific notification by ID
pushClient.getNotification(notificationId).onSuccess { notification ->
    if (notification != null) {
        // Display the notification details
        showNotificationDetails(notification)
    } else {
        // Notification not found
        showNotFoundMessage()
    }
}.onFailure { error ->
    // Handle error
    showError("Failed to retrieve notification: ${error.message}")
}
```

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

```kotlin
// Create a client with custom notification cleanup configuration
val pushClient = PushClient {
    // Configure notification cleanup
    notificationCleanupConfig = NotificationCleanupConfig {
        // Choose a cleanup mode: NONE, COUNT_BASED, AGE_BASED, or HYBRID
        cleanupMode = NotificationCleanupConfig.CleanupMode.HYBRID

        // Maximum notifications to keep when using COUNT_BASED or HYBRID mode
        maxStoredNotifications = 50

        // Maximum age in days for notifications when using AGE_BASED 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:

  * `CleanupMode.NONE`

    The module does not perform automatic cleanup of notifications.

  * `CleanupMode.COUNT_BASED`

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

    This is the default.

  * `CleanupMode.AGE_BASED`

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

  * `CleanupMode.HYBRID`

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

* **maxStoredNotifications**

  The maximum number of notifications to keep, if using the `COUNT_BASED` 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

```kotlin
// Clean up notifications for all credentials
pushClient.cleanupNotifications()
    .onSuccess { count ->
        println("Removed $count old notifications")
    }

// Clean up notifications for a specific credential
pushClient.cleanupNotifications(credentialId)
    .onSuccess { count ->
        println("Removed $count 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

```kotlin
// Close the Push client and clean up
pushClient.close()
```

## Handling errors

The **Push** module uses Kotlin's `Result` API for error handling, which provides a functional approach to error handling:

Handling errors using the Result API

```kotlin
// Using onSuccess/onFailure
pushClient.addCredentialFromUri(uri)
    .onSuccess { credential ->
        // Success path
    }
    .onFailure { exception ->
        when (exception) {
            is IllegalArgumentException -> showError() // Handle invalid URI format
            is MfaException -> showError() // Handle general MFA errors
            is NetworkException -> showError() // Handle network connectivity issues
            else -> showError() // Handle other exceptions
        }
    }

// Using fold for combined handling
pushClient.addCredentialFromUri(uri).fold(
    onSuccess = { credential ->
        // Handle success
    },
    onFailure = { exception ->
        // Handle failure
    }
)

// Using runCatching for additional operations
runCatching {
    pushClient.addCredentialFromUri(uri).getOrThrow()
}.onSuccess { credential ->
    // Do something with credential
}.onFailure { exception ->
    // Handle error
}
```

## Customizing credential storage

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

By default, it uses an SQLite-based implementation, which you can customize.

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

### Customizing the default SQLite-based storage

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

You can customize this SQLite-based default as follows:

Customizing the `SQLPushStorage` implementation

```kotlin
// Create a custom storage instance with specific parameters
val customStorage = SQLPushStorage {
    context = applicationContext
    databaseName = "my_custom_push_db.db"
    passphraseProvider = NonePassphraseProvider()
}

// Create the client with the custom storage
val pushClient = PushClient {
    storage = customStorage
    enableCredentialCache = true
}
```

The properties you can customize are as follows:

* `context`

  The Android application context is a required property.

* `databaseName`

  Optionally, rename the SQLite database.

* `databaseVersion`

  Customize the database version.

  The default is `1`.

* `passphraseProvider`

  Specify a custom passphrase provider for encrypting the SQLite database.

  * Use `KeyStorePassphraseProvider()` for encrypted storage using the Android KeyStore

  * Use `NonePassphraseProvider()` when you do not require the SQLite database to be encrypted.

  We recommend you encrypt credentials whenever possible.

### Implementing your own storage mechanism

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

Implementing custom Push credential storage

```kotlin
class MyCustomStorage : PushStorage {
    override suspend fun storePushCredential(credential: PushCredential) {
        // Implement storing the credential
    }

    override suspend fun retrievePushCredential(credentialId: String): PushCredential? {
        // Implement retrieving a credential by ID
        return null
    }

    override suspend fun removePushCredential(credentialId: String): Boolean {
        // Implement deleting a credential by ID
        return true
    }

    override suspend fun getAllPushCredentials(): List<PushCredential> {
        // Implement listing all stored credentials
        return emptyList()
    }

    override suspend fun storePushNotification(notification: PushNotification) {
        // Implement storing a notification
    }

    override suspend fun retrievePushNotification(notificationId: String): PushNotification? {
        // Implement retrieving a notification by ID
        return null
    }

    override suspend fun removePushNotification(notificationId: String): Boolean {
        // Implement deleting a notification by ID
        return true
    }

}
```

## Customizing Push notification handlers

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

Implementing a custom Push handler

```kotlin
class CustomPushHandler : PushHandler {
    override fun canHandle(messageData: Map<String, String>): Boolean {
        // Check if this handler can process the given payload
        return payload.keys.contains("custom_push_key")
    }

    override fun parseMessage(messageData: Map<String, Any>): Map<String, Any> {
        // Process the push payload and create a PushNotification
        val notificationId = payload["message_id"] ?: return null
        val issuer = payload["issuer"] ?: "Unknown"
        val message = payload["message"] ?: "Authentication request"

        // Populate and return a map associated with PushNotification fields
        return mapOf(
            "messageId" to notificationId,
            "message" to message,
            "issuer" to issuer,
            "pushType" to PushType.DEFAULT // or CHALLENGE, BIOMETRIC based on payload
        )
    }

    override fun sendApproval(
        credential: PushCredential,
        notification: PushNotification,
        params: Map<String, Any>
    ): Boolean {
        // Implement the approval logic
        return runCatching {
            // Make network request to approve the authentication
            // ...
            true
        }
    }

    override suspend fun sendDenial(
        credential: PushCredential,
        notification: PushNotification,
        params: Map<String, Any>
    ): Boolean {
        // Implement the denial logic
        return runCatching {
            // Make network request to deny the authentication
            // ...
            true
        }
    }

    override suspend fun setDeviceToken(
        credential: PushCredential,
        deviceToken: String,
        params: Map<String, Any>
    ): Boolean {
        // Implement the logic to set the device token for the credential
        return runCatching {
            // Make network request to register the device token
            // ...
            true
        }
    }

    override suspend fun register(
        credential: PushCredential,
        params: Map<String, Any>,
    ): Boolean {
        // Implement the registration logic
        return runCatching {
            // Make network request to register the credential
            // ...
            true
        }
    }
}

// Register your custom handler when initializing the PushClient
val pushClient = PushClient {
    customHandlers = listOf(CustomPushHandler())
}
```
