Scope
Authentication messages are template messages used to deliver one-time passcodes (OTP/verification codes). If you deliver login or verification codes over WhatsApp, use authentication templates — not utility or marketing templates.
Authentication Pages in This Section
- OTP Handshake & Android SDK — handshake mechanics, OTP Android SDK, eligibility checks, request_id validation, error codes
- Bulk Management —
upsert_message_templates(multi-language create/update) andmessage_template_previews(generate preview text in any language) - Authentication Best Practices — security (recycled phone numbers, identity hash binding) and UX (opt-in, isWhatsAppInstalled checks, multi-app support)
Preset Text Structure
Authentication templates use fixed, non-customizable preset text:
<VERIFICATION_CODE> is your verification code.(always shown)- Optional security disclaimer:
For your security, do not share this code.(toggle viaadd_security_recommendation: true) - Optional expiration warning:
This code expires in <NUM_MINUTES> minutes.(toggle viacode_expiration_minutesinteger)
You cannot edit the preset text. The button label can be customized with text / autofill_text, but omitting those fields is usually safer because Meta applies the preset localized label for the template language.
Button Types: Copy Code vs One-Tap vs Zero-Tap
| Type | UX | Platform | Requires app integration | Falls back to |
|---|---|---|---|---|
copy_code | Tap → code copied to clipboard, user pastes into your app | All platforms | No | — |
one_tap | Tap → autofills code into your app | Android only | Yes — handshake + Activity intent | copy_code button if eligibility check fails |
zero_tap | No tap — code broadcast directly to your app | Android only | Yes — handshake + BroadcastReceiver + zero_tap_terms_accepted: true | one_tap first, then copy_code |
Recommendation: Start with copy_code (universal), upgrade to one_tap (Android autofill) or zero_tap (silent delivery) once you have an Android app and want lower OTP-friction conversion.
For one-tap and zero-tap, the handshake and SDK details are on a dedicated page: OTP Handshake & Android SDK.
Linked Device Security
Authentication messages now feature linked device security: messages are only delivered to a user's primary WhatsApp device. On linked devices (WhatsApp Web, Desktop, secondary phones), the message body is masked with a prompt instructing the user to view it on their primary device.
This feature is enabled by default, requires no code changes, and cannot be configured or disabled. It applies only to Cloud API authentication messages.
Requirements and Constraints
- Use template category
AUTHENTICATION. - Use the preset authentication text structure and OTP button model.
- URLs, media, and emojis are not supported in authentication template content.
- OTP delivery should target users who explicitly requested the code.
Common operational prerequisites:
- Business verification completed.
- Sufficient messaging limit tier for authentication use.
Template Building Blocks
Authentication templates are assembled from three component types:
body— fixed auth text + optionaladd_security_recommendation: truefooter— optionalcode_expiration_minutes(integer, 1–90)buttons— single OTP button (copy_code,one_tap, orzero_tap)
You cannot add a header, additional buttons, or any other component to an authentication template.
Time-To-Live (TTL) and Code Expiration
Two related but distinct settings:
| Property | Where set | What it controls |
|---|---|---|
message_send_ttl_seconds | Template root | Maximum delivery retry window — Meta will keep retrying delivery for this many seconds before giving up. Useful when network or app-state issues delay delivery. |
code_expiration_minutes | footer component | The expiration shown in the message text and the timeframe after which the OTP button becomes disabled. Defaults to 10 minutes if omitted. Min 1, max 90. |
Keep TTL short so users don't receive expired OTPs. A typical configuration: message_send_ttl_seconds: 60 paired with code_expiration_minutes: 5.
Copy-Code Templates
Copy-code templates are the most compatible OTP option. They do not require any Android handshake or signing integration, work on iOS, and never fall back further than themselves.
{
"name": "authentication_code_copy_code_button",
"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": "copy_code" }
]
}
]
}
Omit the copy-code button text to let Meta use the preset localized label for the template language, such as "Copy code" for English.
Note: in the create request the button type is otp, but the saved template stores it as url. A GET on the created template will return url — that is expected.
One-Tap Templates
One-tap templates autofill the OTP into your Android app. If the message fails its eligibility check, WhatsApp transparently falls back to a copy-code button.
{
"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": 5 },
{
"type": "buttons",
"buttons": [
{
"type": "otp",
"otp_type": "one_tap",
"supported_apps": [
{ "package_name": "com.example.myapplication", "signature_hash": "K8a/AINcGX7" }
]
}
]
}
]
}
supported_apps lets you list up to 5 package_name + signature_hash pairs (different app builds, different platforms, etc.). Eligibility check requires the actual installed app's signature to match one of these. See OTP Handshake & Android SDK for the handshake protocol.
The legacy form — defining package_name and signature_hash directly on the button (without supported_apps) — works on Graph API v20.0 and older, but is deprecated and should not be used in new code.
Zero-Tap Templates
Zero-tap delivers the OTP silently to your app via Android broadcast receiver. The user does not see (or need to tap) any button.
{
"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",
"zero_tap_terms_accepted": true,
"supported_apps": [
{ "package_name": "com.example.myapplication", "signature_hash": "K8a/AINcGX7" }
]
}
]
}
]
}
zero_tap_terms_accepted: true is required — it acknowledges that you understand zero-tap is subject to the WhatsApp Business Terms of Service and that your customers expect codes to be auto-filled silently. Setting it to false blocks template creation.
You also include the text and autofill_text button properties even though zero-tap may never display either — the eligibility-failure fallback chain (zero-tap → one-tap → copy-code) needs them.
Sending an Authentication Template
Provide the OTP value in both the body parameter and the button parameter (the value must appear twice in the payload):
{
"messaging_product": "whatsapp",
"to": "12015550123",
"type": "template",
"template": {
"name": "verification_code",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "839201" }
]
},
{
"type": "button",
"sub_type": "url",
"index": 0,
"parameters": [
{ "type": "text", "text": "839201" }
]
}
]
}
}
The OTP code maximum length is 15 characters.
For one-tap and zero-tap, you must initiate the handshake before calling the send endpoint, otherwise the eligibility check fails and WhatsApp falls back to copy-code.
Dualhook supports template management and operational monitoring. Authentication delivery is still controlled by Meta policies, device eligibility, and app integration readiness. Dualhook does not bypass handshake or signing requirements.
"I didn't request a code" Webhook
When a user receives an OTP they didn't request, they can tap an "I didn't request a code" button (currently in beta, rolling out incrementally). This triggers a messages webhook with type: "button" and payload DID_NOT_REQUEST_CODE:
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "102290129340398",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15550001234",
"phone_number_id": "123456789012345"
},
"messages": [
{
"context": {
"from": "12015550123",
"id": "wamid.HBgLMTIxMTU1NTE0NTYVAgARGBJDMDEyMTFDNTE5NkFCOUU3QTEA"
},
"from": "12015550123",
"id": "wamid.HBgLMTIxMTU1NTE0NTYVAgASGCBBQ0I3MjdCNUUzMTE0QjhFQkM4RkQ4MEU3QkE0MUNEMgA=",
"timestamp": "1753919111",
"type": "button",
"button": {
"payload": "DID_NOT_REQUEST_CODE",
"text": "I didn't request a code"
}
}
]
},
"field": "messages"
}
]
}
]
}
This webhook arrives on the messages field, so in Dualhook setups it goes directly to your endpoint via Webhook Override — Dualhook does not ingest it.
If you receive this webhook for a recipient, treat it as a strong negative signal: that user did not initiate the code request, which can indicate credential stuffing or unauthorized account access on your side. Lock the account, invalidate active sessions, and do not auto-resend codes.
PendingIntent Deprecation (Oct 15, 2026)
The PendingIntent-based handshake method for one-tap and zero-tap will be deprecated on October 15, 2026 (extended from earlier deadline).
If you currently initiate handshakes or verify app identity via PendingIntent, migrate to the OTP Android SDK before that date. The SDK uses request_id (UUID) validation and is the only supported handshake method going forward.
See OTP Handshake & Android SDK for migration details.
International Rates
A separate authentication-international rate applies to authentication template messages delivered to users in: Egypt, India, Indonesia, Malaysia, Nigeria, Pakistan, Saudi Arabia, South Africa, and the United Arab Emirates — but only when the sending business is based outside that country (per Meta's primary_business_location field on the WABA).
Eligibility kicks in automatically at >750,000 outside-CSW messages over a moving 30-day window to users in those countries (across all WABAs in the portfolio). Once eligible, Meta provides 30 days notice via email and an account_update webhook (AUTH_INTL_PRICE_ELIGIBILITY_UPDATE) before the higher rate applies. See Pricing → Authentication-International Rates for the eligibility check and webhook payload.