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:
| Field | Meaning |
|---|---|
template | The template's handle or id. |
templateData | Parameter values every recipient receives. |
bindings | Parameter values read from each recipient's contact. |
businessAccountId | The 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;
templateDataorbindingsname a parameter the template does not declare;- a parameter appears in both
templateDataandbindings.
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.
skippedReason | The contact |
|---|---|
inactive | Is not an active contact. |
no_identity | Has no phone number. |
unsubscribed | Is not eligible for the run's topic on WhatsApp. |
opted_out | Has WhatsApp consent recorded as opted out. |
suppressed | Is on the WhatsApp suppression list. |
unreachable | Has a number that Meta reported is not on WhatsApp. |
consent_required | Has 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.
{
"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:
{
"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_whatsappstep 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:
statusispausedandholdReasoniswhatsapp_messaging_limit.pendingCountis the number of recipients waiting. They are not failed or skipped.holdUntilis the latest the run waits. It resumes sooner when Meta raises the portfolio's limit, andestimatedCompletionAtis 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.
{
"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.