SigFollow
Sign in

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

POST/v1/:wabaId/message_templates

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

NameTypeRequiredDescription
namestringyesTemplate name — lowercase letters, digits, and underscores only. Must be unique within the WABA.
languagestringyesBCP-47 locale code, e.g. en_US, zh_CN, es_ES.
categorystringyesOne of UTILITY, MARKETING, AUTHENTICATION. Determines pricing tier and Meta review rules.
componentsarrayyesSame component schema as Meta's Graph API — header / body / footer / buttons. See Meta's template reference for the per-component schema.
parameter_formatstringnoPOSITIONAL (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"
}
Approval is asynchronous
Templates start in 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).

Three ways to supply the header media
IMAGE / VIDEO / DOCUMENT headers can be sourced via one of three mechanisms — pick based on whether the image already lives in SigFollow's library:
  • 🥇 Pass header_media_file_id (recommended) — reference an existing library asset by its file_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 separate template_media call 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-use header_handle of the form 4::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_id returned by POST /v1/:phoneNumberId/media cannot be used here — Meta keeps the two upload mechanisms strictly separate. Passing a Cloud API media_id as header_handle is rejected at template review.
request bodyjson
{
  "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" }
      ]
    }
  ]
}
JSON

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

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

Named

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.

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

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

Two hard rules on URL button variables
Meta's template review enforces:
  • 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
Plan your URL structure so the dynamic part naturally lands at the end. If you need two variables (e.g. order ID + tenant slug), encode them into a single opaque token on your side and substitute that one value.
request bodyjson
{
  "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"]
        }
      ]
    }
  ]
}
JSON

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

TTL ranges by template category
  • 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.

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

List templates

GET/v1/:wabaId/message_templates

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

NameTypeRequiredDescription
namestringnoFilter 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.