Send WhatsApp templates with AvenPing API
Use
/v2/templatesto list Meta templates, fetch one by name, and send or test-send withphoneNumberId. Send-timecomponentsare forwarded to Cloud API; templateexamplefields are not applied automatically.
Base: /v2/templates
Auth + Premium/Enterprise + active plan required. POST routes also require phoneNumberId ownership — the id may belong to any linked WhatsApp account. See overview — Phone number ownership and Account.
List/get WABA: GET /v2/templates and GET /v2/templates/:name use the default WABA from the token. Pass phoneNumberId (Meta) or accountId (from GET /v2/me) as a query parameter to list templates for a specific linked WABA.
Also read: HTTP API v2 Overview. Meta Cloud API: Send template messages.
Prerequisites
- An API token and Premium/Enterprise plan (Authentication)
- A
phoneNumberIdfrom GET /v2/me - An approved WhatsApp message template on the WABA you will send from
Step-by-step
Authenticate
SendAuthorization: Bearer YOUR_TOKENorX-API-Token: YOUR_TOKEN.Select the WABA
For list/get, omit query params to use the default WABA, or passphoneNumberIdoraccountId. For send,phoneNumberIdin the JSON body is required and must belong to a linked account.List or fetch the template
GET /v2/templateslists Meta templates.GET /v2/templates/:nameloads up to 200 templates from Meta and finds an exact name match. If the template exists but is not in that window, you may get404 NOT_FOUND— use list withsearch/ paging, or send with the known name.Send with to or recipient
POST /v2/templates/sendrequirestemplateName,phoneNumberId, andto(phone) orrecipient(BSUID). Authentication (OTP) templates requireto. Invalid JSON shape returns400 VALIDATION_ERROR(Zoddetails). A blocked contact returns403 FORBIDDENand does not call Meta.
GET /v2/templates
Query (optional)
| Name | Description |
|---|---|
limit | Passed to Meta message templates API when set |
after | Cursor for paging |
search | Case-insensitive substring filter on template name (applied after fetch) |
phoneNumberId | Meta phone number ID — list templates for the WABA that owns this line |
accountId | AvenPing WABA id from GET /v2/me accounts |
Without phoneNumberId / accountId, templates are listed from the default WABA (data.account on /v2/me).
Example request:
bash
curl -sS "https://api.avenping.com/v2/templates?limit=10&search=order" \
-H "Authorization: Bearer YOUR_TOKEN"Response 200 — items 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
Same optional phoneNumberId / accountId query as list, to fetch from a specific linked WABA. Without them, the default WABA is used.
The server loads up to 200 templates from Meta and finds an exact name match. If the template exists but is not in that window, you may get 404. Use GET /v2/templates with search / paging, or send with the known 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)
| Field | Type | Required | Notes |
|---|---|---|---|
phoneNumberId | string | yes | Meta phone number ID (must belong to a linked WABA) |
to | string | one of | E.164-like; non-digits stripped. Use when the contact’s phone is known |
recipient | string | one of | Business-Scoped User ID (BSUID). Same rules as Messages |
templateName | string | yes | Template name |
language | string | no | Default en_US |
components | array | no | Meta send-time template components (see below). Omitted or [] only when the template has no variables. |
contextMessageWamid | string | no | Reply context wamid |
Provide to or recipient. Authentication (OTP) templates require a phone (to); Meta returns 131062 for BSUID-only auth sends.
A successful send also upserts the contact and inbox thread in AvenPing (same as the dashboard). Blocked contacts return 403 FORBIDDEN.
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, builds Graph to / recipient the same way as Messages, and forwards components unchanged inside the Cloud API message payload.
Practical workflow:
- Call
GET /v2/templates/:name(or list and pick one) to seecomponents, variables, and anyexampleblocks. - 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. - Omit
componentsor 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/upload (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 offer — limited_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 200 — data 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.
FAQ
Does send apply template example values automatically?
No. POST /v2/templates/send forwards your components to Cloud API. Copy sample strings from example only for sandbox tests; send real values in production.
How do I send a template for a non-default WABA?
Pass phoneNumberId (any linked line) on send. For list/get, pass phoneNumberId or accountId as a query parameter.
Why does GET /v2/templates/:name return 404 for a template I know exists?
The server loads up to 200 Meta templates and finds an exact name match. If the template is outside that window, use GET /v2/templates with search / paging, or send with the known name on POST /v2/templates/send.
Can I send to a BSUID instead of a phone?
Yes. Provide to or recipient (Business-Scoped User ID). Authentication (OTP) templates require a phone (to); Meta returns 131062 for BSUID-only auth sends. A blocked contact returns 403 FORBIDDEN.