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.If template has a media header
params.header_media_filenameOptional filename hint for DOCUMENT templates (e.g. invoice.pdf).No
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.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.

"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.

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, you can also pass params.header_media_filename to control the filename shown to the recipient (e.g. "header_media_filename": "invoice.pdf"). 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

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

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
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
400Template '...' has a IMAGE header. Use 'header_media_url' instead of 'header_params' for media headers.Caller passed 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 header_media_url
400Template '...' has no HEADER component. Do not pass 'header_params' or 'header_media_url'.Template has no header at all
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

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