# Send a WhatsApp campaign through the API

URL: https://wa.genuka.com/en/docs/guides/campaigns-api
Language: English

> Send a WhatsApp campaign through the API: marketing template, recipients, launch, plan quotas, Meta's marketing limits and delivery tracking.

To send a WhatsApp campaign through the API with Genuka WA, you get a `MARKETING` template
approved by Meta, create the campaign with `POST /api/v1/campaigns` and its recipient list, then
launch it with `POST /api/v1/campaigns/{id}/launch`. Every recipient is tracked through to read,
and Meta bills delivered messages directly to your own WhatsApp Business account.

*Last updated October 8, 2026*

## What you need before sending a campaign

* **A connected number** and its `connectionId` — see [Connecting a number](https://wa.genuka.com/en/docs/onboarding).
* **An API key**, used server-side only — see [Authentication](https://wa.genuka.com/en/docs/authentication).
* **A payment method on your Meta account.** Genuka is a Meta Tech Provider, not a BSP: Meta bills
  your WhatsApp Business Account (WABA) directly, and a client onboarded by a Tech Provider must
  add its own payment method
  ([Meta, Partners](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)).
  Without one, the template gets approved but every send comes back with error `131042`.
* **Recipients who agreed to hear from you.** That is a WhatsApp rule, not a best practice — the
  opt-in section further down covers it.

## How do I create the marketing template?

Outside the 24-hour customer service window, only an approved template is delivered. A campaign
always uses one. Submit it with the `MARKETING` category:

```bash title="POST /api/v1/templates"
curl -X POST https://wa.genuka.com/api/v1/templates \
  -H "Authorization: Bearer $GENUKA_WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "con_1",
    "name": "october_sale",
    "language": "en_US",
    "category": "MARKETING",
    "components": [
      { "type": "BODY",
        "text": "Hi {{1}}, 20% off the whole shop until Sunday.",
        "example": { "body_text": [["Awa"]] } },
      { "type": "BUTTONS", "buttons": [
        { "type": "URL", "text": "See the offers", "url": "https://example.com/sale" }
      ] }
    ]
  }'
```

The response is a `201` carrying the template's `id` and its status. Meta reviews it —
review can take up to 24 hours
([Meta, Template fundamentals](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview)).
The verdict reaches your [webhooks](https://wa.genuka.com/en/docs/webhooks) (`template.status_changed`) and
`GET /api/v1/templates/{id}`. A campaign only goes out with an `approved` template.

Every body variable (`{{1}}`, `{{2}}`…) needs an example, or the template is rejected. The
[API reference](https://wa.genuka.com/en/docs/api#templates) covers media headers, named parameters and buttons.

## How do I create and launch the campaign?

1. ### Create the campaign

   `POST /api/v1/campaigns` takes four fields, all required: `connectionId`, `templateId`, `name`
   and `recipients`. Each recipient carries its number in international format (`+` and country
   code) and its own `variables`. The campaign is created as `draft`: nothing is sent yet.

2. ### Launch it

   `POST /api/v1/campaigns/{id}/launch` sends every recipient still `pending` and answers with the
   count: `sent`, `failed`, `skipped`. `sent` means Meta accepted the message, not that it arrived —
   delivery and read come afterwards, by webhook.

3. ### Track the statuses

   Read the counters on `GET /api/v1/campaigns/{id}` and the per-recipient detail on
   `GET /api/v1/campaigns/{id}/recipients`. The tracking section further down covers both.

**curl**

```bash
# 1. Create the campaign (draft)
curl -X POST https://wa.genuka.com/api/v1/campaigns \
  -H "Authorization: Bearer $GENUKA_WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "con_1",
    "templateId": "tpl_123",
    "name": "October sale",
    "recipients": [
      { "to": "+237690000001", "variables": ["Awa"] },
      { "to": "+237690000002", "variables": ["Paul"] }
    ]
  }'
# { "data": { "id": "cmp_9", "name": "October sale", "status": "draft", "_count": { "recipients": 2 } } }

# 2. Launch it
curl -X POST https://wa.genuka.com/api/v1/campaigns/cmp_9/launch \
  -H "Authorization: Bearer $GENUKA_WA_API_KEY"
# { "data": { "sent": 2, "failed": 0, "skipped": 0 } }
```

**Node.js**

```ts
const API = "https://wa.genuka.com/api/v1";

async function post<T>(path: string, body?: unknown): Promise<T> {
  const response = await fetch(`${API}${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  // A 524 from the CDN (launch running too long, see the limitations below) has no JSON body.
  const json = await response.json().catch(() => ({}));
  if (!response.ok) {
    throw new Error(`${response.status} ${json.error ?? "no_json_body"}: ${json.message ?? ""}`);
  }
  return json.data as T;
}

const campaign = await post<{ id: string }>("/campaigns", {
  connectionId: "con_1",
  templateId: "tpl_123",
  name: "October sale",
  recipients: [
    { to: "+237690000001", variables: ["Awa"] },
    { to: "+237690000002", variables: ["Paul"] },
  ],
});

const result = await post<{ sent: number; failed: number; skipped: number }>(
  `/campaigns/${campaign.id}/launch`,
);
console.log(result); // { sent: 2, failed: 0, skipped: 0 }
```

**Python**

```python
import os
import requests

API = "https://wa.genuka.com/api/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['GENUKA_WA_API_KEY']}",
    "User-Agent": "acme-crm/1.0 (+https://example.com)",
}

created = requests.post(f"{API}/campaigns", headers=HEADERS, timeout=30, json={
    "connectionId": "con_1",
    "templateId": "tpl_123",
    "name": "October sale",
    "recipients": [
        {"to": "+237690000001", "variables": ["Awa"]},
        {"to": "+237690000002", "variables": ["Paul"]},
    ],
})
created.raise_for_status()
campaign_id = created.json()["data"]["id"]

# Past about 2 minutes, the CDN answers 524 while sending carries on server-side:
# do not launch again, follow the campaign's status (see the limitations below).
launched = requests.post(f"{API}/campaigns/{campaign_id}/launch", headers=HEADERS, timeout=150)
launched.raise_for_status()
print(launched.json()["data"])  # {'sent': 2, 'failed': 0, 'skipped': 0}
```

**PHP**

```php
<?php
function genuka_post(string $path, ?array $body = null): array
{
    $ch = curl_init("https://wa.genuka.com/api/v1" . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 150,
        CURLOPT_USERAGENT => "acme-crm/1.0 (+https://example.com)",
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer " . getenv("GENUKA_WA_API_KEY"),
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => $body === null ? "" : json_encode($body),
    ]);
    $json = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status >= 400) {
        // A 524 from the CDN (launch running too long, see the limitations below) has no JSON body.
        throw new RuntimeException("{$status} " . ($json['error'] ?? 'no_json_body'));
    }
    return $json["data"];
}

$campaign = genuka_post("/campaigns", [
    "connectionId" => "con_1",
    "templateId" => "tpl_123",
    "name" => "October sale",
    "recipients" => [
        ["to" => "+237690000001", "variables" => ["Awa"]],
        ["to" => "+237690000002", "variables" => ["Paul"]],
    ],
]);

print_r(genuka_post("/campaigns/{$campaign['id']}/launch")); // sent, failed, skipped
```

`variables` is a positional array for the body parameters. For a media header, a dynamic button
or named parameters, pass an object instead —
`{ "body": [...], "bodyNamed": { "first_name": "Awa" }, "header": {...}, "buttons": [...] }` —
the same shape as [sending a template](https://wa.genuka.com/en/docs/api#messages). A named-parameter template's values
go in `bodyNamed`: put in `body`, they would be sent as positional parameters.

### Why is the launch refused?

These checks run before the first send: a refused launch has sent nothing. The most common
refusals:

| Status | Code                    | Cause                                                                                                                                      |
| ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `402`  | `plan_limit_messages`   | The list does not fit in what is left of the period's quota. Every `pending` recipient counts, including those later excluded as opted out |
| `402`  | `subscription_past_due` | The trial or the paid period has lapsed                                                                                                    |
| `400`  | `send_template`         | The template is not `approved` (in review, rejected, paused) — the message says which                                                      |
| `409`  | `send_config`           | The number was disconnected from the API in the WhatsApp Business app ([coexistence](https://wa.genuka.com/en/docs/guides/coexistence)): reconnect it           |
| `409`  | `number_released`       | The number was released from your plan: reconnect it through Embedded Signup                                                               |
| `403`  | `account_deactivated`   | The client that owns the number is suspended                                                                                               |
| `404`  | `campaign_not_found`    | The `id` does not exist or is outside your key's scope                                                                                     |

On creation, a `templateId` and a `connectionId` belonging to two different clients answer
`400 template_connection_mismatch`, and a list without a single usable `to` answers
`400 no_valid_recipients`. Other codes are in the [error reference](https://wa.genuka.com/en/docs/api#errors).

## How many messages can a campaign send?

Genuka never charges for a message. Your plan does include a **quota of outbound messages per
paid number per billing period**, shared between API sends and campaign recipients. The period's
quota is: numbers paid for × the tier's per-number allowance. The counter starts again from zero
with each new period: every month on a monthly subscription, at renewal on an annual one.

| Tier       | Max numbers | Messages included per number per period |
| ---------- | ----------- | --------------------------------------- |
| Starter    | 5           | 500                                     |
| Growth     | 15          | 5,000                                   |
| Scale      | 100         | 30,000                                  |
| Enterprise | custom      | custom                                  |

Example: 3 numbers on Growth give 15,000 messages for the period. The 7-day free trial runs on the
Growth tier with one number, so 5,000 messages. Only messages Meta accepted are counted: a send
Meta refuses on the call consumes nothing, but a message accepted and then reported as `failed`
by webhook stays counted. Prices are on the [pricing page](https://wa.genuka.com/en#pricing) and in Markdown at
[/pricing.md](https://wa.genuka.com/pricing.md).

## What limits does Meta put on marketing campaigns?

The Genuka quota is not the only ceiling. Meta enforces four of its own, whatever your plan:

| Meta limit                   | What it says                                                                                                                                                                                                                                                                                                            | What you see                                                                                                                                                                                                        |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Messaging limit**          | Unique recipients you can reach outside a customer service window, per moving 24 hours, at business portfolio level: 250 for a new portfolio, then 2,000, 10,000, 100,000, unlimited ([docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits))                               | WhatsApp Manager, **Account tools > Messaging limits** (same page). The `messagingLimitTier` from `GET /api/v1/numbers?refresh=true` is only indicative: it comes from a field Meta has deprecated and can be empty |
| **Per-user marketing limit** | WhatsApp may not deliver a marketing template to someone who receives many and reads few. Wait at least 24 hours before resending ([docs](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits/))                                                    | Error `131049` on the status webhook, recipient `failed`                                                                                                                                                            |
| **US numbers**               | WhatsApp does not currently deliver marketing templates to US phone numbers (same page)                                                                                                                                                                                                                                 | Recipient fails                                                                                                                                                                                                     |
| **Throughput**               | 80 messages per second per number by default, 20 for a coexistence number ([throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput), [coexistence](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)) | The launch is paced to it                                                                                                                                                                                           |

The per-user marketing limit is not currently active for messages sent from or to the European
Economic Area, the United Kingdom, Japan and South Korea (same Meta page).

A campaign's `MARKETING` templates are first offered to Meta's
[Marketing Messages API](https://developers.facebook.com/documentation/business-messaging/whatsapp/marketing-messages/get-started);
if your account does not have access to it, the send falls back to the classic endpoint without
stopping the campaign.

### How much does a campaign cost on Meta's side?

Meta charges per delivered message, at the rate for the template's category and the recipient's
country calling code, on your business portfolio
([Meta, Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)).
Genuka adds no markup and never handles that payment.

## Do I need opt-in for a WhatsApp campaign?

Yes. The WhatsApp Business policy only lets you contact someone who gave you their number **and**
confirmed they want to receive your messages
([WhatsApp Business Messaging Policy](https://whatsappbusiness.com/policy/)). Meta adds that the
request must name your business and state clearly what the person is opting in to, through any
channel you like — website, SMS, paper form
([Meta, Get opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in)).
Quality that stays low for a sustained period gets the number rate-limited by Meta (same page).

Genuka enforces marketing opt-outs on campaigns for you:

* When a user stops marketing messages from within WhatsApp, Meta announces it by webhook and
  Genuka records it. A send refused with `131050` ("this recipient has chosen to stop receiving
  marketing messages") is recorded too.
* When a `MARKETING` campaign launches, those contacts are excluded **before** any send and marked
  `skipped`, with the reason. They consume neither quota nor Meta billing.
* `UTILITY` and `AUTHENTICATION` campaigns are not filtered: they are not marketing messages.

A keyword of your own ("reply STOP") is not interpreted automatically: replies reach you on
`message.received`, and removing those contacts from your lists is up to you.

## How do I track a campaign's results?

`GET /api/v1/campaigns/{id}` returns the campaign, its template, its number and a `stats` object
counting recipients by status:

```json title="GET /api/v1/campaigns/cmp_9"
{
  "data": {
    "id": "cmp_9",
    "name": "October sale",
    "status": "completed",
    "template": { "id": "tpl_123", "name": "october_sale", "language": "en_US", "status": "approved" },
    "_count": { "recipients": 1200 },
    "stats": { "read": 640, "delivered": 410, "sent": 95, "failed": 41, "skipped": 14 }
  }
}
```

Each recipient is counted once, under its most advanced status: a read message sits in `read`,
not in `delivered`. The delivery rate therefore reads `(delivered + read) / (total − skipped)`.

| Status      | Meaning                                           |
| ----------- | ------------------------------------------------- |
| `pending`   | Not sent yet                                      |
| `sent`      | Accepted by Meta                                  |
| `delivered` | Reached the phone                                 |
| `read`      | Read — only if the recipient has read receipts on |
| `failed`    | Refused, with Meta's reason in `errorMessage`     |
| `skipped`   | Excluded at launch: opted out of marketing        |

For failures, `GET /api/v1/campaigns/{id}/recipients?status=failed&limit=1000` lists each recipient
with `errorMessage`, `sentAt`, `deliveredAt`, `readAt` and `failedAt` (200 rows by default, 1,000
at most). In real time, subscribe an endpoint to `message.delivered`, `message.read`,
`message.failed` and `user_preference.stopped` — see [Webhooks](https://wa.genuka.com/en/docs/webhooks).

### How do I resend to recipients blocked by the marketing limit?

The per-user marketing limit shows up after the send: Meta accepts the message, then the status
webhook comes back `failed` with error `131049`
([Meta, Per-user limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits/)).
The recipient moves to `failed`, with Meta's text in `errorMessage` (Meta describes this error as
"This message was not delivered to maintain healthy ecosystem engagement"). Calling `launch` again
will not resend to them: it only picks up `pending` recipients.

To try those people again, wait at least 24 hours, list
`GET /api/v1/campaigns/{id}/recipients?status=failed`, keep the ones carrying that reason (or the
`131049` code received on `message.failed`) and create a new campaign with them. In the rarer case
where Meta refuses the send itself with `131049`, the recipient stays `pending`, and calling
`launch` again 24 hours later picks it up.

## Current limitations

* **The launch is synchronous and time-bound.** The call works through the list one recipient
  after another: each send waits for Meta's answer, then for its status to be stored, so the real
  rate stays well below the number's ceiling. Past about 2 minutes, the CDN in front of the API
  answers `524` — an error page, not JSON — while sending carries on server-side
  ([Cloudflare, error 524](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/)).
  Keep campaigns to a few hundred recipients and split a large audience into several campaigns.
* **After a `524`, follow the campaign instead of launching it again.** Poll
  `GET /api/v1/campaigns/{id}` until `status` is no longer `running`. Never call `launch` on a
  `running` campaign: nothing prevents it today, and two runs in parallel would send the message
  twice to the recipients the first run has not reached yet.
* **A run that goes on too long is cut off.** The server does not let a call run forever. If the
  campaign is still `running` 15 minutes after the call, the run was cut short: the recipients
  still `pending` were not sent, and calling `launch` again picks them up.
* **No scheduling.** There is no date field: call `launch` when you want it to go, from your own
  scheduled job.

## What if I want a UI rather than an API?

Genuka WA has no campaign console: `/api/v1/campaigns` is an integration primitive (template +
recipients + statuses). If you want to segment a customer base and launch campaigns without
writing code, [Genuka Core](https://genuka.com), Genuka's business management platform, does it:
it categorizes your customers, analyzes their buying behavior and launches targeted WhatsApp, SMS
and email campaigns.

## FAQ

### Can I send a campaign to contacts who never messaged me?

Yes, with an approved template and their opt-in. That is exactly what a template is for: it is the
only message delivered outside the 24-hour window. Meta's messaging limit (250 unique recipients
per 24 hours for a new portfolio) applies to those sends.

### How much does a WhatsApp campaign cost with Genuka WA?

Two separate lines. Meta bills each delivered template to your business portfolio, by category and
recipient country. Genuka charges a subscription per WhatsApp number, with a message allowance
included, and takes no markup on Meta's rates.

### What happens if a recipient opted out?

On a `MARKETING` campaign, they are excluded before sending and marked `skipped`. They do not use
your quota and Meta charges you nothing for them.

### Why is `sent` high but `delivered` low?

`sent` only means Meta accepted the message. Delivery receipts follow by webhook; if they do not
come, look at `failed` and at errors `131049` (marketing limit) or `131026` (recipient unreachable).

### Can I launch the same campaign again?

Yes, once it is no longer `running` (`completed` or `failed`): `launch` then only sends recipients
still `pending`, never a message that already went out. Do not launch it again while it is
`running`, even after a `524`: wait for it to leave that status, or 15 minutes if it stays there
(see the limitations above).

## Sources

* Meta — [Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)
* Meta — [Per-user marketing template message limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/marketing-templates/per-user-limits/)
* Meta — [Throughput](https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput)
* Meta — [Pricing on the WhatsApp Business Platform](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)
* Meta — [Get opt-in for WhatsApp](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in)
* Meta — [Template fundamentals](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/overview)
* Meta — [Marketing Messages API: get started](https://developers.facebook.com/documentation/business-messaging/whatsapp/marketing-messages/get-started)
* Meta — [Partners (Tech Providers and Solution Partners)](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)
* Meta — [Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)
* Meta — [Onboard WhatsApp Business app users](https://developers.facebook.com/documentation/business-messaging/whatsapp/embedded-signup/onboarding-business-app-users/)
* WhatsApp — [Business Messaging Policy](https://whatsappbusiness.com/policy/)
* Cloudflare — [Error 524: a timeout occurred](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/)
* Genuka — [genuka.com](https://genuka.com)
