API reference
Templates
Submit message templates for Meta approval and list the templates already attached to your WABA. Templates are the only way to send free-form messages outside the 24-hour customer-service window.
Create a template
/v1/:wabaId/message_templatesScope: templates:write
The :wabaIdis your WhatsApp Business Account ID (same as Meta's). SigFollow forwards the create request to Meta and synchronizes the result into your local template catalog so the agent workbench can render it.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Template name — lowercase letters, digits, and underscores only. Must be unique within the WABA. |
| language | string | yes | BCP-47 locale code, e.g. en_US, zh_CN, es_ES. |
| category | string | yes | One of UTILITY, MARKETING, AUTHENTICATION. Determines pricing tier and Meta review rules. |
| components | array | yes | Same component schema as Meta's Graph API — header / body / footer / buttons. See Meta's template reference for the per-component schema. |
| parameter_format | string | no | POSITIONAL (default, {{1}} placeholders) or NAMED ({{order_id}}). SigFollow auto-detects NAMED when your component text contains {{word}}-style placeholders to avoid a known Meta quirk that breaks later sends. |
Example
curl -X POST https://api.sigfollow.com/v1/WABA_ID/message_templates \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "order_shipped",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has shipped."
}
]
}'Response
{
"id": "1234567890",
"status": "PENDING",
"category": "UTILITY"
}PENDING and move to APPROVED / REJECTEDafter Meta's review (minutes to hours). Subscribe to Meta's status webhooks for real-time updates, or poll the list endpoint.Create recipes
All recipes hit the same endpoint POST /v1/:wabaId/message_templates; only the JSON body changes. Field names mirror Meta's template-create spec exactly — the same body works directly against graph.facebook.com.
1. Header with image and quick-reply buttons
A MARKETING template with an IMAGE header and two QUICK_REPLY buttons. The header sample image is passed by Meta media handle (not a public URL or a regular media ID).
- 🥇 Pass
header_media_file_id(recommended) — reference an existing library asset by itsfile_id. One upload, many templates. The backend forks an immutable copy + obtains the Meta handle + injects it into the first non-TEXT HEADER component automatically. Skip the separatetemplate_mediacall entirely. See Library assets for the upload endpoint. - ✅ Use
POST /v1/:wabaId/template_media— SigFollow wraps Meta's multi-step Resumable Upload and returns a ready-to-useheader_handleof the form4::aW1hZ2UvanBlZw==:ARZ.... The handle is only valid at template creation— not reusable across templates. Best when the image lives only in your CDN and you don't want to mirror it into the SigFollow library. - ❌ The
media_idreturned byPOST /v1/:phoneNumberId/mediacannot be used here — Meta keeps the two upload mechanisms strictly separate. Passing a Cloud APImedia_idasheader_handleis rejected at template review.
{
"name": "weekly_promo_v1",
"language": "en_US",
"category": "MARKETING",
"components": [
{
"type": "HEADER",
"format": "IMAGE",
"example": {
"header_handle": ["4::aW1hZ2UvanBlZw==:ARZ...META_MEDIA_HANDLE..."]
}
},
{
"type": "BODY",
"text": "Your weekly promo is here. Tap below to claim or opt out."
},
{
"type": "BUTTONS",
"buttons": [
{ "type": "QUICK_REPLY", "text": "Claim now" },
{ "type": "QUICK_REPLY", "text": "Unsubscribe" }
]
}
]
}curl -X POST https://api.sigfollow.com/v1/WABA_ID/message_templates \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"name": "weekly_promo_v1",
"language": "en_US",
"category": "MARKETING",
"components": [
{
"type": "HEADER",
"format": "IMAGE",
"example": {
"header_handle": ["4::aW1hZ2UvanBlZw==:ARZ...META_MEDIA_HANDLE..."]
}
},
{
"type": "BODY",
"text": "Your weekly promo is here. Tap below to claim or opt out."
},
{
"type": "BUTTONS",
"buttons": [
{ "type": "QUICK_REPLY", "text": "Claim now" },
{ "type": "QUICK_REPLY", "text": "Unsubscribe" }
]
}
]
}
JSON2. Body with variables
Templates with variables must declare an exampleobject so Meta's review can render the template with sample values. There are two parameter styles — the style is fixed at template-creation time and dictates the call-site payload shape later (see Send recipe #2).
Positional
Placeholders {{1}}, {{2}}, etc.; the example object carries one row of values aligned by position.
{
"name": "order_shipped_positional",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has shipped and will arrive by {{3}}.",
"example": {
"body_text": [["Alice", "ORD-90211", "Apr 22"]]
}
}
]
}curl -X POST https://api.sigfollow.com/v1/WABA_ID/message_templates \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"name": "order_shipped_positional",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has shipped and will arrive by {{3}}.",
"example": {
"body_text": [["Alice", "ORD-90211", "Apr 22"]]
}
}
]
}
JSONNamed
Set parameter_format: "NAMED" at the top of the request and use {{customer_name}}-style placeholders. The example object becomes body_text_named_params with one param_name + example pair per variable.
{
"name": "order_shipped_named",
"language": "en_US",
"category": "UTILITY",
"parameter_format": "NAMED",
"components": [
{
"type": "BODY",
"text": "Hi {{customer_name}}, your order {{order_id}} has shipped and will arrive by {{ship_date}}.",
"example": {
"body_text_named_params": [
{ "param_name": "customer_name", "example": "Alice" },
{ "param_name": "order_id", "example": "ORD-90211" },
{ "param_name": "ship_date", "example": "Apr 22" }
]
}
}
]
}curl -X POST https://api.sigfollow.com/v1/WABA_ID/message_templates \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"name": "order_shipped_named",
"language": "en_US",
"category": "UTILITY",
"parameter_format": "NAMED",
"components": [
{
"type": "BODY",
"text": "Hi {{customer_name}}, your order {{order_id}} has shipped and will arrive by {{ship_date}}.",
"example": {
"body_text_named_params": [
{ "param_name": "customer_name", "example": "Alice" },
{ "param_name": "order_id", "example": "ORD-90211" },
{ "param_name": "ship_date", "example": "Apr 22" }
]
}
}
]
}
JSON3. URL button with a variable in the link
A dynamic URL button: the template stores a URL with a placeholder, and each send substitutes a value. The template-creation request needs an example array with one fully-formed URL (not the variable value alone) so Meta can verify the resulting destination.
- At most one variable per URL button — only
{{1}}is allowed.{{2}}cannot appear in the same button URL. - The variable must be the very last token of the URL — no path / query / fragment characters after it.
- ✅
https://shop.example.com/orders/{{1}} - ✅
https://shop.example.com/track?id={{1}} - ❌
https://shop.example.com/{{1}}/tracking— variable mid-path, rejected at create time - ❌
https://shop.example.com/orders?id={{1}}&type=ship— query content after the variable
- ✅
{
"name": "order_with_tracking",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Your order {{1}} is on its way.",
"example": {
"body_text": [["ORD-90211"]]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Track package",
"url": "https://shop.example.com/orders/{{1}}",
"example": ["https://shop.example.com/orders/ORD-90211"]
}
]
}
]
}curl -X POST https://api.sigfollow.com/v1/WABA_ID/message_templates \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"name": "order_with_tracking",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Your order {{1}} is on its way.",
"example": {
"body_text": [["ORD-90211"]]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Track package",
"url": "https://shop.example.com/orders/{{1}}",
"example": ["https://shop.example.com/orders/ORD-90211"]
}
]
}
]
}
JSON4. Set a delivery validity (TTL)
Add message_send_ttl_seconds at the top of the request to bound how long Meta will attempt delivery. If the recipient is offline past the TTL, the message expires with a Meta error code instead of sitting in the queue forever. Common use case: OTP messages that have no value once the user has moved on.
- AUTHENTICATION— 30 to 900 seconds (15 minutes). Default 10 minutes. Setting this equal to or shorter than your OTP's code-expiration window is recommended.
- UTILITY — 30 to 43,200 seconds (12 hours). Default 30 days. Omitting the field is notequivalent to "no TTL" — Meta still retries for the default 30 days; set this field explicitly if you want shorter retries.
- MARKETING — 43,200 to 2,592,000 seconds (12 hours to 30 days). Default 30 days. Customizing TTL on MARKETING templates requires Meta's Marketing Messages Lite API — the field may be accepted at template-create time but ignored on standard Cloud API sends.
Example: an Authentication template with one-tap OTP, 10-minute delivery TTL aligned with the code's expiration footer.
{
"name": "verification_otp",
"language": "en_US",
"category": "AUTHENTICATION",
"message_send_ttl_seconds": 600,
"components": [
{
"type": "BODY",
"add_security_recommendation": true
},
{
"type": "FOOTER",
"code_expiration_minutes": 10
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "OTP",
"otp_type": "COPY_CODE",
"text": "Copy code"
}
]
}
]
}curl -X POST https://api.sigfollow.com/v1/WABA_ID/message_templates \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"name": "verification_otp",
"language": "en_US",
"category": "AUTHENTICATION",
"message_send_ttl_seconds": 600,
"components": [
{
"type": "BODY",
"add_security_recommendation": true
},
{
"type": "FOOTER",
"code_expiration_minutes": 10
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "OTP",
"otp_type": "COPY_CODE",
"text": "Copy code"
}
]
}
]
}
JSONList templates
/v1/:wabaId/message_templatesScope: templates:read
Returns the local snapshot of templates SigFollow has synced for the WABA. Not paginated yet; the volume is bounded by Meta's per-WABA cap (currently ~250).
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | no | Filter by exact template name (case sensitive). |
Example
curl -G https://api.sigfollow.com/v1/WABA_ID/message_templates \
-H "Authorization: Bearer sflo_live_xxx" \
--data-urlencode "name=order_shipped"Response
{
"data": [
{
"id": "1234567890",
"name": "order_shipped",
"language": "en_US",
"status": "APPROVED",
"category": "UTILITY",
"components": [
{ "type": "BODY", "text": "Hi {{1}}, your order {{2}} has shipped." }
]
}
]
}Sending with a template
Once a template is APPROVED, use it from POST /messages with type: "template" and component parameter substitutions matching the placeholders defined in the template.