Skip to content
Documentation menu

Documentation

Mobile SDKs

iOS, Android, and React Native SDKs run the verification on the device and hand you a token. Here is the flow, and why the device's own answer is not the result.

The mobile SDKs run a verification on the device: they present a screen, send the code, take the user’s input, and return when it is done. The channel is chosen by your account routing, so you pass a recipient and nothing else, the same way you would on the REST API.

They are not a client for the rest of this reference. A mobile app authenticates with a publishable key, which reaches a deliberately smaller surface: it can start a verification and answer one, and nothing else. See Authentication.

The flow has three steps

  1. Your app configures the SDK with a publishable key (otp_pk_live_…) and runs a verification. It gets back a short-lived verification_token.
  2. Your app hands that token to your own backend, over your own API.
  3. Your backend exchanges it at POST /verifications/exchange with your server key (otp_live_…). That response names the recipient that was verified.

The third step is not bookkeeping. It is the only point in the flow where you learn something you can trust. The SDK’s success, and the matched field behind it, are read off a device you do not control: anyone running a modified build can claim any outcome they like, so what the device reports is a hint for your UI, not evidence. Until your backend has exchanged the token, it knows nothing.

The token carries no information on its own and it expires quickly. Send it to your backend as soon as you have it, and treat it as the only thing the app produced that matters.

iOS

Requires iOS 15 and Xcode 16.

Swift Package Manager, in Xcode under File > Add Package Dependencies:

https://github.com/otp-com/sdk-ios

Or CocoaPods:

pod 'Otp', '~> 0.1'
import Otp

// once, at launch
OtpClient.configure(publishableKey: "otp_pk_live_…")

// presents the screen, sends the code, and returns when the user is done
let verification = try await OtpClient.verify(recipient: "+14155552671")
try await yourBackend.exchange(verification.token)  // step 3: the only trustworthy result

Android

Requires Android API 26 (8.0) and Java 17 to build.

dependencies {
    implementation("com.otp:sdk-android:0.1.0")
}

Nothing to add to your manifest: the SDK declares its own permission and screen host, and manifest merging does the rest.

// once, at launch
OtpClient.configure(context, "otp_pk_live_…")

// suspends while the screen is up; throws OtpException of kind CANCELLED if the user closes it
val verification = OtpClient.verify("+14155552671")
yourBackend.exchange(verification.token)  // step 3: the only trustworthy result

The floor is API 26 rather than the more common 24 because of what the SDK relies on: it proves a send came from your app with a hardware-backed Keystore key, and hardware attestation is only guaranteed from 8.0. Below that, a device may fall back to a software key whose signing key is public in AOSP, which proves nothing.

React Native

Requires the New Architecture, iOS 15, and Android API 26. Built and tested against React Native 0.87.

npm install @otp.com/sdk-react-native
cd ios && pod install

React Native’s own floor is API 24, so raise it in android/build.gradle or the Android build fails:

buildscript {
    ext {
        minSdkVersion = 26
    }
}
import {configure, verify} from '@otp.com/sdk-react-native';

await configure({publishableKey: 'otp_pk_live_…'});

const verification = await verify('+14155552671');
await yourBackend.exchange(verification.token);  // step 3: the only trustworthy result

The package is a bridge over the native SDKs rather than a re-implementation, so the screen your user sees is the platform’s own. Autolinking finds the native module on both platforms; there is nothing to register.

Step three, from your backend

curl -X POST https://api.otp.com/api/v1/verifications/exchange \
  -H "Authorization: Bearer otp_live_•••" \
  -H "Content-Type: application/json" \
  -d '{"verification_token":"otp_vt_•••"}'
{
  "otp_id": "6f0d2c5e-…",
  "recipient": "+14155552671",
  "recipient_type": "phone",
  "channel": "sms",
  "verified_at": "2026-09-08T19:33:21Z"
}

Now you know which number was verified, and you can finish the sign-in. The server SDKs make this call for you from 1.2.0. Full reference: POST /verifications/exchange.

Recipients, locales, and your own screen

If your flow has no phone field yet, the SDK can collect one: OtpClient.verify(collecting: .phone) on iOS, OtpClient.verify(collecting = RecipientKind.PHONE) on Android, verifyCollecting('phone') in React Native, each with an email variant. The screen and the message follow the same locale so they cannot disagree; it defaults to the device’s and takes an override.

If you want a screen of your own, build it on the same core the drop-in screen uses: OtpSession on iOS and Android, start and submit in React Native. Everything a screen needs, including the code length and both deadlines, arrives with the pending verification, so nothing polls. The result is the same verification_token, and step three does not change.

Device integrity, and what that costs you before you ship

The SDKs register a hardware-backed key on first use (App Attest on iOS, Keystore attestation on Android) and sign every send with it. That is what stops a publishable key lifted out of your binary from being used outside your app, and it is why the key is safe to ship. You configure none of it.

It has one consequence worth planning for: a simulator or an emulator cannot produce a proof. Whether a proof is required follows the key, and a sandbox publishable key (otp_pk_test_…) never requires one, so a simulator is fine while you integrate. A live key does require one, which makes a real device with your live key the last thing to test before you ship.

On iOS there is a second half to that, and it is the half that looks like a broken SDK. App Attest runs in one of two environments: a build installed from Xcode attests in the development one, while TestFlight and App Store builds attest in production. A live key accepts only production, so the app that registered fine with a sandbox key is refused with a live key on that same real device, with a 403 at registration:

Device attestation rejected: attestation

That is deliberate. A development attestation comes from Xcode, so honouring it on a live key would let anyone holding your publishable key and a Mac register a device as yours. Develop against the sandbox key, and test the live key from a TestFlight build. Android has no equivalent split.

Licensing

The mobile SDKs ship as compiled binaries under a commercial licence, not as open source. Anyone with an otp.com account may install one and distribute it as part of their own app, under the Terms of Service; it may not be redistributed on its own, or reverse engineered. The server SDKs are different: those are MIT and their source is public.

Each repository carries the fuller integration guide for its platform: sdk-ios, sdk-android, sdk-react-native.