← Blog

·

How to send a WhatsApp template message via API: variables, media headers and buttons (full JSON)

Updated on October 29, 2026

To send a template message with the WhatsApp Business API, make a POST to /{phone-number-id}/messages with "type": "template", the exact name of an approved template, the language.code it was created in, and a components array holding the values for the header, body and button variables. You never send the template’s fixed text: you only send the parameters, and they must match the approved template in count, format and order.

This guide gives you a ready-to-use payload for every case: text-only templates with positional and named variables, image, video and document headers (by link and by id), dynamic URL buttons, quick replies with a custom payload, coupon codes, and a combined example. At the end you’ll find the most common parameter errors and how to send the same request through Vectalk.

Anatomy of a template send request

Template messages use the same Cloud API endpoint as every other message:

POST https://graph.facebook.com/v26.0/{PHONE_NUMBER_ID}/messages
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json

The body always has the same shell:

{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "16505551234",
  "type": "template",
  "template": {
    "name": "template_name",
    "language": { "code": "en_US" },
    "components": []
  }
}

Inside components, each object has a type that points to one part of the template:

Component typeWhen to include itWhat goes in parameters
headerHeader with a text variable or mediatext, image, video, document or location
bodyBody with variablestext (also currency and date_time)
buttonButton with a dynamic valueDepends on sub_type: url, quick_reply, copy_code

If the template has no variables at all, you can leave components out. The footer never appears in the send request because it doesn’t support variables. If you haven’t sent any message through the API yet, start with how to send a WhatsApp message via API with cURL, Node.js and Python.

1. Text template with positional variables

This is the default format: unless a different format was chosen at creation, the template uses positional variables, {{1}}, {{2}} and so on. According to Meta’s template documentation, the values must be sent in the same order the placeholders appear in the text.

Approved template (body):

Hi {{1}}! Your order {{2}} is confirmed and will arrive in {{3}} business days.

Send request:

curl -X POST "https://graph.facebook.com/v26.0/$PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": "16505551234",
    "type": "template",
    "template": {
      "name": "order_confirmed",
      "language": { "code": "en_US" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "Jessica" },
            { "type": "text", "text": "#48213" },
            { "type": "text", "text": "3" }
          ]
        }
      ]
    }
  }'

Three placeholders, three text objects, in order. One missing, one extra or the wrong type, and the send fails (see the error table below).

Want to try it while you read? Create a Vectalk account and send your first message through the official API. Create free account →

2. Template with named variables (parameter_name)

At creation, a template can declare "parameter_format": "named". Variables then become names like {{first_name}}, which must be unique and use only lowercase letters and underscores. When sending, each parameter carries a parameter_name and, according to Meta, order no longer matters.

Approved template (body):

Thank you, {{first_name}}! Your order number is {{order_number}}.

Send request:

{
  "messaging_product": "whatsapp",
  "recipient_type": "individual",
  "to": "16505551234",
  "type": "template",
  "template": {
    "name": "order_confirmation",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "parameter_name": "order_number", "text": "SKBUP2-4CPIG9" },
          { "type": "text", "parameter_name": "first_name", "text": "Jessica" }
        ]
      }
    ]
  }
}

Named variables make your code easier to read and prevent the classic bug of swapping two values. The format is set at creation, not at send time: sending parameter_name to a positional template (or the other way around) is a parameter error. How to set the format when creating the template is covered in how to create, edit and delete templates via the API.

Currency and date variables

Besides text, the body accepts currency parameters (with fallback_value, code and amount_1000, the amount multiplied by 1,000) and date_time parameters (with fallback_value), as shown on the template media page. Sending the value already formatted as text also works.

3. Media headers: image, video or document

If the template was approved with a media header, the send request must include the media in a header component. The sample image you uploaded at creation is only used for review; it is never delivered to the customer. The parameter type is image, video or document, and the object with the same name accepts either link or id:

OptionHow it worksWhen to use it
linkPublic HTTPS URL of the file on your serverFiles that change per message (invoices, tickets)
idMedia ID returned when you upload to Meta’s serversThe same media for many recipients (campaigns)

Meta recommends id to reduce errors and requests to your server, and warns that a link must be on a publicly accessible server or the message will fail. When you use link, the Cloud API caches the file for 10 minutes and reuses it if the same URL shows up again in that window; to force a fresh download, append a random query string to the URL, per the send messages documentation.

Image by link

{
  "messaging_product": "whatsapp",
  "to": "16505551234",
  "type": "template",
  "template": {
    "name": "promo_image",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "header",
        "parameters": [
          { "type": "image", "image": { "link": "https://cdn.example.com/promo/october.jpg" } }
        ]
      },
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "20%" }
        ]
      }
    ]
  }
}

Document (PDF) by media ID

{
  "messaging_product": "whatsapp",
  "to": "16505551234",
  "type": "template",
  "template": {
    "name": "invoice_ready",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "header",
        "parameters": [
          {
            "type": "document",
            "document": { "id": "1013859600285441", "filename": "invoice-october.pdf" }
          }
        ]
      },
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Carlos" },
          { "type": "text", "text": "11/10/2026" }
        ]
      }
    ]
  }
}

filename is optional and comes from the document object used in document messages: the WhatsApp app uses the extension to show the right file icon. In Brazil, this pattern is common for sending a boleto (a bank payment slip) as a PDF header.

For video, the structure is the same: "type": "video" and a video object with link or id.

To get a media ID, upload the file with POST /{PHONE_NUMBER_ID}/media. Size limits, supported formats and the upload walkthrough are in how to send images, PDFs, audio and video via the WhatsApp API.

4. Dynamic URL button

A URL button can have one variable, always at the end of the URL. At creation, something like https://shop.example.com/order/{{1}}. At send time, you only pass the suffix:

{
  "messaging_product": "whatsapp",
  "to": "16505551234",
  "type": "template",
  "template": {
    "name": "order_tracking",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "#48213" }
        ]
      },
      {
        "type": "button",
        "sub_type": "url",
        "index": "0",
        "parameters": [
          { "type": "text", "text": "48213?utm_source=whatsapp" }
        ]
      }
    ]
  }
}

Three rules that break a lot of integrations:

  • index is the button’s position in the template, starting at 0. If the URL button is the second approved button, index is 1. Meta explains that when buttons use variables, their type and order must match the template; a wrong index returns an error like “Parameter value for URL was expected but was not found”.
  • Percent-encode special characters before sending. The components page lists space (%20), colon (%3A), pipe (%7C) and accented letters: Gonçalves becomes Gon%C3%A7alves. Unencoded values can make the URL fail validation and the send fail.
  • A URL button without a variable is not part of the request. Only include a button component if that button has a dynamic part.

Meta’s own examples sometimes use index as a string ("0") and sometimes as a number (0). Pick one and keep it consistent in your code. To track clicks in GA4 with UTM parameters in that suffix, see the guide on dynamic URL buttons with UTM.

5. Quick reply with a custom payload

Quick reply buttons are defined at creation with fixed label text (up to 25 characters) and need nothing at send time. What you can send is your own payload, which comes back in the webhook when the customer taps the button. That tells you exactly which campaign, order or offer triggered the reply.

The syntax appears in Meta’s carousel template documentation, which describes payload as optional and delivered in messages.button.payload:

{
  "messaging_product": "whatsapp",
  "to": "16505551234",
  "type": "template",
  "template": {
    "name": "appointment_reminder",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "Anna" },
          { "type": "text", "text": "Nov 5 at 2 pm" }
        ]
      },
      {
        "type": "button",
        "sub_type": "quick_reply",
        "index": 0,
        "parameters": [
          { "type": "payload", "payload": "confirm:appt:98765" }
        ]
      },
      {
        "type": "button",
        "sub_type": "quick_reply",
        "index": 1,
        "parameters": [
          { "type": "payload", "payload": "reschedule:appt:98765" }
        ]
      }
    ]
  }
}

When the customer taps “Confirm”, your webhook receives a message with "type": "button":

{
  "context": { "from": "15550783881", "id": "wamid.HBgLNTUx..." },
  "from": "16505551234",
  "id": "wamid.HBgLNTUy...",
  "timestamp": "1761750000",
  "type": "button",
  "button": { "payload": "confirm:appt:98765", "text": "Confirm" }
}

context.id is the wamid of the template you sent, and button.payload carries your key. In the button webhook reference example, with no custom payload, payload and text carry the same button label. Remember that, at creation, quick reply buttons must be grouped together (all before or all after the other button types), and a template supports up to 10 of them.

6. Coupon code (copy_code)

Marketing templates can include a “copy code” button that copies a string of up to 20 characters to the phone’s clipboard. At send time, the button uses sub_type: "copy_code" and a coupon_code parameter, per the coupon templates page:

{
  "messaging_product": "whatsapp",
  "to": "16505551234",
  "type": "template",
  "template": {
    "name": "black_friday_coupon",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "parameter_name": "coupon_code", "text": "BLACK25" },
          { "type": "text", "parameter_name": "discount", "text": "25%" }
        ]
      },
      {
        "type": "button",
        "sub_type": "copy_code",
        "index": 0,
        "parameters": [
          { "type": "coupon_code", "coupon_code": "BLACK25" }
        ]
      }
    ]
  }
}

As on Meta’s page, the example uses a template with named body variables; for a positional template, drop parameter_name. Notice the code appears twice: once in the body (for the customer to read) and once in the button (to copy). They are independent fields; if you generate unique coupons per customer, fill both with the same value. A template supports one copy code button.

7. Combined example: image, named variables, URL and quick reply

Real marketing templates usually combine several pieces. Assume an approved template with an image header, the body Hi {{first_name}}, the {{collection}} collection is here!, button 0 as a dynamic URL and button 1 as a quick reply “Stop promotions”:

curl -X POST "https://graph.facebook.com/v26.0/$PHONE_NUMBER_ID/messages" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "recipient_type": "individual",
    "to": "16505551234",
    "type": "template",
    "template": {
      "name": "new_collection",
      "language": { "code": "en_US" },
      "components": [
        {
          "type": "header",
          "parameters": [
            { "type": "image", "image": { "id": "1339522734477770" } }
          ]
        },
        {
          "type": "body",
          "parameters": [
            { "type": "text", "parameter_name": "first_name", "text": "Bruna" },
            { "type": "text", "parameter_name": "collection", "text": "Summer 2027" }
          ]
        },
        {
          "type": "button",
          "sub_type": "url",
          "index": "0",
          "parameters": [
            { "type": "text", "parameter_name": "slug", "text": "summer-2027" }
          ]
        },
        {
          "type": "button",
          "sub_type": "quick_reply",
          "index": "1",
          "parameters": [
            { "type": "payload", "payload": "optout:summer_campaign" }
          ]
        }
      ]
    }
  }'

The parameter_name on the URL button only applies if the template is named and the URL uses {{slug}}; for a positional template, drop that key. For cards with an image and a button per card, the structure changes (a carousel component), covered in the WhatsApp carousel template guide.

The language field

language.code must be exactly the code the template was created with. A template named order_confirmed in en_US and another with the same name in pt_BR are two different templates. Some codes from the supported languages list:

LanguageCode
Englishen
English (US)en_US
English (UK)en_GB
English (India)en_IN
Spanish (Mexico)es_MX
Spanish (Spain)es_ES
Portuguese (Brazil)pt_BR
Portuguese (Portugal)pt_PT

Sending en for a template created in en_US? You get error 132001, as if the template didn’t exist.

The API response

When the request is accepted, the API returns:

{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "16505551234", "wa_id": "16505551234" }],
  "messages": [{ "id": "wamid.HBgLMTY1MDM4Nzk0MzkVAgARGBJBRkJENzEx...", "message_status": "accepted" }]
}

Store the id (the wamid): it’s what shows up in status webhooks (sent, delivered, read, failed). The message_status field only appears when the template is under pacing. A 200 means “accepted”, not “delivered”; some errors only show up later, in a webhook with status: failed.

Common parameter errors

These are the codes you’ll see most when sending templates, with descriptions from Meta’s error code table:

CodeWhat it meansTypical cause
132000Number of parameter values doesn’t match the templateA missing or extra text in the body; forgot the dynamic URL button component
132001Template doesn’t exist in that language or isn’t approvedWrong language.code, typo in the name, template still PENDING
132005Translated text is too longCheck in WhatsApp Manager that the template translation is complete
132007Template content violates a WhatsApp policyReview the content against the template review rules
132012Parameter values formatted differently from the templateparameter_name on a positional template (or vice versa); text where image was expected
132015Template paused for low qualityEdit the template and wait for re-approval
132016Template paused too many times and permanently disabledCreate a new template with different content
131008A required parameter is missingA required field such as language or name is absent
131009One or more parameter values are invalidA value outside the accepted format for a field
100Unsupported or misspelled parameterA typo in a key (paramters, langauge)

Errors 132000 and 132012 have their own deep dive: errors 132000 and 132012. For the rest of the API’s codes, see the WhatsApp API error codes guide.

A quick checklist before opening a support ticket:

  1. Open the template in WhatsApp Manager (or query it through the templates API) and confirm status: APPROVED, the language and the parameter_format.
  2. Count the placeholders in each component and compare them with the parameters you send.
  3. Make sure every button with a variable has a button component with the right index.
  4. Open the media URL in a private window and percent-encode dynamic URL values.

How much does a template message cost

Price depends on the template category and the recipient’s country. For recipients in Brazil, since October 1, 2026 Meta charges BRL 0.3217 per marketing template and BRL 0.0350 per utility or authentication template. Also since October 1, 2026, utility templates sent inside the customer service window are charged. How Meta assigns a category is explained in marketing, utility or authentication.

How to send templates with Vectalk

Vectalk is a proxy for the official Cloud API: the request body is the same as Meta’s, only the URL and the token change. Every payload in this article works on POST https://apis.vectalk.com.br/api/messages; just save the JSON to a file and send it:

curl -X POST https://apis.vectalk.com.br/api/messages \
  -H "Authorization: Bearer $VECTALK_TOKEN" \
  -H "Content-Type: application/json" \
  -d @template.json

With an instance token, the phone_number_id is resolved automatically. With an organization token, add "phone_number_id" to the body to choose which number sends the message, as described in the authentication docs. The endpoint reference is at Template in the Vectalk API.

You create and track the templates themselves in the dashboard (Templates, with the Template Library) or through the templates API, using an instance token. For billing, each delivered message is deducted from your prepaid balance, and you don’t need to add a credit card on Meta. Create your account at apps.vectalk.com.br/register.

Frequently asked questions

Do I need to send the template text in the request?

No. The fixed text is already approved by Meta. The request only carries the name, the language and the values for variables, header media and dynamic buttons.

What’s the difference between positional and named variables?

Positional uses {{1}}, {{2}} and requires values in text order. Named uses {{first_name}} and each value carries a parameter_name, in any order. The format is chosen when the template is created; if nobody picks one, positional is the default.

Can I change the header image on every send?

Yes. The approved image is only a sample for review. On each send you pass whatever media you want in the header component, by link or id, as long as it’s the same type defined in the template (image, video or document).

Why do I get a 200 but the message never arrives?

A 200 only confirms the API accepted the request. Template, number or limit errors can arrive later, in the status webhook with failed and an error code. Monitor webhooks, not just the HTTP response.


Official sources checked on October 29, 2026: Template fundamentals (Meta), Template components (Meta), Template media (Meta), Utility templates (Meta), Custom marketing templates (Meta), Coupon code templates (Meta), Media card carousel templates (Meta), Supported languages (Meta), Send messages (Meta), Document messages (Meta), Button messages webhook (Meta), Error codes (Meta), Template sending in the Vectalk API, Vectalk API authentication.

Try the official WhatsApp API today

Create your account, generate a token and send your first message with the quick-start guide. Same payloads and webhooks as Meta's Cloud API.