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 -X POST "https://graph.facebook.com/v19.0/<WHATSAPP_BUSINESS_ACCOUNT_ID>/message_templates" \ -H "Authorization: Bearer <ACCESS_TOKEN>" \ -H "Content-Type: application/json" \ -d ' { "name": "<TEMPLATE_NAME>", "language": "<TEMPLATE_LANGUAGE>", "category": "authentication", "message_send_ttl_seconds": <TIME_TO_LIVE>, "components": [ { "type": "body", "add_security_recommendation": <SECURITY_RECOMMENDATION> }, { "type": "footer", "code_expiration_minutes": <CODE_EXPIRATION> }, { "type": "buttons", "buttons": [ { "type": "otp", "otp_type": "zero_tap", "text": "<COPY_CODE_BUTTON_TEXT>", "autofill_text": "<AUTOFILL_BUTTON_TEXT>", "zero_tap_terms_accepted": <TERMS_ACCEPTED>, "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. Zero-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 |
<COPY_CODE_BUTTON_TEXT>String | Optional. Copy code button label text. If the message fails the eligibility check and displays a copy code button, the button will use this text label. If omitted, and the message fails the eligibility check and displays a copy code button, the text will default to a pre-set value localized to the template’s language. For example, Copy Code for English (US).Maximum 25 characters. | Copy Code |
<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. If the message fails the eligibility check and displays a one-tap autofill button, 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. If the message fails the eligibility check and displays a one-tap autofill button, the button will be disabled 10 minutes from when the message was sent. Minimum 1, maximum 90. | 5 |
<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 fixed 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. | zero_tap_auth_template |
<TERMS_ACCEPTED>Boolean | Required. Set to true to indicate that you understand that your use of zero-tap authentication is subject to the WhatsApp Business Terms of Service, and that it’s your responsibility to ensure your customers expect that the code will be automatically filled in on their behalf when they choose to receive the zero-tap code through WhatsApp.If set to false, the template will not be created as you need to accept zero-tap terms before creating zero-tap enabled message templates. | true |
<TIME_TO_LIVE>Integer | Optional. Authentication message time-to-live value, in seconds. See Time-To-Live. | 60 |
curl 'https://graph.facebook.com/v25.0/102290129340398/message_templates' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer EAAJB...' \
-d '
{
"name": "zero_tap_auth_template",
"language": "en_US",
"category": "authentication",
"message_send_ttl_seconds": 60,
"components": [
{
"type": "body",
"add_security_recommendation": true
},
{
"type": "footer",
"code_expiration_minutes": 5
},
{
"type": "buttons",
"buttons": [
{
"type": "otp",
"otp_type": "zero_tap",
"text": "Copy Code",
"autofill_text": "Autofill",
"zero_tap_terms_accepted": true,
"supported_apps": [
{
"package_name": "com.example.luckyshrub",
"signature_hash": "K8a/AINcGX7"
}
]
}
]
}
]
}'
{ "id": "594425479261596", "status": "PENDING", "category": "AUTHENTICATION" }
./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. See One-Tap Autofill Button Class.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:1.0.0'
…
}
mavenCentral():repositories {
…
mavenCentral()
…
}
<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>
BroadcastReceiver, then define the onReceive method, passing in your context and intent. Instantiate a WhatsAppOtpIncomingIntentHandler object, then run the .processOtpCode() method which will receive the intent, validate the handshake ID against the expected value you stored during handshake initiation, and handle errors.public class OtpCodeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
WhatsAppOtpIncomingIntentHandler whatsAppOtpIncomingIntentHandler = new WhatsAppOtpIncomingIntentHandler();
// Retrieve the expected handshake ID that was stored during handshake initiation
String expectedHandshakeId = retrieveStoredHandshakeId();
whatsAppOtpIncomingIntentHandler.processOtpCode(intent,
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) from the intent to ensure the OTP code is coming from a legitimate handshake initiated by your app:public class OtpCodeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent 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");
// ...
}
}
}
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>
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
}
| 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/<API_VERSION>/<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" } ] }