SigFollow
Sign in

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

POST/v1/:phoneNumberId/messages

Scope: 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

NameTypeRequiredDescription
phoneNumberIdstringyesMeta phone number ID. Find it on the WABA channel detail page or in Meta Business Manager.

Query parameters

NameTypeRequiredDescription
filter_blacklistbooleannoWhen 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

NameTypeRequiredDescription
messaging_productstring ("whatsapp")yesMust be the literal value "whatsapp".
tostringyesRecipient phone number in E.164 form without the + prefix. Use {{Recipient-Phone-Number}} as a placeholder in sample code.
typestringyesOne of text, image, document, video, audio, sticker, location, contacts, reaction, interactive, or template.
<type-object>objectyesA field matching type (e.g. text when type=text). See Meta's reference for the per-type schema — SigFollow accepts the same fields.
contextobjectnoOptional { 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..." }]
}
Response fields
NameTypeRequiredDescription
messages[].idstringnoMeta 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_idstringnoNormalized 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.

24-hour window for non-template sends
WhatsApp's customer-service window only allows free-form messages within 24 hours of the recipient's last inbound message. Outside that window, only approved templates may be sent — plain 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.

Headers up on `components` order
Meta accepts 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).

request bodyjson
{
  "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" }
        ]
      }
    ]
  }
}
JSON

2. 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.

request bodyjson
{
  "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" }
        ]
      }
    ]
  }
}
JSON

Named

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.

request bodyjson
{
  "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" }
        ]
      }
    ]
  }
}
JSON
Mixing styles is rejected
A template is either positional or named end-to-end. Passing parameter_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 payload string 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 text parameter fills the {{1}}suffix on the template's URL. So a template URL https://shop.example.com/orders/{{1}} becomes https://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.

request bodyjson
{
  "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" }
        ]
      }
    ]
  }
}
JSON

4. 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.

request bodyjson
{
  "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" }
        ]
      }
    ]
  }
}
JSON

5. 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.

request bodyjson
{
  "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" }
        ]
      }
    ]
  }
}
JSON
Don't reuse OTP templates
Codes must be cryptographically random per request (e.g. crypto.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

  • 401 code 190 — invalid / revoked API key
  • 403 code 200 — missing scope or phone number not in allowedPhoneNumberIds
  • 400 code 100 — request body validation failure
  • 422 code 131000 error_data.reason = recipient_blacklisted — only when ?filter_blacklist=true
  • 400 code 131026 — 24h window expired, template required
  • 429 code 4 — rate limit exceeded

See the error reference for full body schema.