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 type | Handshake required? |
|---|---|
copy_code | No |
one_tap | Yes |
zero_tap | Yes |
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):
- The handshake was initiated no more than 10 minutes ago (or
code_expiration_minutesif shorter). - The package name in the message (
package_nameon the template) matches the package name set on the intent (verified viagetCreatorPackageon the PendingIntent provided by your app, where applicable). - None of the other apps in the template's
supported_appsarray initiated a handshake in the last 10 minutes. - The app signing key hash in the message (
signature_hash) matches the installed app's signing key hash. - The message includes the autofill button text.
- 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:
| Code | Meaning |
|---|---|
HANDSHAKE_ID_MISSING | The handshake ID was not included in the intent from WhatsApp |
HANDSHAKE_ID_INVALID_FORMAT | The handshake ID is not a valid UUID format |
HANDSHAKE_ID_MISMATCH | The 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 SDK — WhatsAppOtpHandler.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.