Skip to main content

Send WABA Message

Agent Skill

Automate this endpoint with an AI agent using the 2chat-whatsapp-waba skill:

npx skills add 2ChatCo/agent-skills -s 2chat-whatsapp-waba

Send a message via WhatsApp Business API (WABA). You can send either a template message (for starting a conversation or after the 24-hour window) or a session message (plain text within an active conversation).

info

Requires a WABA channel connected to your 2Chat account. Get template UUIDs from the Get WABA Templates endpoint, and the exact values a template expects from Get WABA Template by UUID.

Request body (common fields)​

FieldDescriptionRequired
to_numberRecipient number in E.164 format (e.g. +595981048477).Yes
from_numberYour WABA number in E.164 format (e.g. +5215512345432).Yes

For template messages, also send:

FieldDescriptionRequired
template_uuidTemplate UUID from Get WABA Templates. The template must be in APPROVED status.Yes
paramsTemplate variable values. Required for template messages; use an empty object {} (or e.g. {"body": []}) if the template has no variables.Yes
params.bodyArray of strings replacing {{1}}, {{2}}, … in the template body. The number of values must match the template exactly — see Template parameter values.If template has body variables
params.headerArray of strings replacing {{1}}, {{2}}, … in a TEXT header. Only for templates whose header is TEXT with placeholders.If applicable
params.header_media_urlPublic URL to the image, video, or document for templates with an IMAGE / VIDEO / DOCUMENT header. Must be http or https and publicly downloadable. 2Chat fetches the file and re-hosts it on its own storage before forwarding to WhatsApp, so short-lived signed URLs are fine as long as they are reachable at send time. An empty or blank value ("") is treated as not passed.If template has a media header
params.header_media_filenameFilename the recipient sees for DOCUMENT templates (e.g. invoice.pdf). Not enforced by the API, but strongly recommended: storage and CDN URLs (S3, CloudFront, etc.) often end in a hashed key, and WhatsApp shows that name otherwise. Ignored on other header types and when empty.No (recommended for DOCUMENT)
params.buttonArray with the value for a dynamic URL button — the part that replaces the {{1}} placeholder in the button's URL. E.g. a template button https://shop.example.com/track/{{1}} with params.button: ["a1b2c3"] resolves to https://shop.example.com/track/a1b2c3.If the template has a dynamic URL button

For session messages (within 24-hour window), send:

FieldDescriptionRequired
textPlain text message, at most 4096 characters.Yes
caution

You must send either a template message (template_uuid and params; params is required even if empty) or a session message (text), not both in the same request.


Template parameter values​

Every params.* array is validated against the approved template before the message is queued. Getting these rules wrong is the most common cause of a 400.

Don't guess the values

Call Get WABA Template by UUID to obtain the exact list of variables a template expects — including how many values each array takes and a ready-to-copy example_payload.

Values are positional, not keyed​

params.body, params.header and params.button are arrays. Values are applied in order to the template's placeholders — they are not a map keyed by placeholder number.

Named variables (Meta channels)​

Meta also supports templates with named variables — {{customer_name}} instead of {{1}} (parameter_format: NAMED in Business Manager). For those templates, pass params.body (and params.header) as an object keyed by placeholder name:

{
"template_uuid": "TMP…",
"params": {
"body": { "customer_name": "Ana", "appointment_date": "Tuesday 10:00" }
}
}

An array is also accepted, with values in the placeholders' order of appearance. Every named placeholder must receive a value — a missing or unknown name is rejected with a 400 naming it. Get WABA Template by UUID reports parameter_format and each variable's name, and its example_payload already uses the right shape.

Named variables are only available on Meta-hosted numbers — Gupshup channels reject them with an explanatory error.

"params": { "body": ["Maria", "TRK-98452"] }

The first value replaces the first placeholder, the second replaces the second, and so on.

The count must match exactly​

The expected count is the number of distinct {{N}} placeholders in the component. Passing too few or too many values is rejected:

{
"error": true,
"error_message": "Payload is invalid: Template 'order_shipped' expects 2 body param(s) but 1 were provided"
}

The same rule applies to params.header (TEXT headers) and params.button (dynamic URL buttons). If a template has no variables at all, send "params": {}.

Character and length restrictions​

WhatsApp rejects template parameters containing certain characters (Meta error 132018), so 2Chat validates them up front:

RuleApplies toRejected with
Maximum 1024 characters per valueparams.body, params.header, params.button`params.body[0]` exceeds the maximum length of 1024 characters
No line breaks (\n)all param arrays`params.body[0]` contains invalid characters: line breaks, tabs, and more than 4 consecutive spaces are not allowed in template parameters
No tabs (\t)all param arrayssame as above
No runs of 5 or more consecutive spacesall param arrayssame as above

Values are checked by position, so the error message points at the exact offending index.

Header text values

Meta additionally caps a TEXT header variable at 60 characters. 2Chat forwards longer values, but Meta will reject the send. Get WABA Template by UUID reports the applicable max_length per variable.

Session message text

WhatsApp caps a text message at 4096 characters. A longer text is rejected with 400 before anything is sent.

Which buttons take a value​

Only dynamic URL buttons — a call-to-action button whose URL contains a {{1}} placeholder — consume a value from params.button:

Button typeNeeds a value in params.button?
URL with a {{N}} placeholderYes — one value
URL without a placeholder (static link)No
Quick replyNo
Phone numberNo
OTP button of an AUTHENTICATION templateNo — see below

Only one dynamic URL button per template is supported. Templates with more are rejected with Template '...' has N dynamic URL buttons; only one is supported.

Authentication (OTP) templates​

For an AUTHENTICATION template, pass the verification code in params.body and omit params.button — the OTP button reuses the body value automatically:

{
"to_number": "+595981048477",
"from_number": "+5215512345432",
"template_uuid": "TMP1b44a079-c75c-4403-bc8f-a75c4ce5cd23",
"params": {
"body": ["472913"]
}
}

Omitting params.body on an authentication template is rejected with Template '...' is an authentication template. Pass the verification code in 'params.body'; the button reuses it. Passing the code explicitly in params.button as well is still accepted for backwards compatibility.

Only APPROVED templates can be sent​

A template in PENDING, REJECTED or FAILED status is rejected with Template '...' is not approved (status: PENDING). Only APPROVED templates can be sent. Check status via Get WABA Templates before sending.


Send a template message​

Use when starting a conversation or when the 24-hour session has expired. Replace template placeholders with values in params.body (and optionally params.header or params.button).

curl --request POST \
--url 'https://api.p.2chat.io/open/waba/send-message' \
--header 'Content-Type: application/json' \
--header 'X-User-API-Key: your_api_key_here' \
--data '{
"to_number": "+595981048477",
"from_number": "+5215512345432",
"template_uuid": "TMP1b44a079-c75c-4403-bc8f-a75c4ce5cd23",
"params": {
"body": ["Maria", "TRK-98452"]
}
}'

Send a template with a media header​

Use this shape when the template's header is IMAGE, VIDEO, or DOCUMENT. Pass the public URL of the media in params.header_media_url. The type (image / video / document) is derived from the template — you do not need to specify it.

curl --request POST \
--url 'https://api.p.2chat.io/open/waba/send-message' \
--header 'Content-Type: application/json' \
--header 'X-User-API-Key: your_api_key_here' \
--data '{
"to_number": "+595981048477",
"from_number": "+5215512345432",
"template_uuid": "TMP1b44a079-c75c-4403-bc8f-a75c4ce5cd23",
"params": {
"body": ["Maria"],
"header_media_url": "https://example.com/promo.png"
}
}'

For DOCUMENT templates, also pass params.header_media_filename to control the filename shown to the recipient (e.g. "header_media_filename": "invoice.pdf"). We recommend always setting it: when the URL comes from storage such as S3 or a CDN, its last path segment is often a hashed key that makes a poor filename. For VIDEO templates, the request shape is identical to IMAGE — only params.header_media_url is needed.

Notes on media URLs
  • The URL must use http or https and resolve to a publicly reachable host. URLs pointing to private ranges (loopback, link-local, RFC1918) are rejected.
  • 2Chat downloads the file and re-hosts it on its own storage before sending to WhatsApp, so hot-link-protected CDNs work as long as the file can be fetched server-side.
  • The same hosted asset is reused across sends, so passing identical URLs is cheap.

Send a template with a dynamic URL button​

Use this shape when the template has a dynamic URL button — a call-to-action button whose URL ends in a {{1}} placeholder (e.g. https://shop.example.com/track/{{1}}). Pass the value that replaces it in params.button. The example below resolves the button to https://shop.example.com/track/a1b2c3.

curl --request POST \
--url 'https://api.p.2chat.io/open/waba/send-message' \
--header 'Content-Type: application/json' \
--header 'X-User-API-Key: your_api_key_here' \
--data '{
"to_number": "+595981048477",
"from_number": "+5215512345432",
"template_uuid": "TMP1b44a079-c75c-4403-bc8f-a75c4ce5cd23",
"params": {
"body": ["Maria"],
"button": ["a1b2c3"]
}
}'
caution

If the template's URL button contains a placeholder and you omit params.button, the request is rejected with 400 — the link would otherwise be delivered unresolved. The number of values in params.button must match the button's placeholders (currently one {{1}}).


Send a session message​

Use when replying within the 24-hour customer session. Send only text (no template_uuid or params).

curl --request POST \
--url 'https://api.p.2chat.io/open/waba/send-message' \
--header 'Content-Type: application/json' \
--header 'X-User-API-Key: your_api_key_here' \
--data '{
"to_number": "+595981048477",
"from_number": "+5215512345432",
"text": "Hi, how can we help you today?"
}'

Response​

On success the endpoint returns 202 Accepted — the message passed validation and was queued for delivery:

{
"success": true,
"batched": true,
"message_uuid": "MSG126d9055-9dad-415f-b934-ff909773a8ef"
}
FieldDescription
successtrue when the message was accepted
batchedtrue when the message was successfully queued for sending
message_uuidInternal UUID of the message for tracking

202 means the message was queued, not that WhatsApp accepted it. If WhatsApp rejects it when we send it (for example 130429, over the number's sending rate) or can't deliver it later, you get a whatsapp.waba.message.failed webhook with this message_uuid and the provider's errors.

Error responses​

Errors return an HTTP 4xx status and a JSON body in the shape:

{
"error": true,
"error_code": "UNKNOWN_ERROR",
"error_message": "Payload is invalid: Template 'order_shipped' has a IMAGE header. You must pass 'header_media_url' with a public URL to the media."
}
FieldDescription
errorAlways true on failure
error_codeMachine-readable code. WABA_WINDOW_CLOSED when the 24-hour window is closed; UNKNOWN_ERROR for the payload validation errors below
error_messageHuman-readable explanation. Payload validation errors are prefixed with Payload is invalid:

Request shape​

Statuserror_messageWhen
400from_number is requiredMissing required field
400to_number is requiredMissing required field
400template_uuid or text or interactive is requiredNone is present
400template_uuid and text or interactive cannot be used togetherMore than one is present
400`params` must be an objectparams is not a JSON object
400`text` must be a stringtext has the wrong type
400`text` exceeds the maximum length of 4096 characterstext is longer than 4096 characters

Template parameter values​

Statuserror_messageWhen
400Template '...' expects N body param(s) but M were providedparams.body length does not match the template's placeholders
400Template '...' TEXT header expects N param(s) but M were providedparams.header length does not match the TEXT header's placeholders
400Template '...' expects N URL-button param(s) but M were providedparams.button length does not match the template's dynamic URL buttons
400`params.body` must be an arrayA params.* value is not an array
400`params.body[0]` exceeds the maximum length of 1024 charactersA parameter value is too long
400`params.body[0]` contains invalid characters: line breaks, tabs, and more than 4 consecutive spaces …A parameter value contains characters Meta rejects (error 132018)
400Template '...' is not approved (status: PENDING). Only APPROVED templates can be sent.Template is not in APPROVED status
400Template '...' is an authentication template. Pass the verification code in 'params.body'; the button reuses it.AUTHENTICATION template sent without params.body
400Template '...' has N dynamic URL buttons; only one is supported.Template has more than one placeholder URL button
400template_uuid is not validTemplate does not exist or does not belong to your account

Header media​

Statuserror_messageWhen
400`params.header_media_url` must be a valid public URLURL has invalid syntax (an empty or blank value is not an error; it is treated as not passed)
400`params.header_media_url` must be a string URLURL has the wrong type
400Template '...' has a IMAGE header. You must pass 'header_media_url' with a public URL to the media.Template expects media, caller did not pass header_media_url (or passed it empty)
400Template '...' has a IMAGE header. Use 'header_media_url' instead of 'header_params' for media headers.Caller passed a non-empty params.header for a media header
400Template '...' has a TEXT header. 'header_media_url' is only valid for IMAGE/VIDEO/DOCUMENT headers.Template header is TEXT and caller included a non-empty header_media_url
400Template '...' has no HEADER component. Do not pass 'header_params' or 'header_media_url'.Template has no header at all and caller included a non-empty header or header_media_url
400Could not fetch the media URL provided in 'params.header_media_url' ...URL unreachable, blocked by the SSRF guard (private/loopback host), or rejected by the scheme allowlist

Empty values never trigger these errors on their own: "header": [], "header_media_url": "" and "header_media_filename": "" are treated as not passed. Form-based integrations (Make, Zapier, n8n) can send every field and leave the unused ones empty.

Channel and delivery​

Statuserror_messageWhen
404Resource Not Found: from_number does not exist on your accountNumber not found or marked for deletion
410Source number is not connected to 2Chat.Channel is in a non-connected state
422The 24-hour customer service window for this contact is closed. …A free-form text send was attempted outside the window. Send a template instead, or check first with Conversation Window. error_code is WABA_WINDOW_CLOSED
402Please upgrade your account.Insufficient WABA credit to send the message