Create WABA Template
Automate this endpoint with an AI agent using the 2chat-whatsapp-waba skill:
npx skills add 2ChatCo/agent-skills -s 2chat-whatsapp-waba
Create a WhatsApp Business API (WABA) message template and submit it to WhatsApp for review.
Requires a WhatsApp Business API (WABA) channel connected to your 2Chat account. See How to connect WABA to 2Chat.
A new template is created with status PENDING and reviewed by WhatsApp — usually within minutes, occasionally up to 24 hours. It cannot be sent until its status is APPROVED. Follow it with Get WABA Template or subscribe to the whatsapp.waba.template.status.updated webhook.
Some templates — authentication ones in particular — are approved during this call, in which case the response already says APPROVED. Read template.status instead of assuming.
Endpoint
POST https://api.p.2chat.io/open/waba/templates
Authentication
Send your API key in the X-User-API-Key header. See Authentication.
Request body
| Parameter | Description |
|---|---|
phone_number | Your WABA number in E.164 format: no spaces, with leading plus sign. Required. |
name | Template name. Lowercase letters, digits and underscores only, up to 512 characters. Required. |
language | WhatsApp language code, e.g. en, en_US, es_MX, pt_BR. Required. |
category | MARKETING, UTILITY or AUTHENTICATION. Required. WhatsApp may recategorize the template — read template.category in the response. |
body | Message body, up to 1024 characters. Variables are written {{1}}, {{2}}, … Required, except for AUTHENTICATION templates, where WhatsApp writes the body for you. |
header | Optional object: {"format": "TEXT", "text": "..."}. Header text is limited to 60 characters and at most one variable. |
footer | Optional footer text, up to 60 characters. No variables. |
buttons | Optional array of button objects. See Buttons. |
examples | Sample values per component: {"body": [...], "header": [...], "button": [...]}. Required for every component that contains variables — one value per variable, in order. |
add_security_recommendation | AUTHENTICATION only. When true, WhatsApp appends its "do not share this code" line. |
code_expiration_minutes | AUTHENTICATION only. Between 1 and 90. Adds an expiry line to the footer. |
description | Optional note stored with the template in 2Chat. Only kept for Gupshup-backed channels. |
The examples keys match the params keys of Send WABA Message, so the values you supply here have the same shape as the values you will send later.
Headers with an IMAGE, VIDEO or DOCUMENT format are not supported through the API yet and are rejected with a 400. Create those templates from the 2Chat app.
Buttons
Each button is an object with a type:
| Type | Fields | Limit |
|---|---|---|
QUICK_REPLY | text | Up to 10 |
URL | text, url, and example when the URL contains {{1}} | Up to 2 |
PHONE_NUMBER | text, phone_number | Up to 1 |
OTP | otp_type (COPY_CODE or ONE_TAP), text | Exactly 1, AUTHENTICATION only |
WhatsApp does not allow QUICK_REPLY buttons to be mixed with URL or PHONE_NUMBER buttons, and allows at most 2 call-to-action buttons in total. ONE_TAP buttons also require package_name and signature_hash.
An AUTHENTICATION template with no buttons gets a COPY_CODE button added automatically.
Invocation
- cURL
- Python
- JavaScript
curl --request POST \
--url 'https://api.p.2chat.io/open/waba/templates' \
--header 'Content-Type: application/json' \
--header 'X-User-API-Key: your_api_key_here' \
--data '{
"phone_number": "+5215512345432",
"name": "order_shipped",
"language": "en",
"category": "UTILITY",
"header": { "format": "TEXT", "text": "Order {{1}}" },
"body": "Hi {{1}}, your order {{2}} is on its way and arrives on {{3}}.",
"footer": "2Chat",
"examples": {
"header": ["A-8871"],
"body": ["Ada", "A-8871", "August 12"]
}
}'
import requests
url = "https://api.p.2chat.io/open/waba/templates"
headers = {
"Content-Type": "application/json",
"X-User-API-Key": "your_api_key_here"
}
payload = {
"phone_number": "+5215512345432",
"name": "order_shipped",
"language": "en",
"category": "UTILITY",
"header": {"format": "TEXT", "text": "Order {{1}}"},
"body": "Hi {{1}}, your order {{2}} is on its way and arrives on {{3}}.",
"footer": "2Chat",
"examples": {
"header": ["A-8871"],
"body": ["Ada", "A-8871", "August 12"]
}
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
const axios = require('axios');
axios.post('https://api.p.2chat.io/open/waba/templates', {
phone_number: '+5215512345432',
name: 'order_shipped',
language: 'en',
category: 'UTILITY',
header: { format: 'TEXT', text: 'Order {{1}}' },
body: 'Hi {{1}}, your order {{2}} is on its way and arrives on {{3}}.',
footer: '2Chat',
examples: {
header: ['A-8871'],
body: ['Ada', 'A-8871', 'August 12']
}
}, {
headers: {
'Content-Type': 'application/json',
'X-User-API-Key': 'your_api_key_here'
}
})
.then(response => console.log(response.data))
.catch(error => console.error(error));
Create a template with buttons
{
"phone_number": "+5215512345432",
"name": "summer_promo_2026",
"language": "en",
"category": "MARKETING",
"body": "{{1}}, enjoy 30% off until {{2}}.",
"footer": "Reply STOP to unsubscribe",
"buttons": [
{ "type": "URL", "text": "See offers", "url": "https://shop.example.com/promo/{{1}}" },
{ "type": "PHONE_NUMBER", "text": "Call us", "phone_number": "+5215512345432" }
],
"examples": {
"body": ["Ada", "08/31"],
"button": ["summer"]
}
}
examples.button supplies the sample value for the dynamic URL button, in the same order the dynamic buttons appear.
Create an authentication template
WhatsApp writes the body of authentication templates itself (<VERIFICATION_CODE> is your verification code.), so body, header and footer do not apply — the only things you control are the security line, the expiry and the OTP button.
{
"phone_number": "+5215512345432",
"name": "login_code",
"language": "en",
"category": "AUTHENTICATION",
"add_security_recommendation": false,
"code_expiration_minutes": 30,
"buttons": [
{ "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy Code" }
]
}
The verification code is sent as the first body parameter when you send the template. The example_payload in the response shows exactly where it goes.
Response
Returns 201 Created.
{
"success": true,
"template": {
"uuid": "TMP1b44a079-c75c-4403-bc8f-a75c4ce5cd23",
"name": "order_shipped",
"status": "PENDING",
"category": "UTILITY",
"language": "en",
"content": "Hi {{1}}, your order {{2}} is on its way and arrives on {{3}}.",
"description": null,
"components": [
{ "type": "HEADER", "format": "TEXT", "text": "Order {{1}}" },
{ "type": "BODY", "text": "Hi {{1}}, your order {{2}} is on its way and arrives on {{3}}." },
{ "type": "FOOTER", "text": "2Chat" }
],
"quality_score": "UNKNOWN",
"rejection_reason": null,
"updated_at": "2026-08-12T19:44:11Z"
},
"required_params": {
"send_supported": true,
"unsupported_reasons": [],
"body": [
{ "index": 1, "placeholder": "{{1}}", "required": true, "type": "string", "max_length": 1024, "example": "Ada" },
{ "index": 2, "placeholder": "{{2}}", "required": true, "type": "string", "max_length": 1024, "example": "A-8871" },
{ "index": 3, "placeholder": "{{3}}", "required": true, "type": "string", "max_length": 1024, "example": "August 12" }
],
"header": [
{ "index": 1, "placeholder": "{{1}}", "required": true, "type": "string", "max_length": 60, "example": "A-8871" }
],
"button": [],
"counts": { "body": 3, "header": 1, "button": 0 }
},
"example_payload": {
"from_number": "+5215512345432",
"to_number": "<to_number>",
"template_uuid": "TMP1b44a079-c75c-4403-bc8f-a75c4ce5cd23",
"params": {
"header": ["A-8871"],
"body": ["Ada", "A-8871", "August 12"]
}
},
"message": "Template submitted to WhatsApp for review. Approval is not instant (usually minutes, up to 24 hours) and the template cannot be sent until its status is APPROVED — poll GET /open/waba/templates/{template_uuid} or subscribe to the whatsapp.waba.template.status.updated webhook."
}
| Field | Description |
|---|---|
success | true when the template was created |
template | The created template, in the same shape as Get WABA Template |
template.uuid | Template UUID. Use it as template_uuid when sending, and to poll the approval status |
template.status | PENDING, APPROVED or REJECTED. Only APPROVED templates can be sent |
template.category | The category WhatsApp assigned. It may differ from the one you requested |
template.rejection_reason | Why WhatsApp rejected the template, once it is REJECTED |
required_params | The values you must supply when sending, per component |
example_payload | A ready-to-use body for Send WABA Message — replace <to_number> and the sample values |
message | What to do next, based on the returned status |
Error responses
{
"error": true,
"error_message": "Payload is invalid: `examples.body` is required when `body` contains placeholders"
}
| Status | When |
|---|---|
400 | The payload breaks a template rule: invalid name charset, unknown category or language, a component over its length limit, variables not numbered consecutively from {{1}}, a body that starts or ends with a variable, missing or mismatched examples, an unsupported button combination, or a media header |
400 | WhatsApp rejected the template. The message is prefixed with Meta: and carries their explanation — most often a duplicate name |
401 | Missing or invalid API key |
402 | The account has expired |
404 | phone_number is not a WABA number on your account |
429 | Too many templates created recently, or the same name and language is already being created |
WhatsApp allows 100 template creations per WABA per hour, and caps how many templates an account can hold in total: 250 until the business portfolio is verified, 6,000 once verified. 2Chat caps API creation below the hourly allowance so an automated integration cannot exhaust it for your whole account, including templates created from the app: 40 creations per hour per WABA number and 100 per hour per 2Chat account. Requests over either cap return 429 — wait a few minutes and retry. If WhatsApp rejects a creation because the account is at its total capacity, delete unused templates first — keeping in mind deleted names stay reserved for 30 days.
After creating a template
- Poll Get WABA Template with the returned
uuid, or subscribe to thewhatsapp.waba.template.status.updatedwebhook to be told when WhatsApp decides. - Once
statusisAPPROVED, send it with Send WABA Message using theexample_payloadas a starting point. - If it is
REJECTED, readrejection_reason, fix the content and create a new template — a rejected template cannot be edited.
Every successful creation also fires the whatsapp.waba.template.created webhook, so other systems can react to templates created by someone else — including from the 2Chat web app.