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 — any of the 111 languages WhatsApp supports, 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}}, … — or, on Meta-hosted numbers, named: {{customer_name}} (letters, digits and underscores; one format per template, never mixed). With named variables, pass examples.body as an object keyed by name, e.g. {"customer_name": "Ana"}. Gupshup channels support numbered variables only. Required, except for AUTHENTICATION templates, where WhatsApp writes the body for you. |
header | Optional object. Text: {"format": "TEXT", "text": "..."} — max 60 characters, at most one variable. Media: {"format": "IMAGE" \| "VIDEO" \| "DOCUMENT", "media_url": "https://..."} — see Media headers. |
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.
Supported languages
Templates can be written in any of the 111 languages WhatsApp supports. Pass the code in language exactly as listed below: lowercase language, uppercase region, joined by an underscore (en_US, not en-US or en_us). A plain language code and a regional one are different templates — es is not es_MX.
WhatsApp keys a template by name + language, so the same name can be reused once per language. That is how you offer one template in several languages: create it once per language, each reviewed on its own. Note that Delete WABA Template removes every language variant at once.
All 111 language codes
| Language | Code |
|---|---|
| Afrikaans | af |
| Albanian | sq |
| Arabic | ar |
| Arabic (EGY) | ar_EG |
| Arabic (LBN) | ar_LB |
| Arabic (MAR) | ar_MA |
| Arabic (QAT) | ar_QA |
| Arabic (UAE) | ar_AE |
| Azerbaijani | az |
| Belarusian | be_BY |
| Bengali | bn |
| Bengali (IND) | bn_IN |
| Bulgarian | bg |
| Catalan | ca |
| Chinese (CHN) | zh_CN |
| Chinese (HKG) | zh_HK |
| Chinese (TAI) | zh_TW |
| Croatian | hr |
| Czech | cs |
| Danish | da |
| Dari | prs_AF |
| Dutch | nl |
| Dutch (BEL) | nl_BE |
| English | en |
| English (AUS) | en_AU |
| English (CAN) | en_CA |
| English (GHA) | en_GH |
| English (IND) | en_IN |
| English (IRL) | en_IE |
| English (JAM) | en_JM |
| English (MYS) | en_MY |
| English (NZL) | en_NZ |
| English (QAT) | en_QA |
| English (SGP) | en_SG |
| English (UAE) | en_AE |
| English (UGA) | en_UG |
| English (UK) | en_GB |
| English (US) | en_US |
| English (ZAF) | en_ZA |
| Estonian | et |
| Filipino | fil |
| Finnish | fi |
| French | fr |
| French (BEL) | fr_BE |
| French (CAN) | fr_CA |
| French (CHE) | fr_CH |
| French (CIV) | fr_CI |
| French (MAR) | fr_MA |
| Georgian | ka |
| German | de |
| German (AUT) | de_AT |
| German (CHE) | de_CH |
| Greek | el |
| Gujarati | gu |
| Hausa | ha |
| Hebrew | he |
| Hindi | hi |
| Hungarian | hu |
| Indonesian | id |
| Irish | ga |
| Italian | it |
| Japanese | ja |
| Kannada | kn |
| Kazakh | kk |
| Kinyarwanda | rw_RW |
| Korean | ko |
| Kyrgyz (Kyrgyzstan) | ky_KG |
| Lao | lo |
| Latvian | lv |
| Lithuanian | lt |
| Macedonian | mk |
| Malay | ms |
| Malayalam | ml |
| Marathi | mr |
| Norwegian | nb |
| Pashto | ps_AF |
| Persian | fa |
| Polish | pl |
| Portuguese (BR) | pt_BR |
| Portuguese (POR) | pt_PT |
| Punjabi | pa |
| Romanian | ro |
| Russian | ru |
| Serbian | sr |
| Sinhala | si_LK |
| Slovak | sk |
| Slovenian | sl |
| Spanish | es |
| Spanish (ARG) | es_AR |
| Spanish (CHL) | es_CL |
| Spanish (COL) | es_CO |
| Spanish (CRI) | es_CR |
| Spanish (DOM) | es_DO |
| Spanish (ECU) | es_EC |
| Spanish (HND) | es_HN |
| Spanish (MEX) | es_MX |
| Spanish (PAN) | es_PA |
| Spanish (PER) | es_PE |
| Spanish (SPA) | es_ES |
| Spanish (URY) | es_UY |
| Swahili | sw |
| Swedish | sv |
| Tamil | ta |
| Telugu | te |
| Thai | th |
| Turkish | tr |
| Ukrainian | uk |
| Urdu | ur |
| Uzbek | uz |
| Vietnamese | vi |
| Zulu | zu |
WhatsApp owns this list, and 2Chat does not gate it — when WhatsApp adds a language, you can pass the new code right away. A code that is well formed but unknown to WhatsApp comes back as a 400 prefixed with Meta:.
Media headers
Templates can carry an image, video or document header. WhatsApp requires a sample file to review the template — send it as a public URL in header.media_url and 2Chat uploads it to WhatsApp for you:
{
"phone_number": "+5215512345432",
"name": "order_invoice",
"language": "en",
"category": "UTILITY",
"header": { "format": "DOCUMENT", "media_url": "https://cdn.example.com/sample-invoice.pdf" },
"body": "Hi {{1}}, your invoice is attached.",
"examples": { "body": ["Ada"] }
}
| Format | Accepted files | Max size |
|---|---|---|
IMAGE | JPEG, PNG | 5 MB |
VIDEO | MP4 | 16 MB |
DOCUMENT | 100 MB |
The sample is only used for WhatsApp's review — the actual file goes out per message via header_media_url in Send WABA Message. header.text and header.media_url are mutually exclusive, and the URL must be publicly reachable (2Chat downloads it server-side; private hosts are rejected).
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 marketing template (text only)
The simplest marketing template: a text header, a body and a footer — no media, no buttons. Remember marketing messages need an unsubscribe path, and WhatsApp may still recategorize the template, so read template.category in the response.
{
"phone_number": "+5215512345432",
"name": "new_arrivals_2026",
"language": "en",
"category": "MARKETING",
"header": { "format": "TEXT", "text": "New arrivals" },
"body": "Hi {{1}}! Our new collection just landed — come take a look before it sells out.",
"footer": "Reply STOP to unsubscribe",
"examples": {
"body": ["Ada"]
}
}
Only the body contains a variable here, so examples carries just body. A header with no {{1}} needs no example.
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, a malformed language code, 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 header.media_url that is missing, unreachable, of the wrong file type or over its size limit |
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.