Authentication Best Practices

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

The Recycled-Phone Problem

WhatsApp verifies phone-number ownership via a 6-digit registration code at sign-up, but does not enforce continued ownership. After registration, the WhatsApp account stays bound to the original device's WhatsApp installation, not to the phone number itself.

When mobile carriers recycle a phone number to a new subscriber:

  • The new owner has the SIM and receives SMS to that number.
  • The previous owner may still hold the WhatsApp account tied to that number, on their old device.

If your authentication flow treats "I sent a code to this phone number via WhatsApp" as equivalent to "I sent a code to the rightful account holder," you can deliver OTPs to the wrong person — even when the phone number itself is correct.

This is most dangerous in account recovery flows where the WhatsApp OTP is the only authentication factor.

Identity Hash Binding

Cloud API includes an identity-change check system to detect when the WhatsApp account behind a phone number has changed. The pattern:

  1. User registers in your app and selects WhatsApp as their auth channel.
  2. Send an initial verification OTP via WhatsApp.
  3. From the send response (or status webhook), Meta returns an identity hash for the recipient's WhatsApp account.
  4. Store the identity hash alongside the user's account record.
  5. On every subsequent OTP send, include that stored identity hash on the send request.
  6. If the underlying WhatsApp account has changed (recycled phone, account migrated to a new device, etc.), Meta blocks the send rather than delivering the OTP to a different account.

Failed sends in this case surface as standard error responses on the Cloud API call, which your backend can handle by forcing re-verification through a different channel.

This binding is more secure than SMS, where the carrier-level identity is not exposed to you and a recycled-number takeover is silent.

Dualhook Stance: We Don't Proxy Auth Sends

Dualhook does not sit in your authentication-message send path, augment identity-hash checks, or read OTP content. Authentication template sends go directly from your backend to the Meta Cloud API endpoint, and status webhooks (including identity-change-related errors) are routed by Webhook Override straight to your endpoint without Dualhook ingest.

What this means in practice:

  • Identity hash storage and replay are your responsibility. Dualhook does not capture or persist identity hashes from your send responses. Store them on your user record, server-side.
  • Identity-change errors surface through your normal Cloud API error handling, not through any Dualhook-specific channel. There is no Dualhook hook to "auto-block on identity change" — it's already handled by Meta when you replay the hash on the send call.
  • We never read the messages webhook field, including events like DID_NOT_REQUEST_CODE that some auth flows rely on for fraud detection. Those events still reach you via Webhook Override; Dualhook simply isn't in the loop.

In short: auth security is a contract between your backend and Meta. Dualhook keeps the connection alive (token refresh, webhook subscriptions, quality monitoring) but stays out of every auth-relevant send and inbound event.

Pair Authentication Channels

For sensitive flows (account recovery, password reset, large transactions), do not treat a single WhatsApp OTP as sufficient. Add at least one of:

  • A second factor over a different channel (email, push notification, hardware key).
  • A knowledge factor (security question, last-4 of card, prior transaction detail).
  • A device-trust signal (registered device fingerprint, recent successful login from same device).

The OTP proves "someone with access to this WhatsApp account did the action," not "the account holder did the action."

Anti-Phishing Controls

WhatsApp disables forwarding of authentication template messages — recipients cannot forward an OTP to a third party from inside WhatsApp. Combined with end-to-end encryption between Cloud API and the user, this raises the cost of social-engineering an OTP off a victim.

The flip side: don't include the OTP in any message body shown outside the authentication template (e.g. a follow-up text or notification preview). The forwarding restriction only applies to the authentication template itself.

Do Not Trust Unofficial Clients

WhatsApp cannot validate the security practices of unofficial WhatsApp clients (modded APKs, third-party reimplementations). If a user is on an unofficial client, OTP delivery is not guaranteed to be secure even though Cloud API delivers the message normally.

You cannot detect this from your side. The mitigation is to keep WhatsApp as one of multiple offered channels rather than forcing it.

Collect Explicit Opt-In

Per the WhatsApp Business Messaging Policy, you must collect opt-in before sending any authentication message — even an initial verification.

A clean implementation: at sign-up or when the user first chooses WhatsApp as their auth channel, present a clear choice ("WhatsApp / SMS / email") rather than defaulting to WhatsApp. Document the choice. The selection is the opt-in.

Show Installed Channels Only

If you offer WhatsApp alongside other channels, only show WhatsApp when the device actually has WhatsApp installed. Otherwise users will pick WhatsApp by default and never receive the code.

The check is cheap (Android intent query, iOS scheme check) and removes a class of "I never got the code" support tickets. See Checking WhatsApp Installation: Android and iOS below.

Support Both WhatsApp and Business App

Two separate WhatsApp packages exist on Android:

  • com.whatsapp — consumer WhatsApp
  • com.whatsapp.w4b — WhatsApp Business app (some users use this exclusively)

If you only check or broadcast to one, you'll miss users on the other. Always handle both — Meta's OTP Android SDK does this automatically.

Be Ready for Immediate Delivery

When using one-tap autofill, the OTP can arrive before your code-entry screen finishes loading — especially on slow networks. If your handler only listens after the screen is fully presented, you'll drop the code.

Pattern: persist the received code in memory or local storage as soon as the receiver fires, then read from that storage when the entry screen is ready. Clear it after use.

Confirm the WhatsApp Number, Not the SMS Number

A user's "phone number" can mean different things on different SIMs and apps. The phone number registered with WhatsApp is not always the same as the SIM number used for SMS — common in:

  • Dual-SIM users (one for travel, one for home).
  • Users who registered WhatsApp on a number they no longer use for SMS.
  • Users who ported a number but kept WhatsApp on the old account.

If your sign-up flow assumes the registration phone number is the WhatsApp number, you'll send OTPs to the wrong place. Always give the user a chance to confirm the number specifically used for WhatsApp before sending the OTP.

If WhatsApp delivery is failing but the chosen channel is WhatsApp, suspect this scenario before suspecting the API.

One WABA Per Business

Every business sending authentication templates must use its own WhatsApp Business Account and its own phone numbers. Sharing a WABA across separate business entities violates the WhatsApp Business Terms of Service and the WhatsApp Business Messaging Policy.

Sharing also degrades the user experience — message threads, quality ratings, and policy enforcement are scoped to the WABA, not the underlying business identity.

Checking WhatsApp Installation: Android

Add to AndroidManifest.xml so the package query is allowed:

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

With the OTP Android SDK (preferred):

WhatsAppOtpHandler whatsAppOtpHandler = new WhatsAppOtpHandler();
if (whatsAppOtpHandler.isWhatsAppInstalled(context)) {
    // show WhatsApp option in your UI
}

Without the SDK:

public boolean isWhatsAppInstalled(Context context) {
    return isWhatsAppInstalled(context, "com.whatsapp")
        || isWhatsAppInstalled(context, "com.whatsapp.w4b");
}

private boolean isWhatsAppInstalled(Context context, String packageName) {
    Intent intent = new Intent();
    intent.setPackage(packageName);
    intent.setAction("com.whatsapp.otp.OTP_REQUESTED");
    return !context.getPackageManager().queryBroadcastReceivers(intent, 0).isEmpty();
}

Either form returns true if either the consumer or business app is installed.

Checking WhatsApp Installation: iOS

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

Note: the iOS canOpenURL check requires the whatsapp URL scheme to be declared in your Info.plist LSApplicationQueriesSchemes. iOS authentication delivery uses copy-code only — there is no one-tap or zero-tap on iOS — but the installation check still gates whether it makes sense to offer WhatsApp as a channel at all.

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.
  • OTP Handshake & Android SDKHow 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.
  • Compliance & Data RetentionPrivacy boundary, configurable retention, CSV export, and organization isolation.
Browse more docsStart Free Trial