← Blog

·

WhatsApp OTP: sending verification codes with authentication templates

Updated on October 25, 2026

To send a one-time passcode (OTP) over WhatsApp, Meta requires an authentication template: preset text (“123456 is your verification code.”), an OTP button (copy code, one-tap autofill or zero-tap) and no URLs, media or emojis. You create the template once, Meta approves it and, on every login, your application sends the code as a parameter. For recipients in Brazil, each delivered authentication message costs R$ 0.0350 on the BRL rate card in effect since October 1, 2026, with volume discounts; other markets have their own rates on Meta’s rate cards.

This guide covers the three button types, creation and send payloads, how to set expiration and TTL, and the security practices Meta recommends.

Why it has to be an authentication template

Meta is explicit: only authentication templates may send a one-time passcode for identity verification. Marketing and utility templates cannot. If Meta detects a template from another category being used this way, it gives notice and changes the template status to REJECTED on the first day of the following month, with no option to appeal. The full category rules are in how Meta categorizes your template.

To qualify as authentication, the template must:

  • use Cloud API authentication templates, with fixed text defined by Meta;
  • include an OTP button;
  • contain no URLs, media or emojis, in the content or in parameters;
  • use parameters of up to 15 characters.

Accepted use cases: account creation, account recovery or integrity challenges, and confirmation of orders or transactions.

The text is fixed: what you can configure

You don’t write the body. Meta uses preset text, localized to the template language, and offers two optional add-ons:

OptionFieldEffect
Security disclaimeradd_security_recommendation: true in the bodyAdds “For your security, do not share this code.”
Code expirationcode_expiration_minutes in the footer (1 to 90)Adds “This code expires in N minutes.”
Message time-to-livemessage_send_ttl_seconds (30 to 900)If not delivered within this time, the message is dropped

The default TTL for authentication templates is 10 minutes (templates created before October 23, 2024 had 30 days). Meta recommends a TTL equal to or shorter than the code’s expiration: there is no point delivering a code that has already expired. Since Meta only charges for delivered messages, messages dropped by the TTL are not billed.

Want to try it while you read? Create a Vectalk account and send your first message through the official API. Create free account →

The three button types

TypeHow it worksPlatformApp integration required?
Copy code (COPY_CODE)The user taps the button, the code goes to the clipboard and they paste it into your appAllNo
One-tap (ONE_TAP)The button opens your app with the code already filled inAndroidYes: handshake and an activity to receive the code
Zero-tap (ZERO_TAP)No button: your app receives the code via broadcast and the user never leaves the screenAndroidYes: handshake, broadcast receiver and terms acceptance

On iOS 26 and later, the keyboard itself suggests the code received on WhatsApp, with no integration. On earlier iOS versions, the user sees the copy code button.

One important detail: one-tap and zero-tap have a fallback. If the handshake did not happen, if the app does not match or if the recipient is not on Android, WhatsApp shows the copy code button (zero-tap tries one-tap first). So a one-tap or zero-tap template still works as copy code in the worst case.

Creating a template with a copy code button

With the Cloud API directly, the template is created on the WhatsApp Business account’s message_templates endpoint:

curl -X POST "https://graph.facebook.com/v26.0/YOUR_WABA_ID/message_templates" \
  -H "Authorization: Bearer META_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "verification_code",
    "language": "en_US",
    "category": "AUTHENTICATION",
    "message_send_ttl_seconds": 300,
    "components": [
      { "type": "BODY", "add_security_recommendation": true },
      { "type": "FOOTER", "code_expiration_minutes": 5 },
      {
        "type": "BUTTONS",
        "buttons": [
          { "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" }
        ]
      }
    ]
  }'

Note there is no text field in the body: Meta fills it in. The button label (text) is optional and up to 25 characters; without it, Meta uses a localized default. After creation, the OTP button shows up as type URL when you query the template, which is expected.

One-tap variant (Android)

For one-tap, change the button and list the authorized apps:

{
  "type": "OTP",
  "otp_type": "ONE_TAP",
  "text": "Copy code",
  "autofill_text": "Autofill",
  "supported_apps": [
    { "package_name": "com.example.app", "signature_hash": "K8a/AINcGX7" }
  ]
}

One-tap rules, according to the autofill documentation:

  • supported_apps accepts up to 5 package and hash pairs;
  • package_name has up to 224 characters, with at least two dot-separated segments;
  • signature_hash is exactly 11 characters;
  • autofill_text and text have up to 25 characters;
  • the app must start a handshake with WhatsApp before the send and have an activity with the com.whatsapp.otp.OTP_RETRIEVED action;
  • the handshake is valid for 10 minutes, or for code_expiration_minutes if set.

Meta recommends the official SDK for the handshake. The PendingIntent-based method is being deprecated on October 15, 2026.

Zero-tap variant (Android)

Zero-tap uses "otp_type": "ZERO_TAP", the same supported_apps and the field zero_tap_terms_accepted: true. Without that acceptance, the template is not created. The app needs a broadcast receiver for com.whatsapp.otp.OTP_RETRIEVED. Even with zero-tap, the user sees the fixed-text message in WhatsApp.

To debug one-tap and zero-tap on Android, WhatsApp sends error signals via broadcast with the com.whatsapp.OTP_ERROR action, with keys such as otp_request_expired and missing_handshake_or_disorder.

Sending the code

The send is a regular template, with one twist: the code appears twice, in the body parameter and in the button parameter. The code can have up to 15 characters.

{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "15551234567",
  "type": "template",
  "template": {
    "name": "verification_code",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [{ "type": "text", "text": "482913" }]
      },
      {
        "type": "button",
        "sub_type": "url",
        "index": "0",
        "parameters": [{ "type": "text", "text": "482913" }]
      }
    ]
  }
}

A Node.js function that generates the code, stores its hash and sends it:

import crypto from 'node:crypto';

export async function sendOtp(phone, db) {
  const code = String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
  const hash = crypto.createHash('sha256').update(code).digest('hex');
  await db.saveOtp(phone, hash, Date.now() + 5 * 60 * 1000); // 5 minutes

  const body = {
    messaging_product: 'whatsapp',
    recipient_type: 'individual',
    to: phone,
    type: 'template',
    template: {
      name: 'verification_code',
      language: { code: 'en_US' },
      components: [
        { type: 'body', parameters: [{ type: 'text', text: code }] },
        { type: 'button', sub_type: 'url', index: '0', parameters: [{ type: 'text', text: code }] },
      ],
    },
  };

  const r = await fetch(process.env.WHATSAPP_URL, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.WHATSAPP_TOKEN}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  return r.json(); // keep the wamid to match it with the status webhook
}

Track the status through the webhook: if delivered does not arrive within the TTL, assume the message was dropped and offer another channel. The possible states are covered in message status via webhook.

Security: what Meta recommends

Meta’s authentication best practices include some points worth taking seriously:

  • Recycled numbers. WhatsApp only verifies number ownership at registration. A number can change hands. On the Cloud API, store the identity hash you receive after the first validated OTP and include it in later sends: if the number now belongs to someone else, delivery fails instead of reaching the new owner.
  • Extra challenge for sensitive flows. For account recovery, combine the WhatsApp code with another verification.
  • Forwarding disabled. WhatsApp disables forwarding of authentication messages, which reduces phishing.
  • Channel choice. Get consent and offer options (WhatsApp, email, SMS). Confirm which number the user has on WhatsApp, as it may differ from their SMS number.
  • WhatsApp installed. On Android, check for the com.whatsapp and com.whatsapp.w4b packages; on iOS, check whether the whatsapp://otp scheme can be opened.
  • One account per business. Each business needs its own account, Messaging account and phone number. Sharing them across businesses violates policy.

How much each code costs

Meta charges authentication per delivered message, and rates vary by recipient country. For recipients in Brazil, the BRL rate card in effect since October 1, 2026 has progressive monthly volume discounts, aggregated across all accounts in the business portfolio:

Authentication messages per month (Brazil)Price per message (BRL)
Up to 500,000R$ 0.0350
500,001 to 3,000,000R$ 0.0333
3,000,001 to 5,250,000R$ 0.0315
5,250,001 to 10,000,000R$ 0.0298
10,000,001 to 20,000,000R$ 0.0280
Over 20,000,000R$ 0.0263

Tiers reset every month. The authentication-international rate, charged in some markets, does not apply to Brazil. Messages sent inside a free entry point window, opened by Click-to-WhatsApp ads, are not charged. To model volumes, see how much a WhatsApp campaign costs.

How to do it with Vectalk

On Vectalk, the authentication template is created at POST /api/templates with the same body as Meta’s, using an instance token:

curl -X POST https://apis.vectalk.com.br/api/templates \
  -H "Authorization: Bearer YOUR_INSTANCE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "verification_code",
    "language": "en_US",
    "category": "AUTHENTICATION",
    "components": [
      { "type": "BODY", "add_security_recommendation": true },
      { "type": "FOOTER", "code_expiration_minutes": 5 },
      { "type": "BUTTONS", "buttons": [ { "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" } ] }
    ]
  }'

Sending uses POST https://apis.vectalk.com.br/api/messages with the payload from the previous section. In the Node.js code above, just point WHATSAPP_URL to that endpoint and use your Vectalk token. See the templates documentation and create your account at apps.vectalk.com.br/register.

Frequently asked questions

Can I write my own verification code text?

No. Authentication template text is preset by Meta. You only choose the language, whether to include the security disclaimer, the expiration in minutes and the button labels.

Can the code include letters?

Yes, Meta refers to codes that are usually alphanumeric. The limit is 15 characters per parameter, and the same value goes in the body and the button.

Does one-tap work on iPhone?

No. One-tap and zero-tap are Android only. On iPhone, the user sees the copy code button; on iOS 26 and later, the keyboard also suggests the code automatically.

Do I need a separate template for each language?

Yes. Each language is its own template. For bulk creation, Meta’s upsert_message_templates endpoint accepts a list of languages at once.

Can an authentication template be rejected?

Yes, for invalid format or parameters. The duplicate rule does not apply, since the text is standard. See WhatsApp template rejected.


Official sources checked on October 25, 2026: Authentication templates, Copy code button, One-tap autofill button, Zero-tap, Error signals, Authentication best practices, Time-to-live, Template categorization, Pricing and the “BRL volume tiers” spreadsheet (effective October 1, 2026).

Try the official WhatsApp API today

Create your account, generate a token and send your first message with the quick-start guide. Same payloads and webhooks as Meta's Cloud API.