PendingIntent-based handshake method for authentication templates will be deprecated. If you are currently using PendingIntent to initiate handshakes or verify app identity, the OTP Android SDK is the preferred way to migrate.
curl 'https://graph.facebook.com/v25.0/<WHATSAPP_BUSINESS_ACCOUNT_ID>/message_templates' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer EAAJB...' \
-d
'{
"name": "<TEMPLATE_NAME>",
"language": "<TEMPLATE_LANGUAGE>",
"category": "authentication",
"message_send_ttl_seconds": <TIME_T0_LIVE>, // Optional
"components": [
{
"type": "body",
"add_security_recommendation": <SECURITY_RECOMMENDATION> // Optional
},
{
"type": "footer",
"code_expiration_minutes": <CODE_EXPIRATION> // Optional
},
{
"type": "buttons",
"buttons": [
{
"type": "otp",
"otp_type": "one_tap",
"text": "<COPY_CODE_BUTTON_TEXT>", // Optional
"autofill_text": "<AUTOFILL_BUTTON_TEXT>", // Optional
"supported_apps": [
{
"package_name": "<PACKAGE_NAME>",
"signature_hash": "<SIGNATURE_HASH>"
}
]
}
]
}
]
}'
otp, but upon creation the button type will be set to url. You can confirm this by performing a GET request on a newly created authentication template and analyzing its components.| Placeholder | Description | Example Value |
|---|---|---|
<AUTOFILL_BUTTON_TEXT>String | Optional. One-tap autofill button label text. If omitted, the autofill text will default to a pre-set value, localized to the template’s language. For example, Autofill for English (US).Maximum 25 characters. | Autofill |
<CODE_EXPIRATION>Integer | Optional. Indicates the number of minutes the password or code is valid. If included, the code expiration warning and this value will be displayed in the delivered message. The button will be disabled in the delivered message the indicated number of minutes from when the message was sent. If omitted, the code expiration warning will not be displayed in the delivered message. In addition, the button will be disabled 10 minutes from when the message was sent. Minimum 1, maximum 90. | 5 |
<COPY_CODE_BUTTON_TEXT>String | Optional. Copy code button label text. If omitted, the text will default to a pre-set value localized to the template’s language. For example, Copy Code for English (US).If included, the authentication template message will display a copy code button with this text if the message fails the eligibility check. Maximum 25 characters. | Copy Code |
<PACKAGE_NAME>String | Required. Your Android app’s package name. The string must have at least two segments (one or more dots), and each segment must start with a letter. All characters must be alphanumeric or an underscore [ a-zA-Z0-9_].If using Graph API version 20.0 or older, you can define your app’s package name outside of the supported_apps array, but this is not recommended. See Supported Apps below.Maximum 224 characters. | com.example.luckyshrub |
<SECURITY_RECOMMENDATION>Boolean | Optional. Set to true if you want the template to include the string, For your security, do not share this code. Set to false to exclude the string. | true |
<SIGNATURE_HASH>String | Required. Your app signing key hash. See App Signing Key Hash below. All characters must be either alphanumeric, +, /, or = (a-zA-Z0-9+/=).If using Graph API version 20.0 or older, you can define your app’s signature hash outside of the supported_apps array, but this is not recommended. See Supported Apps below.Must be exactly 11 characters. | K8a/AINcGX7 |
<TEMPLATE_LANGUAGE>String | Required. Template language and locale code. | en_US |
<TEMPLATE_NAME>String | Required. Template name. Maximum 512 characters. | verification_code |
<TIME_TO_LIVE>Integer | Optional. Authentication message time-to-live value, in seconds. See Customizing Time-To-Live. | 60 |
authentication with all optional text strings enabled and a one-tap autofill button.curl 'https://graph.facebook.com/v25.0/102290129340398/message_templates' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer EAAJB...' \
-d '
{
"name": "authentication_code_autofill_button",
"language": "en_US",
"category": "authentication",
"message_send_ttl_seconds": 60,
"components": [
{
"type": "body",
"add_security_recommendation": true
},
{
"type": "footer",
"code_expiration_minutes": 10
},
{
"type": "buttons",
"buttons": [
{
"type": "otp",
"otp_type": "one_tap",
"text": "Copy Code",
"autofill_text": "Autofill",
"package_name": "com.example.luckyshrub",
"signature_hash": "K8a/AINcGX7"
}
]
}
]
}'
{ "id": "594425479261596", "status": "PENDING", "category": "AUTHENTICATION" }
{ "object": "whatsapp_business_account", "entry": [ { "id": "320580347795883", "changes": [ { "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "12345678", "phone_number_id": "1234567890" }, "contacts": [ { "profile": { "name": "John" }, "wa_id": "12345678" } ], "messages": [ { "context": { "from": "12345678", "id": "wamid.HBgLMTIxMTU1NTE0NTYVAgARGBJDMDEyMTFDNTE5NkFCOUU3QTEA" }, "from": "12345678", "id": "wamid.HBgLMTIxMTU1NTE0NTYVAgASGCBBQ0I3MjdCNUUzMTE0QjhFQkM4RkQ4MEU3QkE0MUNEMgA=", "timestamp": "1753919111", "from_logical_id": "131063108133020", "type": "button", "button": { "payload": "DID_NOT_REQUEST_CODE", "text": "I didn't request a code" } } ] }, "field": "messages" } ] } ] }
./sms_retriever_hash_v9.sh --package "com.example.myapplication" --keystore ~/.android/debug.keystore
supported_apps array allows you define pairs of app package names and signing key hashes for up to 5 apps. This can be useful if you have different app builds and want each of them to be able to initiate the handshake:"buttons": [ { "type": "otp", ... "supported_apps": [ { "package_name": "<PACKAGE_NAME_1>", "signature_hash": "<SIGNATURE_HASH_1>" }, { "package_name": "<PACKAGE_NAME_2>", "signature_hash": "<SIGNATURE_HASH_2>" }, ... ] } ]
buttons object properties, but this is not recommended as we will stop supporting this method starting with version 21.0:"buttons": [ { "type": "otp", ... "package_name": "<PACKAGE_NAME>", "signature_hash": "<SIGNATURE_HASH>" } ]

code_expiration_minutes property, if present).package_name property in the components array upon template creation) matches the package name set on the intent. The match is determined through the getCreatorPackage method called in the PendingIntent object provided by your application.supported_apps initiated a handshake in the last 10 minutes (or the number of minutes indicated by the template’s code_expiration_minutes property, if present).signature_hash property in the components array upon template creation) matches your installed app’s signing key hash.dependencies {
…
implementation 'com.whatsapp.otp:whatsapp-otp-android-sdk:0.1.0'
…
}
mavenCentral():repositories {
…
mavenCentral()
…
}
com.whatsapp.otp.OTP_RETRIEVED.<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>
WhatsAppOtpIncomingIntentHandler object to handle the intent. The .processOtpCode() method validates the handshake ID against the expected value you stored during handshake initiation and handles errors.public class ReceiveCodeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WhatsAppOtpIncomingIntentHandler incomingIntentHandler = new WhatsAppOtpIncomingIntentHandler();
// Retrieve the expected handshake ID that was stored during handshake initiation
String expectedHandshakeId = retrieveStoredHandshakeId();
incomingIntentHandler.processOtpCode(
getIntent(),
expectedHandshakeId,
(code) -> {
// The handshake ID has been validated by the SDK
validateCode(code);
},
// call your function to handle errors
(error, exception) -> handleError(error, exception));
}
request_id (handshake ID) to ensure the OTP code is coming from a legitimate handshake initiated by your app.public class ReceiveCodeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
// Extract the handshake ID from the intent
String incomingRequestId = intent.getStringExtra("request_id");
// Retrieve the previously stored handshake ID
String storedRequestId = retrieveStoredRequestId();
// Validate the handshake ID matches
if (storedRequestId != null && storedRequestId.equals(incomingRequestId)) {
// use OTP code
String otpCode = intent.getStringExtra("code");
// ...
}
}
}
WhatsAppOtpHandler object and passing in your context to the .sendOtpIntentToWhatsApp() method. The method returns a UUID (handshake ID) that must be stored and used to validate the incoming OTP code later:WhatsAppOtpHandler whatsAppOtpHandler = new WhatsAppOtpHandler();
UUID handshakeId = whatsAppOtpHandler.sendOtpIntentToWhatsApp(context);
// Store handshakeId to validate the received OTP code later
request_id (UUID) that must be stored and validated when receiving the OTP code.private String currentRequestId;
public void sendOtpIntentToWhatsApp() {
// Generate a unique handshake ID
currentRequestId = UUID.randomUUID().toString();
// Store this ID for later validation when receiving the OTP
storeRequestId(currentRequestId);
// Send OTP_REQUESTED intent to both WA and WA Business App
sendOtpIntentToWhatsApp("com.whatsapp", currentRequestId);
sendOtpIntentToWhatsApp("com.whatsapp.w4b", currentRequestId);
}
private void sendOtpIntentToWhatsApp(String packageName, String requestId) {
/**
* Starting with Build.VERSION_CODES.S, it will be required to explicitly
* specify the mutability of PendingIntents on creation with either
* (@link #FLAG_IMMUTABLE} or FLAG_MUTABLE
*/
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? FLAG_IMMUTABLE : 0;
PendingIntent pi = PendingIntent.getActivity(
getApplicationContext(),
0,
new Intent(),
flags);
// Send OTP_REQUESTED intent to WhatsApp
Intent intentToWhatsApp = new Intent();
intentToWhatsApp.setPackage(packageName);
intentToWhatsApp.setAction("com.whatsapp.otp.OTP_REQUESTED");
// WA will use this to verify the identity of the caller app.
Bundle extras = intentToWhatsApp.getExtras();
if (extras == null) {
extras = new Bundle();
}
extras.putParcelable("_ci_", pi);
// Add the handshake ID for secure validation
intentToWhatsApp.putExtra("request_id", requestId);
intentToWhatsApp.putExtras(extras);
getApplicationContext().sendBroadcast(intentToWhatsApp);
}
AndroidManifest.xml file:<queries> <package android:name="com.whatsapp"/> <package android:name="com.whatsapp.w4b"/> </queries>
WhatsAppOtpHandler object:WhatsAppOtpHandler whatsAppOtpHandler = new WhatsAppOtpHandler();
isWhatsAppInstalled method as the clause in an If statement:If (whatsAppOtpHandler.isWhatsAppInstalled(context)) {
// ... do something
}
if (this.isWhatsAppInstalled(context)) {
// ... do something
}
public boolean isWhatsAppInstalled(final @NonNull Context context){
return isWhatsAppInstalled(context, "com.whatsapp") ||
isWhatsAppInstalled(context, "com.whatsapp.w4b");
}
public boolean isWhatsAppInstalled(final @NonNull Context context,
final @NonNull String type){
final Intent intent = new Intent();
intent.setPackage(type);
intent.setAction("com.whatsapp.otp.OTP_REQUESTED");
PackageManager packageManager = context.getPackageManager();
List<ResolveInfo> receivers = packageManager.queryBroadcastReceivers(intent, 0);
return !receivers.isEmpty();
}
}
let schemeURL = URL(string: "whatsapp://otp")!
let isWhatsAppInstalled = UIApplication.shared.canOpenURL(schemeURL)
| Error Code | Description |
|---|---|
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 value |
curl -X POST "https://graph.facebook.com/v23.0/<WHATSAPP_BUSINESS_PHONE_NUMBER_ID>/messages" \ -H "Authorization: Bearer <ACCESS_TOKEN>" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "recipient_type": "individual", "to": "<CUSTOMER_PHONE_NUMBER>", "type": "template", "template": { "name": "<TEMPLATE_NAME>", "language": { "code": "<TEMPLATE_LANGUAGE_CODE>" }, "components": [ { "type": "body", "parameters": [ { "type": "text", "text": "<ONE-TIME PASSWORD>" } ] }, { "type": "button", "sub_type": "url", "index": 0, "parameters": [ { "type": "text", "text": "<ONE-TIME PASSWORD>" } ] } ] } }'
| Placeholder | Description | Sample Value |
|---|---|---|
<CUSTOMER_PHONE_NUMBER> | The customer’s WhatsApp phone number. | 12015553931 |
<ONE-TIME PASSWORD> | The one-time password or verification code to be delivered to the customer. Note that this value must appear twice in the payload. Maximum 15 characters. | J$FpnYnP |
<TEMPLATE_LANGUAGE_CODE> | The template’s language and locale code. | en_US |
<TEMPLATE_NAME> | The template’s name. | verification_code |
{ "messaging_product": "whatsapp", "contacts": [ { "input": "<INPUT>", "wa_id": "<WA_ID>" } ], "messages": [ { "id": "<ID>" } ] }
| Placeholder | Description | Sample Value |
|---|---|---|
<INPUT>String | The customer phone number that the message was sent to. This may not match wa_id. | +16315551234 |
<WA_ID>String | WhatsApp ID of the customer who the message was sent to. This may not match input. | +16315551234 |
<ID>String | WhatsApp message ID. You can use the ID listed after “wamid.” to track your message status. | wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBI3N0EyQUJDMjFEQzZCQUMzODMA |
curl -L 'https://graph.facebook.com/v25.0/105954558954427/messages' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer EAAJB...' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "12015553931",
"type": "template",
"template": {
"name": "verification_code",
"language": {
"code": "en_US"
},
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "J$FpnYnP"
}
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{
"type": "text",
"text": "J$FpnYnP"
}
]
}
]
}
}'
{ "messaging_product": "whatsapp", "contacts": [ { "input": "12015553931", "wa_id": "12015553931" } ], "messages": [ { "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBI4Qzc5QkNGNTc5NTMyMDU5QzEA" } ] }