Skip to content

Templates

Base: /v2/templates

Auth + Premium/Enterprise + active plan required. POST routes also require phoneNumberId ownership — see overview — Phone number ownership and Account.

Also read: HTTP API v2 Overview


GET /v2/templates

Query (optional)

NameDescription
limitPassed to Meta message templates API when set
afterCursor for paging
searchCase-insensitive substring filter on template name (applied after fetch)

Example request:

bash
curl -sS "https://api.avenping.com/v2/templates?limit=10&search=order" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response 200items are Meta template objects; paging mirrors Meta when present.

json
{
  "success": true,
  "data": {
    "items": [
      {
        "name": "order_confirmation",
        "components": [],
        "language": "en_US",
        "status": "APPROVED",
        "category": "UTILITY",
        "id": "123456789012345"
      }
    ],
    "paging": {
      "cursors": {
        "before": "QVFIUz...",
        "after": "QVFIUz..."
      },
      "next": "https://graph.facebook.com/..."
    }
  }
}

GET /v2/templates/:name

Example request:

bash
curl -sS "https://api.avenping.com/v2/templates/order_confirmation" \
  -H "Authorization: Bearer YOUR_TOKEN"

Response 200 — single Meta template object in data (shape similar to one element of items above).

Response 404:

json
{
  "success": false,
  "error": "NOT_FOUND",
  "message": "Template not found"
}

POST /v2/templates/send

Body (JSON)

FieldTypeRequiredNotes
phoneNumberIdstringyesMeta phone number ID (must belong to account)
tostringyesE.164-like; non-digits stripped
templateNamestringyesTemplate name
languagestringnoDefault en_US
componentsarraynoMeta send-time template components (see below). Omitted or [] only when the template has no variables.
contextMessageWamidstringnoReply context wamid

How this relates to template example fields

GET /v2/templates and GET /v2/templates/:name return Meta template definition objects. Those definitions often include example data (sample text, sample media handles, URL suffix samples, and so on) so Meta and tooling can preview the template.

POST /v2/templates/send does not read or apply those examples. The route validates the JSON body, strips non-digits from to, and forwards components unchanged inside the Cloud API message payload.

Practical workflow:

  1. Call GET /v2/templates/:name (or list and pick one) to see components, variables, and any example blocks.
  2. Map each variable your template expects to a real value (production) or, for sandbox tests, you may temporarily reuse values from example—they are not substituted automatically.
  3. Omit components or send [] only if the template has no header/body/button/carousel/LTO parameters.

Send payloads use lowercase type values on each component: header, body, button, carousel, limited_time_offer, etc., as required by the messages API.


Example: components by template shape

The following snippets are illustrative; field names must match your approved template (language code, number of parameters, button order).

Static template (no variables) — omit components or use an empty array:

json
{
  "phoneNumberId": "109876543210987",
  "to": "+15551234567",
  "templateName": "hello_world",
  "language": "en_US",
  "components": []
}

Body only (positional 1, 2)

json
{
  "phoneNumberId": "109876543210987",
  "to": "+15551234567",
  "templateName": "order_shipped",
  "language": "en_US",
  "components": [
    {
      "type": "body",
      "parameters": [
        { "type": "text", "text": "Alex" },
        { "type": "text", "text": "TRACK123" }
      ]
    }
  ]
}

Body with named parameters (when the template uses named variables—include parameter_name as in Meta’s spec)

json
{
  "type": "body",
  "parameters": [
    {
      "type": "text",
      "parameter_name": "customer_name",
      "text": "Alex"
    }
  ]
}

Header: TEXT

json
{
  "type": "header",
  "parameters": [{ "type": "text", "text": "Weekly deals" }]
}

Header: IMAGE / VIDEO / DOCUMENT — use a media id from POST /v2/media (or your own upload to Meta), not the template’s sample header_handle URL, unless your integration explicitly uses handles as allowed by Meta for your account/version.

json
{
  "type": "header",
  "parameters": [
    { "type": "image", "image": { "id": "1234567890" } }
  ]
}

Header: PRODUCT (catalog product in header)

json
{
  "type": "header",
  "parameters": [
    {
      "type": "product",
      "product": {
        "product_retailer_id": "sku-123"
      }
    }
  ]
}

Button: dynamic URL (https://example.com/1)

json
{
  "type": "button",
  "sub_type": "url",
  "index": "0",
  "parameters": [{ "type": "text", "text": "invoices/INV-9" }]
}

Button: COPY_CODE

json
{
  "type": "button",
  "sub_type": "copy_code",
  "index": "1",
  "parameters": [{ "type": "coupon_code", "coupon_code": "SAVE20" }]
}

Button: CATALOG (optional thumbnail product)

json
{
  "type": "button",
  "sub_type": "catalog",
  "index": "0",
  "parameters": [
    {
      "type": "action",
      "action": {
        "thumbnail_product_retailer_id": "sku-123"
      }
    }
  ]
}

Button: MPM (multi-product message; shape must match your template and catalog—often includes thumbnail_product_retailer_id and sections with product_items)

json
{
  "type": "button",
  "sub_type": "mpm",
  "index": "0",
  "parameters": [
    {
      "type": "action",
      "action": {
        "thumbnail_product_retailer_id": "sku-123",
        "sections": [
          {
            "title": "Featured",
            "product_items": [{ "product_retailer_id": "sku-123" }]
          }
        ]
      }
    }
  ]
}

Body: currency / date_time (when the template was created with those variable types)

json
{
  "type": "body",
  "parameters": [
    {
      "type": "currency",
      "currency": {
        "fallback_value": "$99.99",
        "code": "USD",
        "amount_1000": 99990
      }
    },
    {
      "type": "date_time",
      "date_time": { "fallback_value": "April 10, 2026" }
    }
  ]
}

Quick-reply buttons on a template usually require no button component in the send payload unless Meta’s spec for your template version says otherwise.

Carousel — top-level body (parameters or empty array if required by the template) plus carousel with cards; each card contains its own components (typically header with media or product, and body parameters).

json
{
  "components": [
    {
      "type": "body",
      "parameters": [{ "type": "text", "text": "Spring collection" }]
    },
    {
      "type": "carousel",
      "cards": [
        {
          "card_index": 0,
          "components": [
            {
              "type": "header",
              "parameters": [
                { "type": "image", "image": { "id": "MEDIA_ID_CARD_0" } }
              ]
            },
            {
              "type": "body",
              "parameters": [{ "type": "text", "text": "Item A" }]
            }
          ]
        }
      ]
    }
  ]
}

Limited-time offerlimited_time_offer component with expiration, plus any required copy_code / dynamic url buttons your template defines:

json
{
  "components": [
    {
      "type": "limited_time_offer",
      "parameters": [
        {
          "type": "limited_time_offer",
          "limited_time_offer": { "expiration_time_ms": 1712876400000 }
        }
      ]
    },
    {
      "type": "button",
      "sub_type": "copy_code",
      "index": "0",
      "parameters": [{ "type": "coupon_code", "coupon_code": "FLASH10" }]
    }
  ]
}

TypeScript: using example only as dev defaults (optional)

This does not run on the server; it shows how you might derive placeholder strings from a template definition when building components for tests. Always send real user data in production.

typescript
type MetaTemplate = {
  name?: string;
  language?: string;
  components?: any[];
};

/** Pull sample strings from Meta template definition `example` blocks — for local/sandbox only. */
function devDefaultsFromTemplate(template: MetaTemplate): {
  bodyTexts: string[];
  headerText?: string;
  urlSuffixByButtonIndex: Record<number, string>;
} {
  const bodyTexts: string[] = [];
  let headerText: string | undefined;
  const urlSuffixByButtonIndex: Record<number, string> = {};

  for (const c of template.components ?? []) {
    const t = String(c?.type ?? "").toUpperCase();
    if (t === "BODY" && c.example) {
      const named = c.example.body_text_named_params;
      const positional = c.example.body_text;
      if (Array.isArray(named)) {
        for (const row of named) {
          if (row?.example != null) bodyTexts.push(String(row.example));
        }
      } else if (Array.isArray(positional) && Array.isArray(positional[0])) {
        for (const x of positional[0]) bodyTexts.push(String(x));
      }
    }
    if (t === "HEADER" && (c.format === "TEXT" || c.format === "text") && c.example?.header_text?.[0]) {
      headerText = String(c.example.header_text[0]);
    }
    if (t === "BUTTONS" && Array.isArray(c.buttons)) {
      c.buttons.forEach((b: any, i: number) => {
        const bt = String(b?.type ?? "").toUpperCase();
        if (bt === "URL" && Array.isArray(b.example) && b.example[0]) {
          urlSuffixByButtonIndex[i] = String(b.example[0]);
        }
      });
    }
  }

  return { bodyTexts, headerText, urlSuffixByButtonIndex };
}

/** For named body variables: map template `example.body_text_named_params` → send parameters. */
function devNamedBodyParameters(template: MetaTemplate) {
  const params: { type: "text"; parameter_name: string; text: string }[] = [];
  for (const c of template.components ?? []) {
    if (String(c?.type ?? "").toUpperCase() !== "BODY") continue;
    const rows = c.example?.body_text_named_params;
    if (!Array.isArray(rows)) continue;
    for (const row of rows) {
      const name = row?.param_name ?? row?.parameter_name;
      if (name && row?.example != null) {
        params.push({
          type: "text",
          parameter_name: String(name),
          text: String(row.example),
        });
      }
    }
  }
  return params;
}

/** Minimal send body for AvenPing `POST /v2/templates/send`. */
function buildSendBody(opts: {
  phoneNumberId: string;
  to: string;
  templateName: string;
  language?: string;
  components: unknown[];
}) {
  return {
    phoneNumberId: opts.phoneNumberId,
    to: opts.to,
    templateName: opts.templateName,
    language: opts.language ?? "en_US",
    components: opts.components,
  };
}

// Example: fetch template, then build components using dev defaults for body only.
async function exampleFlow(apiBase: string, token: string, name: string) {
  const res = await fetch(`${apiBase}/v2/templates/${encodeURIComponent(name)}`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  const json = await res.json();
  const template: MetaTemplate = json.data;
  const { bodyTexts, headerText, urlSuffixByButtonIndex } = devDefaultsFromTemplate(template);

  const components: any[] = [];
  if (headerText) {
    components.push({ type: "header", parameters: [{ type: "text", text: headerText }] });
  }
  if (bodyTexts.length) {
    components.push({
      type: "body",
      parameters: bodyTexts.map((text) => ({ type: "text", text })),
    });
  }
  Object.entries(urlSuffixByButtonIndex).forEach(([idx, text]) => {
    components.push({
      type: "button",
      sub_type: "url",
      index: String(idx),
      parameters: [{ type: "text", text }],
    });
  });

  return buildSendBody({
    phoneNumberId: "YOUR_PHONE_NUMBER_ID",
    to: "+15551234567",
    templateName: name,
    language: template.language,
    components,
  });
}

Media headers cannot be filled from example.header_handle alone in most integrations—you still upload media and pass { type: "image", image: { id } } (or video/document).

If the template uses named body variables, build { type: "body", parameters: devNamedBodyParameters(template) } instead of the flat bodyTexts list from devDefaultsFromTemplate.

Example request (curl, static template):

bash
curl -sS -X POST "https://api.avenping.com/v2/templates/send" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "109876543210987",
    "to": "+1 555 123 4567",
    "templateName": "hello_world",
    "language": "en_US",
    "components": []
  }'

Response 200data is Meta’s successful messages API response (illustrative):

json
{
  "success": true,
  "data": {
    "messaging_product": "whatsapp",
    "contacts": [
      {
        "input": "15551234567",
        "wa_id": "15551234567"
      }
    ],
    "messages": [
      {
        "id": "wamid.HBgL..."
      }
    ]
  }
}

POST /v2/templates/:name/test-send

Same body as /send except templateName is taken from the path :name.

Example request:

bash
curl -sS -X POST "https://api.avenping.com/v2/templates/hello_world/test-send" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "109876543210987",
    "to": "15551234567",
    "language": "en_US"
  }'

Response 200 — same envelope as template send; data is Meta’s send response.