Send a WhatsApp message to a lead
const url = 'https://app.whatsetter.com/api/v1/messages';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"lead_id":"example","text":"example","humanize":true}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://app.whatsetter.com/api/v1/messages \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "lead_id": "example", "text": "example", "humanize": true }'Sends a text message to a lead through WhatSetter’s anti-ban engine. Two rules are enforced server-side and cannot be bypassed:
- No cold outreach: the lead must already have a conversation.
First contact goes through
POST /lists/{id}/leads+ a campaign (paced, verified sending). Otherwise you get409 lead_not_contacted. - Daily quota: the send consumes the same daily budget as the
campaign engine for that WhatsApp number. When the number’s safe
daily volume is reached you get
429 quota_exceeded(reset at midnight UTC). Check theX-Quota-Remainingresponse header.
The message is sent from the lead’s own campaign number and appears in
the dashboard conversation thread. Requires scope messages:send.
Note: 201 means the message was accepted by the WhatsApp engine.
Like all WhatsApp APIs, final delivery is asynchronous (device online,
contact state…). Delivery/read receipts will surface via webhooks in a
future version.
Idempotency. Send an Idempotency-Key header (≤ 200 characters).
A key that already produced a response replays it for 15 minutes
(X-Idempotent-Replay: true) instead of sending again; a retry fired
while the first request is still running answers 409 request_in_flight. The X-Quota-* headers are set on the 201 and
on the 429 alike.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
The lead’s id from /leads or /conversations.
When true (default, recommended), the send replays a human choreography (read receipt, “typing…” indicator, a typing delay paced to the message length) before the message goes out. Expect the request to take ~3–15 s. Set to false for an instant send (still quota-gated).
Responses
Section titled “Responses”Message sent
object
object
Conversation row id
Whether the human choreography ran.
Typing delay that was played, in milliseconds (0 when humanize is false).
object
Example
{ "data": { "id": "68f2bb33cc44dd55ee66ff77", "message_id": "true_33612345678@c.us_3EB0C8A1B2C3D4E5F6", "lead_id": "68e0a1b2c3d4e5f6a7b8c9d0", "text": "Bonjour Marie, je vous confirme notre appel de demain à 10h.", "sent_at": "2026-09-19T08:12:45.000Z", "humanized": true, "typing_ms": 4200, "quota": { "limit": 250, "used": 41, "remaining": 209, "resets_at": "2026-09-20T00:00:00.000Z" } }}Headers
Section titled “Headers”Daily quota of the sending number
Missing, unknown or revoked API key
object
object
Examplegenerated
{ "error": { "code": "example", "message": "example" }}The key lacks the required scope
object
object
Examplegenerated
{ "error": { "code": "example", "message": "example" }}Resource not found in this workspace
object
object
Examplegenerated
{ "error": { "code": "example", "message": "example" }}Either the lead was never contacted (lead_not_contacted: cold
outreach must go through lists + campaigns) or a request with the
same Idempotency-Key is still in flight (request_in_flight).
object
object
Example
{ "error": { "code": "lead_not_contacted", "message": "This lead has never been contacted. First contact must go through a list + campaign (the anti-ban pipeline) — see POST /v1/lists/{id}/leads." }}Invalid parameter or body (also an invalid or expired cursor)
object
object
Examplegenerated
{ "error": { "code": "example", "message": "example" }}Daily anti-ban quota reached for this number (quota_exceeded, resets at midnight UTC)
object
object
Example
{ "error": { "code": "quota_exceeded", "message": "Daily WhatsApp quota reached for this number (250 messages — anti-ban protection). It resets at midnight UTC." }}Headers
Section titled “Headers”Messaging backend not configured on this deployment (internal_error)
object
object
Examplegenerated
{ "error": { "code": "example", "message": "example" }}send_failed: the WhatsApp engine refused the message, or did not
confirm it in time. With an Idempotency-Key, retrying is safe
(an ambiguous send keeps the key so the retry can never
double-deliver); without one, do not blindly retry.
object
object
Example
{ "error": { "code": "send_failed", "message": "The message could not be sent. Retry shortly." }}agent_disconnected: the campaign has no WhatsApp number, or the
number is currently disconnected. Reconnect it in the dashboard.
object
object
Example
{ "error": { "code": "agent_disconnected", "message": "The WhatsApp number for this campaign is currently disconnected. Reconnect it in the dashboard." }}
