Skip to content

Send a WhatsApp message to a lead

POST
/messages
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:

  1. 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 get 409 lead_not_contacted.
  2. 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 the X-Quota-Remaining response 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.

Media typeapplication/json
object
lead_id
required

The lead’s id from /leads or /conversations.

string
text
required
string
<= 4096 characters
humanize

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).

boolean
default: true

Message sent

Media typeapplication/json
object
data
object
id

Conversation row id

string | null
message_id
string
lead_id
string
text
string
sent_at
string format: date-time
humanized

Whether the human choreography ran.

boolean
typing_ms

Typing delay that was played, in milliseconds (0 when humanize is false).

integer
quota
object
limit
integer
used
integer
remaining
integer
resets_at
string format: date-time
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"
}
}
}
X-Quota-Limit
integer

Daily quota of the sending number

X-Quota-Remaining
integer
X-Quota-Reset
string format: date-time

Missing, unknown or revoked API key

Media typeapplication/json
object
error
object
code
string
message
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example"
}
}

The key lacks the required scope

Media typeapplication/json
object
error
object
code
string
message
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example"
}
}

Resource not found in this workspace

Media typeapplication/json
object
error
object
code
string
message
string
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).

Media typeapplication/json
object
error
object
code
string
message
string
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)

Media typeapplication/json
object
error
object
code
string
message
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example"
}
}

Daily anti-ban quota reached for this number (quota_exceeded, resets at midnight UTC)

Media typeapplication/json
object
error
object
code
string
message
string
Example
{
"error": {
"code": "quota_exceeded",
"message": "Daily WhatsApp quota reached for this number (250 messages — anti-ban protection). It resets at midnight UTC."
}
}
X-Quota-Limit
integer
X-Quota-Remaining
integer
X-Quota-Reset
string format: date-time

Messaging backend not configured on this deployment (internal_error)

Media typeapplication/json
object
error
object
code
string
message
string
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.

Media typeapplication/json
object
error
object
code
string
message
string
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.

Media typeapplication/json
object
error
object
code
string
message
string
Example
{
"error": {
"code": "agent_disconnected",
"message": "The WhatsApp number for this campaign is currently disconnected. Reconnect it in the dashboard."
}
}