Skip to main content

Send a WhatsApp Template

Sends a pre-approved template to a WhatsApp recipient. Templates can be sent at any time, including outside Meta's 24-hour customer-service window — this is the right endpoint for proactive sends like order confirmations, appointment reminders, OTP verification, and re-engagement messages.

Which endpoint do I call?​

POST to /v1/whatsapp/templates over HTTPS. The endpoint requires the whatsapp:send scope on your API key — same scope as free-form text sends.

Not sure of a template's exact name, its language or what it needs? List the number's templates first: each one comes with its status, its placeholders and its buttons.

POST /v1/whatsapp/templates

What does the request look like?​

A minimal call needs an agent, a from-phone, a recipient, and the template name + language. Most real sends also include parameters to fill in placeholders, and many templates also require buttons[], header_parameters, or header_media_url depending on what they were approved with.

curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
-H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "12345678-1234-1234-1234-123456789012",
"from": "+15557654321",
"to": "+15551234567",
"template": {
"name": "arabic_signup_message",
"language": "ar"
}
}'

With body parameters:

curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
-H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345-shipped" \
-d '{
"agent_id": "12345678-1234-1234-1234-123456789012",
"from": "+15557654321",
"to": "+15551234567",
"template": {
"name": "order_shipped",
"language": "en",
"parameters": {
"customer_name": "Sara",
"tracking_url": "https://example.com/track/A123"
}
}
}'

What goes in the request body?​

The first five fields are required for any send. The rest are optional and only matter when your template was approved with the matching component.

FieldTypeRequiredDescription
agent_idUUIDYesThe agent to send from.
fromE.164 stringYesThe WhatsApp business number to send from.
toE.164 stringYesRecipient's phone.
template.namestringYesMeta-approved template name (e.g. order_shipped).
template.languagestringYesMeta's language code for the template, as the template list shows it (e.g. en, ar, en_US).
template.parametersobjectNoBody placeholders. Flat name → value map. Works for both POSITIONAL ({{1}}) and NAMED ({{customer_name}}) templates.
template.header_parametersobjectNoTEXT-header placeholder values. Independent from parameters — Meta numbers header and body placeholders separately. Header allows one placeholder maximum.
template.header_media_urlstringNoFor IMAGE / VIDEO / DOCUMENT header templates — a public URL pointing at the asset to send. Omitted → Mojeeb uses the template's approval-time sample image.
template.buttonsarrayNoPer-button values for templates with dynamic buttons. Each entry targets one button by its 0-based index. See How do buttons work?

How do body parameters work?​

Templates can have placeholders in their body — e.g. a template body of Hello {{customer_name}}, your order is on the way: {{tracking_url}}. Pass values as a flat map:

"parameters": {
"customer_name": "Sara",
"tracking_url": "https://example.com/track/A123"
}

For POSITIONAL templates (e.g. body Hello {{1}}, your code is {{2}}), use the placeholder number as the key:

"parameters": {
"1": "Sara",
"2": "493281"
}

You don't need to tell us which style the template uses — Mojeeb detects it from the approved template body.

How do TEXT-header variables work?​

Templates with a TEXT header can have a single placeholder in the header text — for example a header of Hello {{name}}. Send the value separately from body parameters:

curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
-H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "12345678-1234-1234-1234-123456789012",
"from": "+15557654321",
"to": "+15551234567",
"template": {
"name": "promo_with_header",
"language": "en",
"header_parameters": { "promo_label": "Summer Sale" },
"parameters": { "customer_name": "Sara", "discount_pct": "30" }
}
}'

Meta numbers header and body placeholders separately, so both can use {{1}} without conflict — keep them in their own field. Headers allow at most one placeholder; templates with multi-placeholder headers won't pass Meta approval.

How do buttons work?​

Templates can include dynamic buttons (URL, QUICK_REPLY, COPY_CODE, OTP, FLOW, MPM, CATALOG, ORDER_DETAILS). Each gets one entry in template.buttons[], keyed by its 0-based index in the approved template. The template list gives each button's index and, as send_field, the field that carries its value. The fields you provide depend on the button's sub-type:

Approved buttonField to sendExample value
URL (with {{1}} in URL)url_suffix"orders/12345"
OTP (AUTHENTICATION templates)otp_code"493281"
QUICK_REPLYquick_reply_payload"FEEDBACK_GOOD"
COPY_CODE (Marketing coupon)coupon_code"SPRING30"
FLOW / MPM / SPM / CATALOG / ORDER_DETAILSactionobject — see Meta's docs
PHONE_NUMBER / VOICE_CALL—Static; no value needed

Dynamic URL button​

A template's URL button can be approved with a {{1}} at the end of the URL — Meta substitutes your value at send-time:

"buttons": [
{ "index": 0, "url_suffix": "orders/12345" }
]

If the approved URL is https://shop.example.com/{{1}}, the customer sees a button that opens https://shop.example.com/orders/12345.

QUICK_REPLY button​

The quick_reply_payload is echoed back to your webhook when the user taps the button — it's how you distinguish which button was tapped. The button label itself is fixed at template approval; this is just the routing string:

"buttons": [
{ "index": 0, "quick_reply_payload": "FEEDBACK_GOOD" },
{ "index": 1, "quick_reply_payload": "FEEDBACK_BAD" }
]

COPY_CODE button (Marketing coupon)​

Maximum 15 characters per Meta's rules. The customer sees a button that copies the code to their clipboard:

"buttons": [
{ "index": 0, "coupon_code": "SPRING30" }
]

OTP button (AUTHENTICATION templates)​

For one-tap, copy-code, and zero-tap OTP templates, send the verification code via otp_code. The code typically also appears in the body {{1}}, so pass it in both places:

curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
-H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "12345678-1234-1234-1234-123456789012",
"from": "+15557654321",
"to": "+15551234567",
"template": {
"name": "otp_verification_v1",
"language": "en",
"parameters": { "1": "493281" },
"buttons": [
{ "index": 0, "otp_code": "493281" }
]
}
}'

Both copy-code and one-tap OTP buttons use the same wire shape — Meta picks the right rendering based on how the template was approved. otp_code is a friendlier alias for url_suffix; either works.

FLOW button​

For interactive Flow templates, supply the action payload Meta documents. Mojeeb forwards it verbatim:

"buttons": [
{
"index": 0,
"action": {
"flow_token": "tok_3f4a91",
"flow_action_data": { "branch_id": "cairo-downtown" }
}
}
]

See Meta's Flow Templates docs for the full action shape per flow.

Static buttons (PHONE_NUMBER, VOICE_CALL, static URL)​

If the approved button has no placeholder (e.g. a fixed phone number, or a URL with no {{1}}), omit it from buttons[]. Meta uses the value baked into the template.

How do custom header images work?​

For templates with an IMAGE, VIDEO, or DOCUMENT header, you can send a different asset to each recipient via header_media_url:

curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
-H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "12345678-1234-1234-1234-123456789012",
"from": "+15557654321",
"to": "+15551234567",
"template": {
"name": "order_shipped_with_photo",
"language": "en",
"header_media_url": "https://cdn.customer.example.com/shipments/A12345.jpg",
"parameters": { "tracking_no": "EG-A12345" }
}
}'

Mojeeb checks one thing before queueing: the URL must be an absolute https:// URL, or the request fails with 422 template_header_media_url_invalid. Meta enforces the rest:

  • Must be publicly reachable from Meta's servers — Meta downloads the asset itself, so intranet, VPN-only, or IP-allowlisted URLs won't work.
  • MIME and size must match Meta's per-format limits — typically JPG/PNG ≤ 5 MB for IMAGE, MP4 ≤ 16 MB for VIDEO, PDF ≤ 100 MB for DOCUMENT.
  • If the URL is unreachable or rejected, the send transitions to status: failed (visible via Get Message Status).

When you omit header_media_url, Mojeeb sends the approval-time sample image for every recipient — useful when the same asset is fine for everyone.

What does a success response look like?​

HTTP 202 Accepted with the message object: its id, status: "queued", the agent_id and to you sent, type: "template", and created_at. Every field, with an example, is in the reference for this endpoint, rendered from the API's own contract so it is never out of date.

Use the returned id with Get Message Status to track delivery.

What can go wrong?​

The standard error envelope. Common template cases (the full catalog is on the Errors page):

codeHTTPWhen
invalid_request_body422A required field is missing or blank (for example template.name or template.language) — param names which
from_phone_not_found_for_agent422from doesn't match any active WhatsApp connection — available_phones lists what would work
agent_not_authorized403API key isn't allowed to use this agent
template_not_found422The connection has no template with that name, whatever its status — available_templates lists the names it has
template_not_found_for_language422The template doesn't exist in that language — available_languages lists the languages it has
template_header_media_url_invalid422header_media_url isn't an absolute HTTPS URL

These errors fire before the send is queued (synchronous 422). Meta-side failures surface asynchronously, as do Mojeeb's send-time checks for missing button or header values — the request still returns 202 with a queued message id, and you discover the failure by polling Get Message Status: status becomes failed and failure_reason says why (TemplatePaused, TemplateBroken, RecipientUnreachable and others).

Common questions​

How do I create a template?​

Use Mojeeb's WhatsApp page: it creates, edits, deletes and submits templates on every plan that includes WhatsApp; see WhatsApp Templates. Meta reviews each one, and once it's approved you can send it. The API doesn't create or edit templates yet: List WhatsApp Templates lists them, with each one's status.

What's the difference between language en and en_US?​

Both are Meta's template language codes. Meta uses them to pick the right template translation: en is generic English, and en_US is US English specifically. They aren't BCP-47 tags: Meta writes a regional variant with an underscore (en_US), not a hyphen (en-US). Send the exact code your template was approved with, as the template list shows it.

Can I omit parameters if my template has no placeholders?​

Yes. Omit the parameters field entirely (or pass an empty object). Templates with no placeholders ignore parameters anyway. Same applies to header_parameters, header_media_url, and buttons.

Does the API validate parameters against the template's expected placeholders?​

Partly. Mojeeb checks that the connection has a template with that name in your language, whatever its status, and that header_media_url is an absolute HTTPS URL. Those failures return a 422 with a specific code before the send is queued. A template that isn't approved passes this check and then fails at Meta, so list the templates and send only those with status: approved. When Mojeeb can't read the templates from Meta, or the number has more templates than Mojeeb reads and yours isn't among them, it skips the name and language check: the send is accepted (202), and Meta's verdict shows in the message's status.

Mojeeb also checks two things when the queued send runs. First, every dynamic button that needs a value has one: a URL button with a placeholder, OTP, COPY_CODE, FLOW and the other action buttons. QUICK_REPLY falls back to its label. Second, a TEXT header with placeholders gets at least one header_parameters value. If one is missing, the request still returns 202. The message then becomes status: failed with failure_reason: TemplateBroken, failure_code: null and a failure_detail that names the button index or header placeholders.

Mojeeb doesn't check placeholder values themselves, how many body or header values you send, or whether they match. Meta checks those, and a mismatch also shows up as status: failed when you look up the message.

Can I send the same template twice safely?​

Yes — use Idempotency-Key. See Idempotency.

Can I upload an image directly instead of hosting it myself?​

Not in v1 — header_media_url requires you to host the asset on a public HTTPS URL. Customers with private CDNs need to expose a Meta-reachable URL (signed URLs, a public bucket, etc.). A direct-upload endpoint may land in a future version.

What's the maximum size for header_media_url?​

Limits are Meta's, not Mojeeb's. As of 2026: JPG/PNG ≤ 5 MB, MP4 ≤ 16 MB, PDF ≤ 100 MB. See Meta's media specs for the current numbers.

How do I find a button's index?​

List the number's templates: each button comes with its index, and send_field names the field its value goes in. Buttons are 0-based in the order they appear in the approved template. The first button is index: 0, the second is index: 1, etc. Static buttons (PHONE_NUMBER, VOICE_CALL, URL without a placeholder) keep their index even though you don't send values for them — pass dynamic-button values keyed by their literal position in the template.