Skip to content
Documentation menu

Documentation

Mobile SDKs

iOS, Android, React Native, and Flutter 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.

The install lines below are deliberately unpinned, so every package manager here resolves the latest release on its own. Where a version number is unavoidable, take it from that SDK’s repository README: those move with the release, and this page would not.

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'
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.

Gradle needs an explicit version, so take the current one from the sdk-android README:

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

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.

Flutter

Requires Flutter 3.35, Dart 3.9, iOS 15, and Android API 26.

flutter pub add otp_flutter

Flutter’s own Android floor is lower, so raise it in android/app/build.gradle.kts or the Android build fails:

android {
    defaultConfig {
        minSdk = 26
    }
}
import 'package:otp_flutter/otp_flutter.dart';

// once, at launch
await Otp.configure(publishableKey: 'otp_pk_live_…');

// presents the screen, sends the code, and resolves when the user is done
final verification = await Otp.verify('+14155552671');
await yourBackend.exchange(verification.token);  // step 3: the only trustworthy result

Like the React Native package, this one bridges the native SDKs, so the screen is the platform’s own. Flutter’s plugin resolution finds both native halves; Swift Package Manager and CocoaPods both work on iOS and neither needs a Podfile line.

Every call throws OtpException and nothing else, and kind is what you branch on. It can arrive as OtpErrorKind.unknown, because the list grows server-side and an app already on a phone cannot be updated to match.

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, Otp.verifyCollecting(RecipientKind.phone) in Flutter, 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 and Flutter. 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.

The Flutter package is the one place that boundary is visible: the published otp_flutter package is Apache-2.0, because it is glue, and the native SDKs it bridges stay commercial.

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