WhatsApp

Broadcasts and journeys

Send approved WhatsApp templates to an audience with campaigns or journey steps, fill parameters from each contact, and read skips and message costs.

A broadcast sends one approved template to many contacts. You run it two ways: as a campaign, a named send to an audience that you schedule, or as a send_whatsapp step in a journey, which sends to each contact as they reach it. Both reach contacts outside a 24-hour service window, so both send a Meta-approved template and nothing else. Free-form text, media, and flow messages stay one-to-one sends; a template's flow button still opens a flow.

Before you start

  • A connected business account with a registered number.
  • A WhatsApp template that Meta has approved on that number's business account.
  • Contacts with a phone number. For a marketing-category template, each contact also needs a recorded WhatsApp opt-in; utility templates need none.

Create a WhatsApp campaign

A WhatsApp campaign has channel: "whatsapp" and a whatsapp content object that names the template. The audience works as it does for email campaigns: topicId names the topic, and includeTags, groupIds, or contactIds select its contacts, with excludeTags removing matches. Eligibility uses each contact's WhatsApp subscription to the topic.

curl -X POST https://api.samva.dev/v1/campaigns \
  -H "X-API-Key: samva_sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Back in stock",
    "channel": "whatsapp",
    "content": {
      "channel": "whatsapp",
      "whatsapp": {
        "template": "restock-alert",
        "templateData": { "product": "Linen shirt" },
        "bindings": {
          "firstName": { "contact": "firstName" },
          "plan": { "customField": "plan" }
        }
      }
    },
    "audience": { "topicId": "unsub_1234567890abcdef", "includeTags": ["restock-waitlist"] }
  }'

The whatsapp object takes:

FieldMeaning
templateThe template's handle or id.
templateDataParameter values every recipient receives.
bindingsParameter values read from each recipient's contact.
businessAccountIdThe business account whose number sends. Omitted, the default number sends.

Fill parameters per recipient

A binding maps a template parameter to a contact field. Use { "contact": "<field>" } for a field on the contact, where the field is one of id, name, displayName, externalId, phone, email, firstName, lastName, company, or jobTitle. Use { "customField": "<key>" } for one of the contact's custom fields. Every required parameter of the template must be a literal in templateData or a binding, and a parameter cannot be both.

A recipient whose contact has no value for a bound parameter is not sent an empty value. Their message fails with a reason naming the parameter and the field.

Open a flow from a template button

A template with a flow button sends from a campaign or a journey step like any other. Each contact's button carries the id of the message sent to them as its token, so the customer's answer and every request to the flow's endpoint arrive with flowToken set to that message's id. A campaign run's recipients each list their messageId beside the contactId.

Schedule a run

Creating a campaign sends nothing. Schedule a run to start it, immediately or at a future time:

curl -X POST https://api.samva.dev/v1/campaigns/<campaign-id>/runs \
  -H "X-API-Key: samva_sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: restock-alert-v1" \
  -d '{
    "scheduledFor": "2026-08-01T09:00:00Z"
  }'

Scheduling checks the template against Meta's approval on the sending number's business account. It fails with a validation error, and sends nothing, when:

  • the template has no Meta-approved registration on that business account;
  • a required parameter is neither a literal nor a binding;
  • templateData or bindings name a parameter the template does not declare;
  • a parameter appears in both templateData and bindings.

Pausing, resuming, cancelling, and idempotency work as they do for email campaigns.

Who is skipped

When the run starts, Samva resolves the audience and skips each contact who cannot receive the template. Skipped contacts are counted, not failed, and never reach Meta.

skippedReasonThe contact
inactiveIs not an active contact.
no_identityHas no phone number.
unsubscribedIs not eligible for the run's topic on WhatsApp.
opted_outHas WhatsApp consent recorded as opted out.
suppressedIs on the WhatsApp suppression list.
unreachableHas a number that Meta reported is not on WhatsApp.
consent_requiredHas no recorded opt-in, and the template is in the marketing category.

The run reports skippedCount and skippedReasons, the count per reason, largest first. Each recipient carries its own skippedReason.

A run's skipped counts
{
  "totalRecipients": 1200,
  "dispatchedCount": 1043,
  "failedCount": 2,
  "skippedCount": 155,
  "skippedReasons": [
    { "reason": "consent_required", "count": 97 },
    { "reason": "no_identity", "count": 41 },
    { "reason": "unreachable", "count": 17 }
  ]
}

To broadcast to marketing recipients, record their opt-in first with PUT /v1/contacts/{id}/identities/{identityId}/consent/whatsapp, then schedule the run. Consent and suppression are also checked when each message is accepted and again just before it goes to Meta, so a withdrawal during a long run still stops the send.

Send from a journey

A send_whatsapp step takes the same configuration as a campaign's whatsapp content. This journey sends a template when a contact joins a group, waits a day, then sends a second one:

journey.json
{
  "name": "Order follow-up",
  "definition": {
    "trigger": { "type": "contact.joined_group", "groupId": "cgrp_7q2xk9mvt4znw8rh" },
    "rootNodeId": "welcome",
    "nodes": {
      "welcome": {
        "type": "send_whatsapp",
        "config": {
          "template": "order-welcome",
          "bindings": { "firstName": { "contact": "firstName" } }
        },
        "next": [{ "nodeId": "pause" }]
      },
      "pause": { "type": "wait", "config": { "durationSeconds": 86400 }, "next": [{ "nodeId": "check-in" }] },
      "check-in": {
        "type": "send_whatsapp",
        "config": { "template": "order-check-in", "templateData": { "supportHours": "9 to 5" } },
        "next": [{ "nodeId": "done" }]
      },
      "done": { "type": "exit", "config": {}, "next": [] }
    }
  }
}

Create, publish, and pause it through the journeys API like any other journey.

  • Publishing checks every send_whatsapp step as scheduling checks a campaign: the template is approved on the sending number's business account and every required parameter is filled. A failing step is named in the error.
  • Each send reads the contact as it is when the step runs: their current phone number, whether the number is reachable on WhatsApp, and the values behind the bindings. A contact who has since lost their number or a bound value fails that step.
  • Consent and suppression are checked when the message is accepted and again before it goes to Meta.
  • A run stays on the journey version it entered. The template is read when the step sends, so it sends what Meta has approved at that moment.

Pace to the messaging limit

Meta caps how many different customers your business portfolio can start template conversations with in a rolling 24 hours (its messagingLimit, see Business account). Every number in the portfolio shares the cap, and Samva counts them all. Samva never sends past it, and a run that exceeds it does not fail. A campaign run sends to as many recipients as the number's window has room for and holds the rest. A recipient the number already reached in the last 24 hours, or whose customer service window is open, needs no room and is sent even while the window is full.

While a run is held:

  • status is paused and holdReason is whatsapp_messaging_limit.
  • pendingCount is the number of recipients waiting. They are not failed or skipped.
  • holdUntil is the latest the run waits. It resumes sooner when Meta raises the portfolio's limit, and estimatedCompletionAt is when it should finish at the limit's pace.

Nobody needs to act. The run continues as the window frees room, or as soon as Meta raises the limit, and settles like any other run. If a raise is still not enough, the run holds again.

Scheduling never refuses a large audience. When the audience is larger than the number's limit has room for, the scheduled run carries a capacityWarning that says it will take longer than one window. The warning is advisory.

You can still control a held run:

  • Pause it to keep it paused. It stays paused after the window frees, until you resume it.
  • Resume it to ask the window again. A resumed run sends what the window has room for and holds the rest again.
  • Cancel it to stop it. Recipients still waiting are not sent.
A run held by the messaging limit
{
  "status": "paused",
  "totalRecipients": 4000,
  "dispatchedCount": 1000,
  "pendingCount": 3000,
  "holdReason": "whatsapp_messaging_limit",
  "holdUntil": "2026-08-02T09:00:00.000Z",
  "estimatedCompletionAt": "2026-08-04T09:00:00.000Z",
  "capacityWarning": null
}

A journey step that the limit refuses waits at its node. The run reports holdReason whatsapp_messaging_limit and holdUntil, and the step sends when the window frees or Meta raises the limit, without failing the run or the contact.

In the CLI, samva campaigns runs get shows the hold, the waiting count, and the resume time, and samva whatsapp broadcasts run prints the warning after scheduling. The hosted MCP tools campaigns_get_run and journeys_get_run return the same fields.

Read template outcomes

GET /v1/whatsapp/templates/stats reports what each template did, split by what sent it: a campaign, a journey step, or a direct send.

curl "https://api.samva.dev/v1/whatsapp/templates/stats?campaignId=<campaign-id>" \
  -H "X-API-Key: samva_sk_live_your_api_key"

Each item has the template, its source, and sent, delivered, read, and failed counts, plus estimatedProviderCosts: Samva's estimate of Meta's charge, one amount per billing currency. It is an estimate; Meta's invoice is the final amount, as What a message costs explains. Narrow the report with startDate, endDate, templateId, campaignId, or journeyId. The template stats also appear on the WhatsApp page in the dashboard.

Limits and cost

  • Credits. Each WhatsApp template message draws sending credits from your plan, like every other send, whether a campaign, a journey step, or a direct send sends it. Meta's fee is separate and billed to your business account.
  • Messaging limit. A run paces itself to your number's messaging limit; see Pace to the messaging limit.
  • Audience size. A run covers at most 25,000 resolved recipients, as for email campaigns.
  • Templates only. A broadcast cannot carry free-form text, media, or a flow message; a template's flow button is how it opens a flow.

In the dashboard

Creating a campaign offers WhatsApp as a channel: choose the sending number and the template, then map each template parameter to a contact field or a fixed value. The journey editor has a WhatsApp step with the same choices. The campaign run page shows the skipped counts by reason.

Related documentation

On this page