GhalaGhalaHelp Center
Back to ghala.ioghala.ioSign in
  • Getting Started
    • Ghala Documentation
    • Send Your First WhatsApp Message via API
    • Receiving Events with Webhooks
  • Commerce
    • Start Selling on WhatsApp
    • Set Up the AI Sales Agent
    • Get Paid with Snippe
  • Ai Automation
    • Configuring AI Auto-Reply for WhatsApp
    • Setting Up Human Handover Protocol
  • Campaigns Messaging
    • WhatsApp Message Templates: Complete Guide
    • Bulk WhatsApp Messaging: Complete Campaign Guide
  • Contacts Crm
    • WhatsApp Contact Management Guide
  • Best Practices
    • WhatsApp Customer Support Best Practices
    • Message Template Best Practices
  • Api Reference
    • Ghala Developer API Reference
    • Ghala API vs Meta Cloud API
    • Supported Capabilities
    • Multiple Numbers and Multi-Tenant Platforms
    • Errors and Retries
    • Limits and Quotas
    • Authentication and Access Tokens
    • Connecting and Onboarding a Number
    • Templates and Media
    • Send One-Time Codes
    • Developer Tooling
    • Versioning and Changelog

Products

  • Ghala
  • Sarufi
  • Snippe
  • Sema

Explore

  • Use Cases
  • Pricing
  • Ghala Academy
  • Blog

Developers

  • Docs
  • API Reference
  • API Quickstart
  • Webhooks Guide

Contact

  • SkyCity Mall, 9th Floor, Dar es Salaam, Tanzania
  • info@ghala.io
  • +255 699 920 009
© 2026 Neurotech Company LimitedTerms of ServicePrivacy PolicySitemap
  1. Help Center
  2. Api Reference
  3. Send One-Time Codes

Send One-Time Codes

v2

Send sign-in and verification codes on WhatsApp with an authentication template: create it, send the code once, and handle every error.

What an authentication template is

An authentication template is a WhatsApp template in the AUTHENTICATION category. It sends a one-time code, such as a sign-in or verification code, with a copy code button the customer taps to copy it.

Meta writes the message, in the template's language. You do not write any text:

  • Body: "482913 is your verification code." With the security reminder on, it adds "For your security, do not share this code."
  • Footer: optional. With an expiry set, it reads "This code expires in 10 minutes."
  • Button: one copy code button. Its text is yours, or Meta's own "Copy code" when you leave it blank.

You supply one thing at send time: the code.

Create the template in the dashboard

The public API cannot create templates. Create it once in the dashboard:

  1. Go to Templates and click New template
  2. Name it in lowercase with underscores, for example login_code, and pick the language
  3. Set the category to Authentication
  4. Choose whether to Add the security reminder
  5. Set Code expires after to a whole number of minutes from 1 to 90, or turn on No expiry
  6. Optionally set the Button text, at most 25 characters and no emoji
  7. Submit, and wait for Meta's approval

An authentication template cannot have a header, its own body or footer text, named placeholders, or any button other than the one copy code button. Once created it is not edited in the dashboard, because Meta writes its text. One-tap and zero-tap code buttons are not supported.

The expiry also tells WhatsApp when to stop trying to deliver the code, capped at 15 minutes. With no expiry, Meta's default of 10 minutes applies.

The message templates guide covers review times and the Templates page.

Find the template

GET /api/v2/templates lists authentication templates like any other, with "category": "AUTHENTICATION". There is no category filter on the endpoint, so filter the list yourself:

curl "https://v2.ghala.io/api/v2/templates?sendable=true&limit=100" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

An approved authentication template, once Ghala has synced it from Meta:

{
  "id": "01JZ8Q4R2K7N3M5P9V1X6T0B2C",
  "name": "login_code",
  "language": "en_US",
  "category": "AUTHENTICATION",
  "status": "APPROVED",
  "sendable": true,
  "unsendable_reason": null,
  "components": [
    {
      "type": "BODY",
      "text": "*{{1}}* is your verification code. For your security, do not share this code.",
      "add_security_recommendation": true
    },
    {
      "type": "FOOTER",
      "text": "This code expires in 10 minutes.",
      "code_expiration_minutes": 10
    },
    {
      "type": "BUTTONS",
      "buttons": [
        {
          "type": "URL",
          "otp_type": "COPY_CODE",
          "text": "Copy code",
          "url": "https://www.whatsapp.com/otp/code/?otp_type=COPY_CODE&code=otp{{1}}"
        }
      ]
    }
  ],
  "quality_score": "GREEN",
  "approved_at": "2026-10-01T09:12:00Z",
  "status_changed_at": "2026-10-01T09:12:00Z"
}

Before its first sync, the same template can show the shape it was created with: a body with no text and a button of "type": "OTP". Branch on category, not on the component shape.

Send a code

POST /api/v2/messages with type: "template". Put the code once, as the body's one text parameter:

curl -X POST https://v2.ghala.io/api/v2/messages \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: login-7f3a9c-attempt-1" \
  -d '{
    "to": "255712345678",
    "type": "template",
    "template_name": "login_code",
    "template_language": "en_US",
    "template_components": [
      { "type": "body", "parameters": [{ "type": "text", "text": "482913" }] }
    ]
  }'
import os, requests

resp = requests.post(
    "https://v2.ghala.io/api/v2/messages",
    headers={
        "Authorization": f"Bearer {os.environ['ACCESS_TOKEN']}",
        "Idempotency-Key": "login-7f3a9c-attempt-1",
    },
    json={
        "to": "255712345678",
        "type": "template",
        "template_name": "login_code",
        "template_language": "en_US",
        "template_components": [
            {"type": "body", "parameters": [{"type": "text", "text": "482913"}]}
        ],
    },
)
print(resp.json())
const resp = await fetch("https://v2.ghala.io/api/v2/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "login-7f3a9c-attempt-1",
  },
  body: JSON.stringify({
    to: "255712345678",
    type: "template",
    template_name: "login_code",
    template_language: "en_US",
    template_components: [
      { type: "body", parameters: [{ type: "text", text: "482913" }] },
    ],
  }),
});
console.log(await resp.json());

Meta needs the code twice, in the body and in the copy code button. Ghala fills the button from the body, so what goes to Meta is:

[
  { "type": "body", "parameters": [{ "type": "text", "text": "482913" }] },
  {
    "type": "button",
    "sub_type": "url",
    "index": "0",
    "parameters": [{ "type": "text", "text": "482913" }]
  }
]

You may send the button component yourself, as type button, sub_type url, index "0". It must then carry the same code as the body.

A code template is for one person at one moment. Send it from your server when the customer asks for a code, never from a campaign.

The code rules

Ghala checks the code before anything reaches WhatsApp:

  • A JSON string, never a number. "text": "482913", not "text": 482913. A string keeps a leading zero; a number is refused.
  • 1 to 15 characters. A blank code is refused.
  • No new line or tab, no space at the start or the end, and not four spaces in a row.
  • No link: nothing starting https://, http:// or www., and nothing that reads as a domain, such as example.com.
  • No emoji.
  • Only the body and the copy code button. A header or any other component is refused.

Each refusal is 422 invalid_one_time_code, and its message says which rule failed.

The response

A send returns 200 with the recorded message. The code is not in it:

{
  "id": "01M3W01AMD5SAJNXR9MTB4QHV0",
  "direction": "OUTBOUND",
  "message_type": "template",
  "content": "Verification code sent",
  "status": "SENT",
  "source": "HUMAN",
  "wa_message_id": "wamid.bff38cd60dea4478a405dd4a81dcc36b",
  "media_url": null,
  "media_mime_type": null,
  "media_filename": null,
  "media_duration_ms": null,
  "interactive": null,
  "referral": null,
  "sent_at": "2026-10-01T15:06:28.362866Z",
  "delivered_at": null,
  "read_at": null,
  "played_at": null,
  "failed_at": null,
  "failure_reason": null,
  "template_name": "login_code",
  "redacted": true,
  "created_at": "2026-10-01T15:06:28.365433Z"
}

redacted: true marks a message whose code was not kept. Delivery is asynchronous: the message.status event reports delivered, read, or failed.

The code is never stored

Ghala sends the code to WhatsApp and keeps no copy of it:

  • The message is kept in the conversation with the template's name and language, and reads Verification code sent. The dashboard inbox shows it that way, without the code.
  • Where the code would otherwise appear, in the recorded components, the message.sent event, logs, error reports, and an error message returned to you, it reads [code withheld].
  • Idempotency compares requests by a keyed hash, so a stored key cannot be used to recover the code.

Keep the code on your side if you need to check what the customer types back. Ghala cannot tell you which code it sent.

Retries and idempotency

Send an Idempotency-Key on every code send. A timeout must not become a second code on the customer's phone.

  • The same key and the same body within 24 hours replays the first response, with Idempotency-Replayed: true. Nothing is sent again.
  • The same key with another code is 422 idempotency_key_reused. A new code is a new request, so give it a new key.
  • A failed send releases its key, so the retry is a real attempt.

Derive the key from the attempt, for example the sign-in attempt id, so a retry after a crash reuses it.

Errors

Every error has the same body, a stable code and a message in plain words:

{
  "code": "invalid_one_time_code",
  "message": "the copy code button's code must match the body's code"
}
Status code When Retry?
400 invalid_template The template is not in that language (the message names the ones that exist), or it is in a state WhatsApp will not send: rejected, paused, disabled, archived, and others No
400 ambiguous_number The token is held by more than one connected number and X-Phone-Number-Id did not say which; the ids are in phone_number_ids No, add the header
401 not_authenticated Missing, unknown, or rotated-away token No
402 plan_feature_locked The plan does not include API access No
402 plan_limit_reached The number is new to the workspace, and adding it would pass the plan's contact limit No
403 number_not_for_token X-Phone-Number-Id names a number this token does not hold No
403 account_suspended The workspace is suspended No
409 idempotency_in_progress A request with this key is still running Yes, after a short pause
422 invalid_one_time_code The code breaks a rule above, the button's code differs from the body's, or a component other than the body and the button was sent No
422 template_not_found The template is not on the number's WhatsApp account, or WhatsApp could not be asked No; sync or check the name
422 idempotency_key_reused This key was used for a different request, including another code No, use a new key
422 validation_error The body is malformed, for example a missing to; detail lists the fields No
429 rate_limited WhatsApp is rate-limiting this number Yes, with backoff and the same key
502 send_failed WhatsApp refused the message, or the number is not connected; the reason is in message Depends on the reason
500 internal_error Something failed on Ghala's side Yes, with the same key

A template is the one message type allowed outside the 24-hour window, so a code send never gets 409 outside_messaging_window, even to a customer who has never written to you.

A template Ghala has not synced yet is looked up on WhatsApp before the send, so its code is recognised and withheld on the first send. If WhatsApp does not have it, or cannot be asked, the send is refused with template_not_found rather than forwarded.

Errors and Retries has the full catalogue and a retry loop.

Where a code template cannot be used

  • Campaigns. An authentication template cannot be a campaign step, and a campaign is not launched with one.
  • The inbox composer. Authentication templates are not offered when you send a template from a conversation.

Send a test from the dashboard

Once Meta approves the template, open it on the Templates page and click Send test. Pick a contact, such as your own number, type a code, and send. It goes out exactly as an API send does, and is recorded without the code.

Checklist

  • An approved template with category Authentication, found with GET /api/v2/templates
  • The exact template_language it was approved in
  • The code as a JSON string in the body's one text parameter, 1 to 15 characters, no link, emoji, new line, tab, or space at either end
  • An Idempotency-Key per attempt, and a new key for a new code
  • The code kept on your side to check against what the customer types
  • 422 invalid_one_time_code and template_not_found handled as fixes, 429 and 5xx retried with the same key
  • Tested with Send test on your own number first

What's next

  • Templates and Media: every other kind of template send
  • Ghala Developer API Reference: the send and templates endpoints
  • Errors and Retries: the full catalogue and what is safe to retry
  • WhatsApp Message Templates: creating templates in the dashboard
PreviousTemplates and MediaNextDeveloper Tooling

On this page

  • What an authentication template is
  • Create the template in the dashboard
  • Find the template
  • Send a code
  • The code rules
  • The response
  • The code is never stored
  • Retries and idempotency
  • Errors
  • Where a code template cannot be used
  • Send a test from the dashboard
  • Checklist
  • What's next