---
title: Integrating PingOne Recognize on iOS
description: Integrate PingOne Recognize biometric enrollment and authentication into iOS apps using the Orchestration SDK with PingOne Advanced Identity Cloud journeys
component: orchsdks
page_id: orchsdks:journey:use-cases/recognize/ios/index
canonical_url: https://developer.pingidentity.com/orchsdks/journey/use-cases/recognize/ios/index.html
llms_txt: https://developer.pingidentity.com/orchsdks/llms.txt
docs_for_agents: https://developer.pingidentity.com/build-with-ai/docs-for-agents.md
revdate: Fri, 11 Sep 2026 14:00:00 +0000
keywords: ["PingOne Recognize", "Biometric", "Face", "Recognition", "Journey", "Integration", "SDK", "iOS"]
section_ids:
  keyless-registry: Step 1. Preparing the project to download the dependency
  install: Step 2. Installing the module
  install-cocoapods: Add the dependency using CocoaPods
  install-spm: Add the dependency using Swift Package Manager
  enroll: Step 3. Handling the enroll callback
  authenticate: Step 4. Handling the authenticate callback
  error_handling: Error handling
---

# Integrating PingOne Recognize on iOS

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

The PingOne Recognize module abstracts away the underlying biometric enrollment and authentication protocols, so you can add strong, passwordless authentication to your app with minimal code.

During a journey, the server returns a single callback, `PingOneRecognizeCallback`, and tells your app which ceremony to run using the `operationType` output field:

* `ENROLL`: perform a biometric enrollment ceremony.

* `AUTHENTICATE`: perform a biometric authentication ceremony.

The Orchestration SDK reads the `operationType` and instantiates the matching typed callback, so the callback your app receives in the node's callback list is not the raw `PingOneRecognizeCallback`, but one of:

* `PingOneRecognizeEnrollCallback`: returned when `operationType` is `ENROLL`; call `enroll()` on it.

* `PingOneRecognizeAuthenticateCallback`: returned when `operationType` is `AUTHENTICATE`; call `authenticate()` on it.

Your app never configures the underlying mobile SDK itself. All configuration, such as the service host and API key, arrives in the callback output from the node.

Before you start, make sure your app meets the following requirements:

* The app declares camera usage in its `Info.plist` with the `NSCameraUsageDescription` key; the biometric ceremony needs camera access to capture the user's face.

* The app runs the ceremony on the main thread with an active view controller in the hierarchy, so the biometric UI can present over your app.

Also, ensure that you have completed the [server-side setup](https://docs.pingidentity.com/pingoneaic/latest/advanced-use-cases/pingone-recognize-service.html) for PingOne Recognize.

## Step 1. Preparing the project to download the dependency

The `PingRecognize` module depends on the [PingOne Recognize mobile SDK](https://docs.pingidentity.com/recognize/mobile-sdk/mobile-sdk-introduction.html), which Ping distributes from a private Cloudsmith repository. The `PingRecognize` module itself is published to the Orchestration SDK for iOS repository, but your package manager cannot resolve its dependency tree, and therefore cannot download the module, until you configure access to that repository.

To configure your project to download the SDK, obtain an API key and add the repository:

1. When your PingOne Recognize tenant has been provisioned, sign in to the dashboard for your region to obtain an API key for the `keyless` repositories:

   * [Europe](https://dash-eu.recognize.keyless.technology/auth)

   * [United States](https://dash-us.recognize.keyless.technology/auth)

   * [Australia](https://dash-au.recognize.keyless.technology/auth)

   * [Singapore](https://dash-sg.recognize.keyless.technology/auth)

2. Add the secure repository so your project can download the underlying Keyless module.

   Your package manager downloads the PingOne Recognize mobile SDK from this protected repository when it resolves the `PingRecognize` dependency you add in [Step 2. Installing the module](#install).

   Complete only the option for your chosen package manager:

   * **CocoaPods**

     1. If you are using CocoaPods, add the Keyless spec repository to your `Podfile` when you create it in [Step 2. Installing the module](#install), replacing **\<your-api-key>** with the API key you obtained in the previous step:

        ```ruby
        source 'https://dl.cloudsmith.io/<your-api-key>/keyless/partners/cocoapods/index.git'
        ```

        |   |                                                                                                                                                    |
        | - | -------------------------------------------------------------------------------------------------------------------------------------------------- |
        |   | Never commit API keys to source control.Inject the key into your `Podfile` from an environment variable, or generate the file in your CI pipeline. |

   * **Swift Package Manager**

     1. If you are using the Swift Package Manager, with **Xcode** closed, in a terminal window run the following commands to map the registry to the `keyless` scope:

        ```shell
        swift package-registry set --global --scope keyless https://swift.cloudsmith.io/keyless/partners/
        ```

     2. Run the following to authenticate with the API key you obtained in the previous step:

        ```shell
        swift package-registry login https://swift.cloudsmith.io/keyless/partners/ --token <your-api-key>
        ```

        |   |                                                                                                                                              |
        | - | -------------------------------------------------------------------------------------------------------------------------------------------- |
        |   | Never commit API keys to source control.The login command stores the key in your macOS keychain, so it is never written to any project file. |

## Step 2. Installing the module

Add the `PingRecognize` module to your Orchestration SDK project.

You can use CocoaPods or the Swift Package Manager to add the dependencies to your iOS project.

### Add the dependency using CocoaPods

1. If you do not already have a `Podfile`, in a terminal window, run the following command in your project directory to create one:

   ```shell
   pod init
   ```

2. Add the following lines to your `Podfile`:

   ```ruby
   pod 'PingJourney'
   pod 'PingRecognize' // Add-on for PingOne Recognize
   ```

3. If you configured the CocoaPods option in Step 1, run the following command to install the pods:

   ```shell
   pod install
   ```

### Add the dependency using Swift Package Manager

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 modal, ensure that the `PingRecognize` library is added to your target project.

5. Click Add Package.

|   |                                                                                                                                                                                                    |
| - | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|   | If the `PingRecognize` product does not appear in the Choose Package Products modal, confirm that you completed Step 1, because the product only appears after you configure the Keyless registry. |

1. Import the module in the files where you handle the callback:

   ```swift
   // Import the Journeys library
   import PingJourney

   // Import the PingOne Recognize library
   import PingRecognize
   ```

## Step 3. Handling the enroll callback

The server triggers an enrollment ceremony by returning a `PingOneRecognizeCallback` with `operationType = "ENROLL"`.

The Orchestration SDK resolves the callback from the output fields and instantiates `PingOneRecognizeEnrollCallback`, so your app only needs to identify that callback in the node's callback list and call `enroll()` on it:

```swift
import PingJourney
import PingRecognize

if let continueNode = node as? ContinueNode {
  for callback in continueNode.callbacks {
    if let callback = callback as? PingOneRecognizeEnrollCallback {
      let result = await callback.enroll()
      switch result {
      case .success:
        // Enrollment successful, proceed to the next step in the journey
        await continueNode.next()
      case .failure(let error):
        // Enrollment failed, handle the error; see Error handling
        print("Enrollment failed: code=\(error.code), message=\(error.message)")
        await continueNode.next()
      }
    }
  }
}
```

Optionally, set the `retrieveSelfie` option to `true` to receive the image captured during the ceremony on the result:

```swift
let result = await callback.enroll { config in
  config.retrieveSelfie = true
}

switch result {
  case .success(let enrollSuccess):
    let selfie: CGImage? = enrollSuccess.selfie
    // Enrollment successful, proceed to the next step in the journey
    await continueNode.next()
  case .failure(let error):
    // Enrollment failed, handle the error
    print("Enrollment failed: code=\(error.code), message=\(error.message)")
    await continueNode.next()
}
```

## Step 4. Handling the authenticate callback

The server requests a biometric authentication ceremony by returning a `PingOneRecognizeCallback` with `operationType = "AUTHENTICATE"`.

The Orchestration SDK resolves the callback from the output fields and instantiates `PingOneRecognizeAuthenticateCallback`, so to authenticate an enrolled user, your app only needs to identify that callback in the node's callback list and call `authenticate()` on it:

```swift
import PingJourney
import PingRecognize

if let continueNode = node as? ContinueNode {
  for callback in continueNode.callbacks {
    if let callback = callback as? PingOneRecognizeAuthenticateCallback {
      let result = await callback.authenticate()
      switch result {
        case .success:
          // Authentication successful, proceed to the next step in the journey
          await continueNode.next()
        case .failure(let error):
          // Authentication failed, handle the error; see Error handling
          print("Authentication failed: code=\(error.code), message=\(error.message)")
          await continueNode.next()
      }
    }
  }
}
```

The callbacks submit their inputs to the journey automatically. Do not call `input()` manually, or you risk submitting a duplicate result.

On success, the SDK submits the following inputs to the server automatically:

* Signed JWT

  A JWT signed by the Keyless authentication service attesting that the ceremony completed on the device: the Keyless service issued it after the user's captured face matched their enrolled biometric template and passed the liveness checks. The Keyless authentication service, not your app, signs the JWT, so the server can verify its signature rather than trust a result claimed by the client. If the node configured transaction data, the signed JWT also carries it, which binds the ceremony to that specific transaction.

* Current user ID

  The user's PingOne Recognize identifier.

* Client state

  An opaque blob used to restore the user's account on a new device.

* Device public signing key

  The public part of the key pair the device generated during enrollment.

|   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| - | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|   | If the server sends a value in the `clientState` output field, such as when the node enables the Generate Client State property, and the device is not yet enrolled, then the device is enrolled automatically during the authentication ceremony.The Orchestration SDK passes the client state to the PingOne Recognize mobile SDK, which uses the client state to restore the user's account on the device.The Orchestration SDK handles this fallback itself, so no extra code is needed in your app. |

## Error handling

All Recognize SDK failures are wrapped in a `RecognizeError` before they reach your code:

```swift
struct RecognizeError: Error, LocalizedError, Sendable {
    let message: String                  // Human-readable description
    let code: Int                        // Numeric code from the underlying Keyless SDK
    let debuggingInfo: [String: String]  // Diagnostic entries, such as the ceremony flow ID, session ID, and stack trace
}
```

The `code` and `message` values are injected into the `clientError` and `clientErrorCode` input slots. You must call `continueNode.next()` after encountering an error so your server receives the error detail, regardless of how your app handles the failed result.

The `failure` case of the result carries the `RecognizeError` as its `error` value, so you can read its `code` directly, for example:

```swift
import PingRecognize

let result = await callback.authenticate()

switch result {
case .success:
  // Authentication successful, proceed to the next step in the journey
  await continueNode.next()
case .failure(let error):
  if let error = error as? RecognizeError {
    switch error.code {
    case 30003: showRetryPrompt("Face scan cancelled")       // User cancelled
    case 30004: showRetryPrompt("Face did not match")        // Face not matching
    case 30005: showRetryPrompt("No network connection")     // No network connection
    case 30007: showLockoutScreen()                          // User lockout
    case 30009: requestCameraPermission()                    // Camera permission denied
    default: logUnexpectedError(error)
    }
  }
  // Continue the journey to report client errors back to the server
  await continueNode.next()
}
```

The codes in the example are in the user error range of 30000 and above, which result from user behavior during the ceremony.

Codes below 30000 indicate integration or internal errors, such as `20000` when the user is not yet enrolled on the device.
