OTP Handshake & Android SDK

How the WhatsApp OTP handshake works for one-tap and zero-tap authentication templates: OTP Android SDK, manifest setup, eligibility checks, multi-app support, error codes, and PendingIntent deprecation.

What the Handshake Does

A handshake is an Android intent your app sends to the WhatsApp client (or WhatsApp Business app) before you call the authentication template send endpoint. The handshake tells WhatsApp to expect imminent delivery of a one-time password and authorizes WhatsApp to deliver the OTP back to your app via intent (one-tap) or broadcast (zero-tap).

Without a successful handshake, WhatsApp falls back to displaying a copy-code button — even if your template is configured for one-tap or zero-tap.

When You Need It

Authentication template typeHandshake required?
copy_codeNo
one_tapYes
zero_tapYes

The handshake is per-OTP-request, not per-session. Initiate it every time the user requests a code.

The OTP Android SDK (Preferred)

Meta provides an Android SDK that wraps the handshake protocol, generates the request_id (UUID), validates incoming OTPs, and maps low-level errors to typed error codes. It is the preferred and only supported method going forward — see PendingIntent Deprecation below.

Add to your Gradle file:

dependencies {
    implementation 'com.whatsapp.otp:whatsapp-otp-android-sdk:1.0.0'
}

repositories {
    mavenCentral()
}

Manifest Setup

Declare a query for the WhatsApp packages so Android allows your app to detect and broadcast to them:

<queries>
    <package android:name="com.whatsapp" />
    <package android:name="com.whatsapp.w4b" />
</queries>

For one-tap, declare an Activity that will receive the OTP intent:

<activity
   android:name=".ReceiveCodeActivity"
   android:enabled="true"
   android:exported="true"
   android:launchMode="standard">
   <intent-filter>
       <action android:name="com.whatsapp.otp.OTP_RETRIEVED" />
   </intent-filter>
</activity>

For zero-tap, declare a BroadcastReceiver instead:

<receiver
   android:name=".app.receiver.OtpCodeReceiver"
   android:enabled="true"
   android:exported="true">
   <intent-filter>
       <action android:name="com.whatsapp.otp.OTP_RETRIEVED" />
   </intent-filter>
</receiver>

If your template's eligibility-failure fallback can drop from zero-tap to one-tap (which it does by default), declare both — the broadcast receiver for zero-tap and the activity for the one-tap fallback.

Initiating the Handshake

The SDK generates a UUID handshake ID, broadcasts the OTP_REQUESTED intent to both com.whatsapp and com.whatsapp.w4b (Business), and returns the UUID for you to store and validate against the incoming OTP later.

WhatsAppOtpHandler whatsAppOtpHandler = new WhatsAppOtpHandler();
UUID handshakeId = whatsAppOtpHandler.sendOtpIntentToWhatsApp(context);
// Persist handshakeId so the receiver can validate it when the OTP arrives

After this returns, call your backend to send the authentication template via the WhatsApp Cloud API. The handshake remains valid for 10 minutes (or code_expiration_minutes if set lower on the template).

Receiving the OTP (One-Tap)

In the Activity declared in the manifest, instantiate WhatsAppOtpIncomingIntentHandler, pass the stored handshake ID, and process the intent:

public class ReceiveCodeActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        WhatsAppOtpIncomingIntentHandler handler = new WhatsAppOtpIncomingIntentHandler();

        String expectedHandshakeId = retrieveStoredHandshakeId();

        handler.processOtpCode(
            getIntent(),
            expectedHandshakeId,
            (code) -> {
                // SDK validated the handshake ID — code is trusted
                validateCode(code);
            },
            (error, exception) -> handleError(error, exception)
        );
    }
}

processOtpCode validates the incoming request_id against your stored handshake ID. If they match, the success callback receives the code. If they don't (or any other validation fails), the error callback receives a typed HANDSHAKE_ID_* error.

Receiving the OTP (Zero-Tap)

For zero-tap, the BroadcastReceiver follows the same pattern:

public class OtpCodeReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        WhatsAppOtpIncomingIntentHandler handler = new WhatsAppOtpIncomingIntentHandler();
        String expectedHandshakeId = retrieveStoredHandshakeId();

        handler.processOtpCode(
            intent,
            expectedHandshakeId,
            (code) -> validateCode(code),
            (error, exception) -> handleError(error, exception)
        );
    }
}

The user does not see anything — your app receives the broadcast and processes the OTP silently.

Eligibility Checks

When WhatsApp receives an authentication template message, it runs an eligibility check before deciding whether to honor the configured button type. If any check fails, the message falls back: zero-tap → one-tap → copy-code.

Checks performed (per Meta):

  1. The handshake was initiated no more than 10 minutes ago (or code_expiration_minutes if shorter).
  2. The package name in the message (package_name on the template) matches the package name set on the intent (verified via getCreatorPackage on the PendingIntent provided by your app, where applicable).
  3. None of the other apps in the template's supported_apps array initiated a handshake in the last 10 minutes.
  4. The app signing key hash in the message (signature_hash) matches the installed app's signing key hash.
  5. The message includes the autofill button text.
  6. Your app has declared an Activity (one-tap) or BroadcastReceiver (zero-tap) that handles com.whatsapp.otp.OTP_RETRIEVED.

If any of these fail, you'll see the message rendered with a copy-code button instead of the configured one-tap or zero-tap button.

Multi-App Support (supported_apps)

The supported_apps array on an authentication template's button accepts up to 5 package_name + signature_hash pairs. Useful when you have multiple app builds (debug, staging, production) or distribute your app under different package names per platform.

"supported_apps": [
  { "package_name": "com.example.app",       "signature_hash": "K8a/AINcGX7" },
  { "package_name": "com.example.app.debug", "signature_hash": "Lp9/BJOdHY8" }
]

Eligibility check passes if any of the listed pairs matches the installed app initiating the handshake. Only one of the listed apps can have an active handshake at a time (per check #3 above).

App Signing Key Hash

To compute your app signing key hash, follow Google's Computing Your App's Hash String guide.

Alternatively, after downloading your signing key certificate, run Meta's helper script:

./sms_retriever_hash_v9.sh --package "com.example.myapplication" --keystore ~/.android/debug.keystore

The hash is exactly 11 characters and contains only a-z, A-Z, 0-9, +, /, or =.

Android Notifications

Android only shows a system notification for an incoming WhatsApp authentication template message if all of these are true:

  • The user is logged into WhatsApp (or WhatsApp Business) with the same phone number the message was sent to.
  • The user is logged into your app.
  • Android OS is KitKat (4.4, API 19) or above.
  • "Show notifications" is enabled in WhatsApp's settings.
  • Device-level notifications are enabled for WhatsApp.
  • The prior message thread between the user and your business is not muted.

Notification absence does not mean delivery failure — the OTP still arrives via intent or broadcast even with notifications suppressed.

Error Codes

When using the SDK with handshake ID validation, these error codes can be returned:

CodeMeaning
HANDSHAKE_ID_MISSINGThe handshake ID was not included in the intent from WhatsApp
HANDSHAKE_ID_INVALID_FORMATThe handshake ID is not a valid UUID format
HANDSHAKE_ID_MISMATCHThe handshake ID in the intent does not match the expected (stored) value

A HANDSHAKE_ID_MISMATCH error is the most security-relevant — it can indicate the OTP was delivered for a request initiated by another instance of your app (or a different process), or that an attacker is attempting to inject an OTP. Never accept the code on mismatch.

PendingIntent Deprecation (Oct 15, 2026)

The PendingIntent-based handshake method (used in older code samples) will be deprecated on October 15, 2026 (extended from the earlier deadline).

The legacy approach used PendingIntent.getActivity to attach a caller identity to the OTP_REQUESTED intent so WhatsApp could verify the source via getCreatorPackage. This works today but stops working after the deadline.

Migrate to the OTP Android SDKWhatsAppOtpHandler.sendOtpIntentToWhatsApp(context) returns a UUID handshake ID and replaces all the PendingIntent boilerplate. The SDK is forward-compatible and is the only supported handshake method post-deprecation.

If you cannot use the SDK for some reason, the manual replacement uses an explicit request_id (UUID) extra on the OTP_REQUESTED broadcast and validates the same request_id on the receiving side. But there is no good reason to avoid the SDK in new code.

iOS and Other Platforms

One-tap and zero-tap are Android-only. Authentication templates sent to users on iOS or any other platform always render with a copy-code button regardless of how you configured the template.

To check WhatsApp installation on iOS:

let schemeURL = URL(string: "whatsapp://otp")!
let isWhatsAppInstalled = UIApplication.shared.canOpenURL(schemeURL)

For copy-code on any platform, no handshake is required and no app-side integration is needed beyond reading the pasted value.

Related

  • Authentication MessagesOTP and verification code delivery via WhatsApp: copy-code, one-tap, and zero-tap templates, linked device security, the 'I didn't request a code' webhook, and PendingIntent deprecation.
  • Authentication Best PracticesSecurity and UX best practices for WhatsApp authentication: identity hash binding for recycled phone protection, opt-in collection, isWhatsAppInstalled checks for Android and iOS, multi-app support.
Browse more docsStart Free Trial