Send a message
POST /messages/whatsapp — scope WHATSAPP_SEND.
A message is a template, free text or media, exactly one of the three.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
to |
string | yes | the recipient, international form: +919876543210 |
from |
string | no | which of your numbers to send from. Omitted: your default number, or your only one |
template |
object | one of three | { name, language, variables } — the only form deliverable outside the window |
text |
string | one of three | 1–4096 characters. Inside the window only |
media |
object | one of three | see Media |
idempotencyKey |
string | no | 8–128 characters. A repeat returns the first result instead of sending again |
metadata |
object | no | your own JSON, stored and returned on every read of this message |
Template
Section titled “Template”The only form Meta allows outside the 24-hour window.
{ "to": "+919876543210", "from": "+919812345678", "template": { "name": "order_shipped", "language": "en_US", "variables": { "name": "Ravi", "order_id": "BLR-4471" } }, "idempotencyKey": "order-4471-shipped"}variables is an object, not a list, because WhatsApp templates can be named as well
as numbered:
{ "name": "Ravi", "order_id": "BLR-4471" } // named: {{name}}, {{order_id}}{ "1": "Ravi", "2": "BLR-4471" } // numbered: {{1}}, {{2}}language defaults to en_US. Every variable the template declares must be present and
non-empty, or the send is refused with 400 MISSING_VARIABLES — GET /templates tells you
which ones a template needs.
Free text
Section titled “Free text”Inside an open window only.
{ "to": "+919876543210", "text": "Your order is out for delivery." }Your own metadata
Section titled “Your own metadata”metadata is yours: up to 50 keys and 8 KB once serialized, keys up to 64 characters. Use
it to carry your own order or ticket id so a webhook or a log read can be matched back to
your records without a lookup table.
We never read inside it, so never put a secret there.
Response
Section titled “Response”201 Created, or 200 OK when an idempotencyKey replayed:
{ "data": { "id": "cmu102948182741", "channel": "WHATSAPP", "status": "SENT", "to": "+919876543210", "from": "+919812345678", "providerMessageId": "wamid.HBgM109…", "metadata": { "orderId": "A-1043" }, "error": null, "createdAt": "2026-09-27T10:00:00.000Z" }, "meta": { "replayed": false }}status is SENT once Meta accepted it, FAILED if Meta refused — with the reason in
error. Delivery is not confirmed yet: DELIVERED and READ arrive later, as
webhooks or on a later read.
Idempotency
Section titled “Idempotency”idempotencyKey is scoped to your workspace. Re-sending the same key returns the original
message with 200 and meta.replayed: true, and never calls Meta twice — which is what
makes a retry after a timeout safe.
Key it on the event that caused the send — order-4471-shipped — not on a fresh UUID. A
random key per attempt is not idempotent.
Failures
Section titled “Failures”| Failure | HTTP | Code |
|---|---|---|
| free text or media with no open window | 409 | WINDOW_CLOSED |
| no template with that name and language | 404 | TEMPLATE_NOT_FOUND |
| template not approved by Meta | 409 | TEMPLATE_NOT_APPROVED |
| a template variable is missing or blank | 400 | MISSING_VARIABLES |
from is not one of your numbers |
404 | NUMBER_NOT_FOUND |
from has not finished registering with Meta |
409 | NUMBER_NOT_REGISTERED |
| you have no number that can send yet | 409 | CHANNEL_NOT_CONFIGURED |
Preview a template
Section titled “Preview a template”POST /messages/whatsapp/preview — scope WHATSAPP_SEND. Fills a stored template with the
values you pass and returns the text Meta would render, without sending anything. Useful
for showing an operator what is about to go out.
curl -X POST "https://vaarto.com/api/v1/messages/whatsapp/preview" \ -H "Authorization: Bearer $VAARTO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "templateName": "order_shipped", "language": "en_US", "variables": { "1": "Rahul", "2": "ORD-9921" } }'const res = await fetch("https://vaarto.com/api/v1/messages/whatsapp/preview", { method: "POST", headers: { Authorization: `Bearer ${process.env.VAARTO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ templateName: "order_shipped", language: "en_US", variables: { "1": "Rahul", "2": "ORD-9921", }, }),});
const { data } = await res.json();import os, requests
res = requests.post( "https://vaarto.com/api/v1/messages/whatsapp/preview", headers={"Authorization": f"Bearer {os.environ['VAARTO_API_KEY']}"}, json={ "templateName": "order_shipped", "language": "en_US", "variables": { "1": "Rahul", "2": "ORD-9921", }, }, timeout=10,)
data = res.json()["data"]$data = Http::withToken(env('VAARTO_API_KEY')) ->post('https://vaarto.com/api/v1/messages/whatsapp/preview', [ 'templateName' => 'order_shipped', 'language' => 'en_US', 'variables' => [ '1' => 'Rahul', '2' => 'ORD-9921', ], ]) ->json('data');variables accepts an object (named or numbered keys) or a plain array in placeholder
order.
{ "data": { "templateName": "order_shipped", "language": "en_US", "category": "UTILITY", "status": "APPROVED", "header": "Order Status Update", "body": "Hi Rahul, your order ORD-9921 is confirmed.", "footer": "Thank you for shopping with us!", "buttons": ["Track Order"], "fullText": "Order Status Update\n\nHi Rahul, your order ORD-9921 is confirmed.\n\nThank you for shopping with us!" }}