Send OTP (authentication templates)
Authentication templates are the only supported way to send OTP /
verification codes on WhatsApp. They use category AUTHENTICATION, have
fixed body copy generated by Meta, and usually include a copy-code
button that Meta stores internally as a URL button at approval time.
Kirimdev forwards your components array to Meta verbatim — the
send shape below matches what Meta’s
authentication template guide
expects. It is not the same as marketing/utility copy_code buttons
documented on Buttons.
Prerequisites
Section titled “Prerequisites”- An approved template with category
AUTHENTICATIONon the WhatsApp account behind$PHONE_ID. - The exact
languagecode Meta approved (e.g.id,en_US) — confirm withGET /v1/{phone_number_id}/templates/{name}.
Create an authentication template
Section titled “Create an authentication template”Authentication templates are auto-approved by Meta (no 24-hour marketing review). Body text is fixed; you only configure security disclaimer, optional expiry footer, and the OTP button type.
curl -X POST "https://api.kirimdev.com/v1/$PHONE_ID/templates" \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "otp_verification", "category": "AUTHENTICATION", "language": "id", "messageSendTtlSeconds": 600, "components": [ { "type": "BODY", "add_security_recommendation": true }, { "type": "FOOTER", "code_expiration_minutes": 5 }, { "type": "BUTTONS", "buttons": [{ "type": "OTP", "otp_type": "COPY_CODE" }] } ] }'import { Kirim } from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })
const template = await kirim .phoneNumbers(process.env.PHONE_ID!) .templates.create({ name: 'otp_verification', category: 'AUTHENTICATION', language: 'id', messageSendTtlSeconds: 600, components: [ { type: 'BODY', add_security_recommendation: true }, { type: 'FOOTER', code_expiration_minutes: 5 }, { type: 'BUTTONS', buttons: [{ type: 'OTP', otp_type: 'COPY_CODE' }], }, ], })
console.log(template.status) // usually "approved" quicklyAfter creation (or POST .../templates/sync), inspect
GET .../templates/otp_verification. The BUTTONS component typically
shows type: "URL" with a WhatsApp OTP URL containing otp{{1}} — that
is normal.
Send the OTP code
Section titled “Send the OTP code”Pass the same one-time code in two places:
body→parameters[0].textbutton→sub_type: "url",index: 0,parameters[0].text
The code must be ≤ 15 characters (Meta limit for authentication parameters). No URLs, media, or emoji in the value.
curl -X POST \ https://api.kirimdev.com/v1/$PHONE_ID/messages \ -H "Authorization: Bearer $KIRIM_KEY" \ -H "Content-Type: application/json" \ -d '{ "messaging_product": "whatsapp", "to": "+628123456789", "type": "template", "template": { "name": "otp_verification", "language": "id", "components": [ { "type": "body", "parameters": [ { "type": "text", "text": "847291" } ] }, { "type": "button", "sub_type": "url", "index": 0, "parameters": [ { "type": "text", "text": "847291" } ] } ] } }'import { Kirim } from '@kirimdev/sdk'
const kirim = new Kirim({ apiKey: process.env.KIRIM_KEY! })const phone = kirim.phoneNumbers(process.env.PHONE_ID!)
const code = '847291'
await phone.messages.send({ messaging_product: 'whatsapp', to: '+628123456789', type: 'template', template: { name: 'otp_verification', language: 'id', components: [ { type: 'body', parameters: [{ type: 'text', text: code }], }, { type: 'button', sub_type: 'url', index: 0, parameters: [{ type: 'text', text: code }], }, ], },})The API returns status: "pending" immediately; poll
GET /v1/{phone_number_id}/messages/{id} or subscribe to a
message.status webhook for sent / failed.
Body only (no copy button)
Section titled “Body only (no copy button)”If your approved template has no dynamic button (uncommon for OTP), send
only the body component and omit the button block entirely.
Troubleshooting (#100) Invalid parameter
Section titled “Troubleshooting (#100) Invalid parameter”| Mistake | Fix |
|---|---|
sub_type: "copy_code" + coupon_code on an AUTHENTICATION template | Use sub_type: "url" and parameters[].type: "text" (see above) |
copy_code works for MARKETING/UTILITY coupon buttons only | See Buttons — different category |
| OTP in body ≠ OTP in button | Both values must match |
Wrong language (id vs id_ID) | Copy from GET .../templates/{name} |
Button index not 0 | Match the button order in the approved template |
Extra button block when template has no {{ in button URL | Drop the button component |