Documentation

Broadcast API Guide & Reference

Everything you need to integrate your own system (online store, CRM, internal tooling) with our WhatsApp broadcast platform — from core concepts to the full detail of every endpoint.

Documentation

Quick Start

Three steps to start sending broadcasts through your own integration.

  1. Connect your WhatsApp Business number. If you haven't already, connect it from the Numbers & WABA page in the dashboard — this is the source every message you send goes out from.
  2. Create an API key. Open the API Key page in the dashboard and click "Create New Key". The key (prefixed fvb_...) works across every WABA number your account owns — it's only shown once when created, so store it somewhere safe.
  3. Make your first call. Call GET /api/broadcast/numbers to confirm the key works and get your number's connection_id. Or try it interactively from the dashboard's API Playground before writing any code.
Documentation

Core Concepts

Templates vs. Free-form Text

There are two ways to send a WhatsApp Business message, and this split isn't our choice — it's Meta's rule:

  • Templates — a fixed-format message pre-approved by Meta. This is the only way to start a new conversation with a number that hasn't messaged you before. Used by POST /send and POST /send-to-group.
  • Free-form text — an unrestricted reply, but only sendable inside the 24-hour session window since the customer last messaged your number. Used by POST /send-text.

Why does every call need a connection_id?

One API key works across every WhatsApp number your account owns — not just one. Because of that, every send call must state which number it's sending from via connection_id. Get the full list from GET /api/broadcast/numbers.

Who Prepares What?

Two things need to exist before you can send, and they're prepared in different places — this trips up new integrators more than anything else:

  • The message content (Template) — must be created and Meta-approved through our dashboard first, not via the API. That's not a design choice on our part — template approval is a manual review by Meta itself, no platform can make that instant through an API call. The good news: this is a one-time step per message type, not per send — once promo_september is approved, you can call it through the API as many times as you want.
  • The recipient list and its data — this can come entirely from your own system, no need to touch our dashboard at all: pass numbers + data directly to POST /send. Only if you actually want to reuse contacts already stored and grouped in our dashboard do you reach for POST /send-to-group instead — that's optional, not a requirement.

How Template Variables Work ({{1}}, {{2}}, etc.)

Template variables are positional, not named — Meta has no concept of a "variable name", just a running number. Here's how it works:

  1. Call GET /api/broadcast/templates/{name} and read the body text to understand what each number means (e.g. "Hi {{1}}, here's your {{2}}% discount!" — {{1}} is the name, {{2}} is the discount).
  2. Fill the variables array in that exact order: ["Budi", "20"].

Important for personalized sends (different content per person): one POST /send call uses one variables array for every number in that request — not per-recipient. If each recipient needs different content, call POST /send multiple times, once per recipient, each with its own numbers (a single number) and variables — not one call for everyone at once.

Documentation

Common Scenarios

The three situations most integrations reach for our API — and which endpoint fits each one.

1

Promo to New Numbers

Your system (e.g. an online store) wants to send an offer to hundreds of numbers from its own database — those numbers may have never messaged your WABA before, so it must use a Template that's already Meta-approved.

POST /send
2

Broadcast to a Customer Group

Contacts are already stored and grouped in our dashboard (e.g. a "VIP Customers" group) with a Template already mapped — your system just needs to pass the group_id, no need to resend the whole number list.

POST /send-to-group
3

Auto-Reply to Customers

A customer just messaged your number (still inside the 24-hour window) — your system hears about it through the webhook message.received, then your own code decides what to reply (e.g. order confirmation) and calls this endpoint to send it. This isn't an automated auto-reply feature from us — the "what to reply" logic is entirely yours. (Our own reply-automation features, Keyword Auto-Reply & AI Agent, live in the dashboard but aren't available through the API yet.)

POST /send-text
Documentation

Authentication

Every request is authenticated via the Authorization header using the Bearer scheme. Paste your API key (prefixed fvb_...) as-is, no extra quoting:

Required header on every request

Authorization: Bearer fvb_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

If the header is missing, the key is wrong, revoked, or expired — every endpoint returns 401 with a clear error message, never a silent failure.

Documentation

Dashboard vs. API — Which One Should I Use?

Both go through the exact same delivery pipeline behind the scenes — the only difference is who triggers it, and a few dashboard features aren't exposed via the API yet.

  • Dashboard is the right fit when you (or your team) manually pick contacts/groups and send a broadcast straight from the browser — no code required.
  • API is the right fit when sending needs to be triggered automatically by another system — e.g. your online store automatically sending a WhatsApp notification when a new order comes in, or your CRM sending its own scheduled reminders.
Feature Dashboard API
Send a Template to manual numbers
Send a Template to a contact group
Send a Carousel Template
Free-text reply to 1 number
Free-text broadcast to a whole groupnot on the API yet
CTA buttons (link / quick-reply)not on the API yet
Media attachment on free-textnot on the API yet
Schedule a send for laternot on the API yet
Delivery history & logs

Need CTA, media, or scheduled sends over the API? Those are dashboard-exclusive for now — let us know if you need them opened up over the API too.

Documentation

Webhook

The reverse of everything above — instead of you calling us, we automatically push data to your system the moment something happens, instead of you having to keep calling an endpoint just to check "is there anything new yet?".

What Can It Do?

Two events can currently be pushed to your system:

A New Reply Arrives — message.received

A customer just messaged your number. A fit for building your own customer-service bot — when this event lands, your system can immediately call POST /send-text to auto-reply (still inside that same 24-hour window), or simply log it to your own CRM/helpdesk without staff ever opening our dashboard.

POST /send-text

A Delivery Status Changes — message.status_update

A message you sent changes status (sent → delivered → read, or failed). The combo: call GET /broadcasts/{id} once to get the initial state right after sending, then stop polling that endpoint entirely — every subsequent status change gets pushed here instead.

GET /broadcasts/{id}

The webhook is an optional add-on, not a replacement — if plain API polling already works fine for your system, that's completely fine, you don't have to set up a webhook at all.

How to Set It Up — 1. Register your URL

Open the API Key page in the dashboard and paste in one URL on your own server that can accept POST (must be https://, and can't point at an internal/private address). We immediately send one verification request to it — your endpoint must echo the random code back exactly for the status to flip to "Active". This proves you actually control that URL, the same way we verify our own webhook back to Meta.

Verification request body we send

{
  "event": "webhook.verification",
  "sent_at": "2026-08-28T10:00:00+07:00",
  "data": { "challenge": "aB3xZ..." }
}

Respond with 200 — either plain text containing exactly the challenge value, or JSON {"challenge": "aB3xZ..."}. Either form is accepted.

How to Set It Up — 2. Verify the signature

Every request (verification or a real event) carries 2 extra headers — use these to confirm the request genuinely came from us, not someone else hitting your URL:

Headers on every webhook request

X-Webhook-Event: message.received
X-Webhook-Signature: sha256=1a2b3c...

The signing secret lives on the same API Key page (the "Lihat" button on the Webhook card) — never put it in client/browser-side code, only on your own server. To verify: recompute the HMAC-SHA256 of the raw request body (exactly as received, before parsing it into an object) using that secret, then compare it against the value after sha256= in the header — using a timing-safe comparison (hash_equals), never plain ==.

Example receiving endpoint (PHP) — drop-in ready

$rawBody = file_get_contents('php://input'); // must be RAW, not already json_decode()'d
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret = 'whsec_...'; // from the Webhook card on the API Key page

$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);

if (! hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}

// Verified — safe to process now.
$payload = json_decode($rawBody, true);
$event = $payload['event']; // 'message.received' / 'message.status_update' / 'webhook.verification'
$data = $payload['data'];

Technical Details — Each Event's Shape

message.received — a new reply landed in Inbox:

Example event body

{
  "event": "message.received",
  "sent_at": "2026-08-28T10:05:00+07:00",
  "data": {
    "message_id": 1042,
    "wamid": "wamid.HBg...",
    "connection_id": 4,
    "from": "6281234567890",
    "contact_name": "Budi Santoso",
    "type": "text",
    "body": "Hi, is this in stock?",
    "timestamp": "2026-08-28T10:05:00+07:00"
  }
}

message.status_update — a message you sent changed status (same fields as one row of recipients from GET /broadcasts/{id}):

Example event body

{
  "event": "message.status_update",
  "sent_at": "2026-08-28T10:06:00+07:00",
  "data": {
    "broadcast_id": 514,
    "phone": "6281234567890",
    "message_id": "wamid.HBg...",
    "status": "delivered",
    "error": null,
    "sent_at": "2026-08-28T10:05:50+07:00",
    "delivered_at": "2026-08-28T10:06:00+07:00",
    "read_at": null
  }
}

status progresses sent → delivered → read, or goes straight to failed (with a reason in error) — the exact same progression GET /broadcasts/{id} uses, so any parsing code you've already written for that endpoint can be reused here.

Delivery rules

  • Respond with 200 quickly (within 5 seconds) — a slow or unresponsive server counts as a failed delivery.
  • A failed delivery is automatically retried up to 3 times (30 seconds, then 2 minutes, then 10 minutes apart) before giving up.
  • Every delivery attempt (success or failure, with its response code) is visible in the Webhook card on the API Key page.
  • One client can have only 1 active webhook URL for now — shared across every event and every WhatsApp number you have.
API Reference

Available Endpoints

Every endpoint below requires an Authorization: Bearer fvb_... header — get your API key from the API Key page in the dashboard. Without a valid key, every endpoint here returns 401.

You may see 2 different error shapes: missing/wrong-type fields return Laravel's default 422 shape ({"message": "...", "errors": {"field": ["..."]}}), while errors from our own business logic (template not found, 24-hour window closed, etc.) return the simpler {"error": "..."} shape — every error example below each endpoint uses the latter.

Postman Collection

Every endpoint below in one ready-to-import file — complete with example requests, responses, and a description of every parameter. Import it into Postman, or hand it to an AI/another developer for instant context.

Download Collection
Resource

Numbers & Connections

One API key works across every WhatsApp number your account owns. This resource tells you which numbers are available and each one's connection_id — a value every send operation below requires.

Operations 1

GET /api/broadcast/numbers

List Numbers

Which WhatsApp numbers this API key can send from.

Example Request


Example Success Response 200

{
    "numbers": [
        {
            "connection_id": 4,
            "phone_number": "6281234567890",
            "label": "Nomor Utama",
            "waba_name": "Toko Maju Jaya"
        }
    ]
}
Resource

Templates

A Template is a fixed-format, Meta-approved message — the only way to start a new conversation with a number that hasn't messaged you before (see Core Concepts). This resource only lists templates you've flagged "Usable via API" in the dashboard.

Operations 2

GET /api/broadcast/templates

List Templates

Templates that are Meta-approved and toggled "Usable via API". Variables are exposed by position ({{1}}, {{2}}, ...) — how you fill them in is entirely up to your own system.

Example Request


Example Success Response 200

{
    "templates": [
        {
            "name": "info_promo",
            "category": "MARKETING",
            "language": "id",
            "body": "Halo {{1}}, ada promo spesial buat kamu: {{2}}!",
            "variable_count": 2,
            "waba_name": "Toko Maju Jaya",
            "note": null
        },
        {
            "name": "otp_login",
            "category": "AUTHENTICATION",
            "language": "id",
            "body": "",
            "variable_count": 0,
            "waba_name": "Toko Maju Jaya",
            "note": "Kategori Authentication: teks pesan dibuat otomatis oleh Meta, tidak ada variabel bebas. Kirim lewat POST /send-otp dengan parameter code."
        }
    ]
}
GET /api/broadcast/templates/{name}

Single Template Detail

The full component structure of one template (HEADER/BODY/FOOTER/BUTTONS) — for when you only need one template, not the whole list.

Field Type Required Description
name string required Path parameter — the template name from GET /templates.

Example Request


Example Success Response 200

{
    "name": "info_promo",
    "category": "MARKETING",
    "language": "id",
    "variable_count": 2,
    "waba_name": "Toko Maju Jaya",
    "components": [
        {
            "type": "HEADER",
            "format": "IMAGE"
        },
        {
            "type": "BODY",
            "text": "Halo {{1}}, ada promo spesial buat kamu: {{2}}!"
        },
        {
            "type": "FOOTER",
            "text": "Balas STOP untuk berhenti menerima pesan promosi."
        }
    ],
    "note": null
}

Example Error Response 404

{
    "error": "Template 'info_promo' tidak ditemukan atau belum bisa dipakai lewat API."
}
Resource

Messages — Using a Template

Send a broadcast using a Meta-approved Template — the only way to start a new conversation with a number that hasn't messaged you before. The two operations below differ only in where the recipients come from: raw numbers you supply, or a contact group already stored in the dashboard.

Operations 4

GET /api/broadcast/groups

List Contact Groups

Every contact group this client owns, account-wide (not tied to one WABA) — this is what tells you the group_id value to use in the "Send to Contact Group" operation below.

Example Request


Example Success Response 200

{
    "groups": [
        {
            "group_id": 12,
            "name": "Pelanggan VIP",
            "active_contacts_count": 48,
            "total_contacts_count": 50
        }
    ]
}
GET /api/broadcast/groups/{id}/contacts

List Contacts in a Group

The contents of one contact group — each contact's name, phone, custom fields, and opt-out status. Useful for previewing a group before calling "Send to Contact Group" below.

Field Type Required Description
id integer required Path parameter — the group_id from GET /groups.

Example Request


Example Success Response 200

{
    "group_id": 12,
    "name": "Pelanggan VIP",
    "contacts": [
        {
            "contact_id": 30,
            "name": "Andi Saputra",
            "phone": "6281234567890",
            "custom_fields": {
                "kota": "Bandung"
            },
            "opted_out": false
        }
    ]
}

Example Error Response 404

{
    "error": "Grup kontak tidak ditemukan."
}
POST /api/broadcast/send

Send to Numbers Directly

Send a template broadcast straight to the phone numbers in your request — those numbers are never stored as our own contacts, ideal when your own system already manages customer data.

If template_name is a Media Card Carousel template, the cards parameter is REQUIRED (its count must exactly match the template's card count, same order as in the dashboard). For a regular template, omit cards entirely.

Field Type Required Description
connection_id integer required From GET /numbers.
template_name string required Must be approved on this WABA.
numbers array<string> required Max 1000 numbers per request.
variables array optional Values for {{1}}, {{2}}, ... in the main body — same values used for every number in this request.
cards array optional REQUIRED if the template is a Carousel — one object per card, same order as in the dashboard: <code>header_media_url</code> (required, image/video URL), <code>header_type</code> (<code>image</code>/<code>video</code>, default <code>image</code>), <code>body_params</code> (that card's own <code>{{n}}</code> values), <code>buttons</code> (<code>[{type, text, payload}]</code>).
dry_run boolean optional If <code>true</code>, only validates and returns the message text that would be sent (from our locally-stored template) for each number, including a per-card carousel preview (<code>cards_preview</code>) when applicable — no Meta call, no broadcast created or queued.

Example Request


Example Success Response 201

{
    "broadcast_id": 512,
    "status": "queued",
    "template": "info_promo",
    "total_recipients": 2
}

Example Error Response 422

{
    "error": "Template 'info_promo' tidak ditemukan atau belum approved buat nomor ini."
}
POST /api/broadcast/send-to-group

Send to Contact Group

Send to one of the contact groups already in our dashboard — that group must already have exactly one template mapped (via the Template page), unless you pass template_name to pick one explicitly.

Same as Send to Numbers Directly — if the mapped template_name is a Media Card Carousel template, the cards parameter is REQUIRED (same count and order as the dashboard), sent identically to every contact in this group. For a regular template, omit cards.

Field Type Required Description
connection_id integer required From GET /numbers.
group_id integer required Contact group ID.
template_name string optional Required if the group has more than 1 mapped template.
variables array optional Only for variables not already auto-filled from contact data.
cards array optional REQUIRED if the template is a Carousel — same shape as <a href="#ep-send">Send to Numbers Directly</a>: one object per card (<code>header_media_url</code>, <code>header_type</code>, <code>body_params</code>, <code>buttons</code>), sent identically to every contact in the group.
dry_run boolean optional If <code>true</code>, only validates and returns the message text that would be sent for each contact in the group (including variables auto-filled from contact data), including a per-card carousel preview (<code>cards_preview</code>) when applicable — no Meta call, no broadcast created or queued.

Example Request


Example Success Response 201

{
    "broadcast_id": 513,
    "status": "queued",
    "template": "info_promo",
    "group": "Pelanggan VIP",
    "total_recipients": 48
}

Example Error Response 422

{
    "error": "Grup \"Pelanggan VIP\" punya lebih dari 1 template ter-mapping \u2014 sertakan 'template_name' buat pilih salah satu.",
    "available_templates": [
        "info_promo",
        "reminder_bayar"
    ]
}
Resource

Messages — Free Text

A free-form reply with no Template — but Meta only allows it inside the 24-hour window since the customer last messaged your number. Good for customer-service auto-replies, not for promotional broadcasts to new numbers (use the Messages — Using a Template resource for that).

Operations 1

POST /api/broadcast/send-text

Send Free Text

Send a free-form text reply (no template) — only works inside Meta's 24-hour session window (the recipient must have messaged this WABA recently). Sent synchronously — the real result comes back immediately, not queued.

Field Type Required Description
connection_id integer required From GET /numbers.
phone string required Recipient number.
message string required Max 4096 characters.

Example Request


Example Success Response 201

{
    "status": "sent",
    "message_id": "wamid.HBg...",
    "broadcast_id": 514
}

Example Error Response 422

{
    "error": "(#131047) Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
    "error_code": 131047,
    "broadcast_id": 514
}
Resource

Messages — OTP/Verification

Send an OTP/verification code (login, password change, etc.) using the dedicated Authentication Template category — unlike a regular Template, its wording is generated by Meta itself (you don't write it), you only supply the code. The code itself is generated and verified by your own system — this resource only delivers it over WhatsApp. Sent synchronously — the real result comes back immediately, not queued — a fit for someone actively waiting for a code on their phone.

Operations 1

POST /api/broadcast/send-otp

Send OTP Code

template_name must already be approved, category Authentication, and toggled "Usable via API" (see List Templates — templates in this category show up there too, flagged via the note field).

Field Type Required Description
connection_id integer required From GET /numbers.
template_name string required An approved Authentication-category template.
phone string required Recipient number.
code string required The OTP code itself — max 20 characters. Generated and stored by your own system, not by us.

Example Request


Example Success Response 201

{
    "status": "sent",
    "message_id": "wamid.HBg...",
    "broadcast_id": 515
}

Example Error Response 422

{
    "error": "Template 'otp_login' tidak ditemukan, belum approved, bukan kategori Authentication, atau belum dicentang \"Bisa dipakai lewat API\"."
}
Resource

Broadcast Status

Every send operation above (/send, /send-to-group, /send-text, /send-otp) returns a broadcast_id as soon as the request is accepted — this resource lets your system check the outcome later, without opening the dashboard.

Operations 1

GET /api/broadcast/broadcasts/{id}

Check Broadcast Status

Overall totals (sent_count/delivered_count/read_count/failed_count) plus a per-recipient breakdown — per-recipient status progresses pending → sent → delivered → read as Meta sends us the callback for each step, or straight to failed if it didn't make it.

Field Type Required Description
id integer required Path parameter — the broadcast_id from a send operation's response.

Example Request


Example Success Response 200

{
    "broadcast_id": 514,
    "status": "completed",
    "template": "info_promo",
    "group": null,
    "total_count": 2,
    "sent_count": 0,
    "delivered_count": 1,
    "read_count": 1,
    "failed_count": 1,
    "created_at": "2026-08-18T02:10:00+00:00",
    "recipients": [
        {
            "phone": "6281234567890",
            "status": "read",
            "message_id": "wamid.HBg...",
            "error": null,
            "sent_at": "2026-08-18T02:10:05+00:00",
            "delivered_at": "2026-08-18T02:10:08+00:00",
            "read_at": "2026-08-18T02:11:40+00:00"
        },
        {
            "phone": "6281298765432",
            "status": "failed",
            "message_id": null,
            "error": "Nomor Tidak Terdaftar di WhatsApp \u2014 (#131026) ...",
            "sent_at": null,
            "delivered_at": null,
            "read_at": null
        }
    ]
}

Example Error Response 404

{
    "error": "Broadcast tidak ditemukan."
}

Want to try it live?

The dashboard's API Playground lets you send a real request to every endpoint above, straight from your browser, before writing a single line of integration code.

Sign in to Try