API reference
Messages
Send WhatsApp messages through the SigFollow gateway. The request and response shapes mirror Meta Cloud API exactly, so existing Cloud API clients work after a base URL swap.
Send a message
/v1/:phoneNumberId/messagesScope: messages:send
The :phoneNumberId path parameter is your Meta phone number ID — the same value you would use against graph.facebook.com. SigFollow validates that the ID belongs to the API key's tenant before forwarding to Meta.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| phoneNumberId | string | yes | Meta phone number ID. Find it on the WABA channel detail page or in Meta Business Manager. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| filter_blacklist | boolean | no | When true, the recipient is checked against the tenant blacklist before sending. A blacklisted number is rejected with HTTP 422 and error_data.reason = recipient_blacklisted. No message is created and Meta is not called. Default false. |
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| messaging_product | string ("whatsapp") | yes | Must be the literal value "whatsapp". |
| to | string | yes | Recipient phone number in E.164 form without the + prefix. Use {{Recipient-Phone-Number}} as a placeholder in sample code. |
| type | string | yes | One of text, image, document, video, audio, sticker, location, contacts, reaction, interactive, or template. |
| <type-object> | object | yes | A field matching type (e.g. text when type=text). See Meta's reference for the per-type schema — SigFollow accepts the same fields. |
| context | object | no | Optional { message_id } reply context — quotes a previous inbound message in the WhatsApp UI. |
Example: text message
curl -X POST https://api.sigfollow.com/v1/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "text",
"text": { "body": "Hello from SigFollow", "preview_url": false }
}'Response
{
"messaging_product": "whatsapp",
"contacts": [{ "input": "{{Recipient-Phone-Number}}", "wa_id": "{{Recipient-Phone-Number}}" }],
"messages": [{ "id": "wamid.HBgN..." }]
}| Name | Type | Required | Description |
|---|---|---|---|
| messages[].id | string | no | Meta WAMID — the same identifier Meta returns. Use this to correlate status webhooks (sent / delivered / read) and to quote later via context.message_id. |
| contacts[].wa_id | string | no | Normalized WhatsApp ID for the recipient. |
Other message types
Same endpoint, different type + payload field. A few common shapes:
Image (URL)
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "image",
"image": {
"link": "https://example.com/picture.jpg",
"caption": "Optional caption"
}
}Use id instead of link to reference a media previously uploaded via the media endpoint.
text calls return error code 131026.Template message recipes
All recipes share the same endpoint POST /v1/:phoneNumberId/messages; only the JSON template body shape changes. The template itself must already be APPROVED in Meta — see Templates for creation.
components in any order, but the convention is header → body → footer → buttons. The indexon button parameters must match the button's position in the template definition (0-based), not its position in this array.1. Header with image
Template has an IMAGE header and a single positional body variable. The header parameter accepts either image.link (publicly accessible URL) or image.id (a media ID from the media endpoint).
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "weekly_promo",
"language": { "code": "en_US" },
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": { "link": "https://cdn.example.com/banner.jpg" }
}
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Alice" }
]
}
]
}
}curl -X POST https://api.sigfollow.com/v1/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "weekly_promo",
"language": { "code": "en_US" },
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": { "link": "https://cdn.example.com/banner.jpg" }
}
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Alice" }
]
}
]
}
}
JSON2. Body with multiple variables (named vs positional)
Meta supports two parameter styles in a body component. The style is fixed at template-creation time and shows up in the template definition as either {{1}} {{2}} {{3}} (positional) or {{customer_name}} {{order_id}} (named). The send payload must match.
Positional
Pass parameters in the same order as {{1}}, {{2}}, {{3}} appear in the body text. No parameter_name field.
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "order_shipped_positional",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Alice" },
{ "type": "text", "text": "ORD-90211" },
{ "type": "text", "text": "Apr 22" }
]
}
]
}
}curl -X POST https://api.sigfollow.com/v1/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "order_shipped_positional",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Alice" },
{ "type": "text", "text": "ORD-90211" },
{ "type": "text", "text": "Apr 22" }
]
}
]
}
}
JSONNamed
Every parameter carries a parameter_name that must match a placeholder declared in the template. Order is irrelevant — Meta resolves by name. Use named variables when your template has more than a few variables or when you want to refactor the template later without re-aligning every caller.
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "order_shipped_named",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Alice" },
{ "type": "text", "parameter_name": "order_id", "text": "ORD-90211" },
{ "type": "text", "parameter_name": "ship_date", "text": "Apr 22" }
]
}
]
}
}curl -X POST https://api.sigfollow.com/v1/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "order_shipped_named",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Alice" },
{ "type": "text", "parameter_name": "order_id", "text": "ORD-90211" },
{ "type": "text", "parameter_name": "ship_date", "text": "Apr 22" }
]
}
]
}
}
JSONparameter_name on a positional template (or omitting it on a named one) returns Meta error code 132012"Parameter format mismatch".3. Buttons (quick reply + dynamic URL + static URL)
The button schema for sending follows two rules:
- QUICK_REPLY — always required at send time. The
payloadstring is what your inbound webhook receives when the user taps; routinely 64–128 chars of opaque correlation data. - URL with a placeholder (dynamic URL) — required at send time, the
textparameter fills the{{1}}suffix on the template's URL. So a template URLhttps://shop.example.com/orders/{{1}}becomeshttps://shop.example.com/orders/ORD-90211. - URL without placeholders (static URL) — do not include it in the send payload. Meta uses the URL fixed in the template itself.
Below, the template has three buttons in this order: 0 QUICK_REPLY "Track package", 1URL (dynamic) " View order", 2URL (static) "Contact support". Only buttons 0 and 1 appear in the request body.
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "shipping_with_actions",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "ORD-90211" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "0",
"parameters": [
{ "type": "payload", "payload": "TRACK_PKG_90211" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "1",
"parameters": [
{ "type": "text", "text": "ORD-90211" }
]
}
]
}
}curl -X POST https://api.sigfollow.com/v1/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "shipping_with_actions",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "ORD-90211" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "0",
"parameters": [
{ "type": "payload", "payload": "TRACK_PKG_90211" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "1",
"parameters": [
{ "type": "text", "text": "ORD-90211" }
]
}
]
}
}
JSON4. Full scenario — header image + named body + multiple buttons
Everything combined: an image header, three named body variables, two quick replies, and one dynamic URL button. Static buttons (if any in the template) are omitted from the request.
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "order_confirmation_v2",
"language": { "code": "en_US" },
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": { "link": "https://cdn.example.com/orders/ORD-90211.jpg" }
}
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Alice" },
{ "type": "text", "parameter_name": "order_id", "text": "ORD-90211" },
{ "type": "text", "parameter_name": "amount", "text": "$259.00" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "0",
"parameters": [
{ "type": "payload", "payload": "TRACK_PKG_90211" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "1",
"parameters": [
{ "type": "payload", "payload": "CONTACT_AGENT_90211" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "2",
"parameters": [
{ "type": "text", "text": "ORD-90211" }
]
}
]
}
}curl -X POST https://api.sigfollow.com/v1/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "order_confirmation_v2",
"language": { "code": "en_US" },
"components": [
{
"type": "header",
"parameters": [
{
"type": "image",
"image": { "link": "https://cdn.example.com/orders/ORD-90211.jpg" }
}
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "parameter_name": "customer_name", "text": "Alice" },
{ "type": "text", "parameter_name": "order_id", "text": "ORD-90211" },
{ "type": "text", "parameter_name": "amount", "text": "$259.00" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "0",
"parameters": [
{ "type": "payload", "payload": "TRACK_PKG_90211" }
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "1",
"parameters": [
{ "type": "payload", "payload": "CONTACT_AGENT_90211" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "2",
"parameters": [
{ "type": "text", "text": "ORD-90211" }
]
}
]
}
}
JSON5. OTP / Authentication template
Meta's Authentication templatecategory (one-tap autofill) is the recommended path for OTP delivery — it skirts most pacing rules and renders a native "Copy code" or "Auto fill" CTA. The template body has one positional variable for the code, and the button (declared sub_type="url"in the template; Meta's autofill machinery is invisible) receives the same code at send time so the receiving app can autofill it.
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "verification_otp",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "123456" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{ "type": "text", "text": "123456" }
]
}
]
}
}curl -X POST https://api.sigfollow.com/v1/PHONE_NUMBER_ID/messages \
-H "Authorization: Bearer sflo_live_xxx" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"messaging_product": "whatsapp",
"to": "{{Recipient-Phone-Number}}",
"type": "template",
"template": {
"name": "verification_otp",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "123456" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{ "type": "text", "text": "123456" }
]
}
]
}
}
JSONcrypto.randomInt(100000, 1000000)) and short-lived (5 minutes is conventional). Send them only over the Authentication template — using a generic textmessage reduces deliverability and bypasses Meta's anti-fraud rate-limits.Errors
401code190— invalid / revoked API key403code200— missing scope or phone number not inallowedPhoneNumberIds400code100— request body validation failure422code131000error_data.reason = recipient_blacklisted— only when?filter_blacklist=true400code131026— 24h window expired, template required429code4— rate limit exceeded
See the error reference for full body schema.