Campaigns
A campaign is one AI agent, one WhatsApp number and one audience. The API gives you the directory (ids, status, connection state), the pause and resume switch, and the door for opt-in leads.
Endpoints
Section titled “Endpoints”| Method | Path | Scope |
|---|---|---|
| GET | /campaigns |
campaigns:read |
| POST | /campaigns/{id}/pause |
campaigns:write |
| POST | /campaigns/{id}/resume |
campaigns:write |
| POST | /campaigns/{id}/leads |
messages:send |
List campaigns
Section titled “List campaigns”curl "https://app.whatsetter.com/api/v1/campaigns" \ -H "Authorization: Bearer ws_live_…"const res = await fetch('https://app.whatsetter.com/api/v1/campaigns', { headers: { Authorization: 'Bearer ws_live_…' },});const { data: campaigns } = await res.json();import requests
r = requests.get( "https://app.whatsetter.com/api/v1/campaigns", headers={"Authorization": "Bearer ws_live_…"},)campaigns = r.json()["data"]{ "data": [ { "id": "66f1a2b3c4d5e6f7a8b9c0c1", "campaign_id": "acme-france-1-ab12", "name": "Alex", "status": "active", "purpose": "setter", "whatsapp_connected": true, "created_at": "2026-08-01T09:00:00.000Z" } ]}| Field | Meaning |
|---|---|
id |
The campaign document id. Accepted in every /campaigns/{id}/... path. |
campaign_id |
The slug that leads, conversations and bookings carry in their own campaign_id. Also accepted in paths and filters. |
name |
The agent’s display name. |
status |
active, draft, paused, archived, or unknown for an internal value. |
purpose |
setter (1-to-1 conversations) or group (WhatsApp groups). |
whatsapp_connected |
true when the campaign’s number can send right now. |
The list holds up to 100 campaigns, newest first. There is no cursor.
Pause and resume
Section titled “Pause and resume”Pausing stops AI replies and sending, the same as the dashboard toggle. Resuming reactivates them.
curl -X POST "https://app.whatsetter.com/api/v1/campaigns/acme-france-1-ab12/pause" \ -H "Authorization: Bearer ws_live_…"const res = await fetch('https://app.whatsetter.com/api/v1/campaigns/acme-france-1-ab12/pause', { method: 'POST', headers: { Authorization: 'Bearer ws_live_…' },});const { data: campaign } = await res.json();import requests
r = requests.post( "https://app.whatsetter.com/api/v1/campaigns/acme-france-1-ab12/pause", headers={"Authorization": "Bearer ws_live_…"},)campaign = r.json()["data"]The response is the campaign object with its new status. Only two transitions exist:
| Call | Allowed from | Result | Otherwise |
|---|---|---|---|
POST /campaigns/{id}/pause |
active |
paused |
409 invalid_state |
POST /campaigns/{id}/resume |
paused |
active |
409 invalid_state |
A draft campaign must be activated from the dashboard. An archived campaign cannot be resumed.
Resuming is subject to your plan’s active-campaign limit: Free and Starter 1, Business 3, Scale 10, plus any extra agents added to your plan. When the limit is full, resume answers 402 plan_limit_reached: pause another campaign or upgrade.
Push an opt-in lead
Section titled “Push an opt-in lead”When someone signs up on your funnel or CRM, push them to the campaign. WhatSetter runs the same pipeline as the campaign’s sales-page webhook: phone validation and WhatsApp check, deduplication against contacts already contacted, daily first-contact quota, then an AI-written first message from the campaign’s template, the lead’s name and every key of additional_data.
curl -X POST "https://app.whatsetter.com/api/v1/campaigns/acme-france-1-ab12/leads" \ -H "Authorization: Bearer ws_live_…" \ -H "Content-Type: application/json" \ -d '{ "phone": "+33612345678", "name": "Julien", "email": "julien@example.com", "source_url": "crm:hubspot", "external_reference": "4471", "additional_data": { "offer": "Pack Pro", "city": "Lyon", "budget": "500-1000" } }'const res = await fetch('https://app.whatsetter.com/api/v1/campaigns/acme-france-1-ab12/leads', { method: 'POST', headers: { Authorization: 'Bearer ws_live_…', 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: '+33612345678', name: 'Julien', email: 'julien@example.com', source_url: 'crm:hubspot', external_reference: '4471', additional_data: { offer: 'Pack Pro', city: 'Lyon', budget: '500-1000' }, }),});const { data } = await res.json(); // 202import requests
r = requests.post( "https://app.whatsetter.com/api/v1/campaigns/acme-france-1-ab12/leads", headers={"Authorization": "Bearer ws_live_…"}, json={ "phone": "+33612345678", "name": "Julien", "email": "julien@example.com", "source_url": "crm:hubspot", "external_reference": "4471", "additional_data": {"offer": "Pack Pro", "city": "Lyon", "budget": "500-1000"}, },)data = r.json()["data"] # 202{ "data": { "status": "accepted", "campaign_id": "acme-france-1-ab12", "phone": "+33612345678", "external_reference": "4471", "note": "Queued into the first-contact pipeline: deduplication, daily first-contact quota and anti-ban pacing apply. The lead is created later, so no lead id exists yet…" }}| Field | Rules |
|---|---|
phone |
Required. International format, 6 to 15 digits. Echoed back with a +. |
name, email |
Optional, cut at 255 characters. |
source_url |
Optional, up to 255 characters. Where the lead came from. Defaults to api. |
external_reference |
Optional, up to 255 characters. Your own record id, stored verbatim and echoed on every lead and webhook. |
additional_data |
Optional object, up to 30 keys and 2000 characters once serialized. Used to personalize the first message, never stored, never returned. Keys named like the fields above (phone, name, email, source_url, external_reference) are ignored. |
It is asynchronous
Section titled “It is asynchronous”The endpoint answers 202 as soon as the lead is accepted. The lead row is created later, after deduplication and pacing, so there is no lead id yet. To follow up:
- look the lead up with
GET /leads?external_reference=4471(or?phone=+33612345678); - or subscribe to webhooks:
contact.external_referencecarries your id.
A contact already contacted by the campaign is deduplicated silently: no second first message, and the 202 looks the same. Over-quota leads are queued for the next window, never dropped.
Quotas
Section titled “Quotas”Two budgets protect the number, and the API cannot bypass either:
- Daily first contacts per campaign: opt-in leads beyond it wait for the next day.
- Daily messages per WhatsApp number: shared with
POST /messages, see Messages for theX-Quota-*headers.
On top of that, requests are limited per workspace per minute by plan (429 rate_limited with Retry-After).
Errors you will meet
Section titled “Errors you will meet”| HTTP | code |
When |
|---|---|---|
| 401 | missing_api_key, invalid_api_key |
Key absent, unknown or revoked. |
| 402 | plan_limit_reached |
Resume refused: the plan’s active-campaign limit is full. |
| 403 | insufficient_scope |
The key lacks campaigns:read, campaigns:write or messages:send. |
| 404 | not_found |
Unknown campaign, or a campaign of another workspace. |
| 409 | invalid_state |
Pause on a non-active campaign, resume on a non-paused one, lead pushed to a non-active campaign, or a campaign that has never been opened in the dashboard. |
| 422 | validation_error |
Bad phone, additional_data not an object or too big, external_reference too long. |
| 429 | rate_limited |
Too many requests this minute. Honor Retry-After. |
| 502 | upstream_error |
The first-message pipeline was unreachable. Retry in a moment. |

