Messages
POST /messages écrit à un lead depuis le numéro WhatsApp de sa campagne, comme si un coéquipier avait pris la main dans le dashboard. Les règles anti-ban sont appliquées côté serveur et ne peuvent pas être contournées.
Endpoints
Section intitulée « Endpoints »| Méthode | Chemin | Scope |
|---|---|---|
| POST | /messages |
messages:send |
Envoyer un message
Section intitulée « Envoyer un message »curl -X POST "https://app.whatsetter.com/api/v1/messages" \ -H "Authorization: Bearer ws_live_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8c3d2f9e-7b1a-4e6f-9c0d-2a1b3c4d5e6f" \ -d '{"lead_id":"66f1a2b3c4d5e6f7a8b9c0d1","text":"On se confirme pour demain 14h ?"}'const res = await fetch('https://app.whatsetter.com/api/v1/messages', { method: 'POST', headers: { Authorization: 'Bearer ws_live_…', 'Content-Type': 'application/json', 'Idempotency-Key': '8c3d2f9e-7b1a-4e6f-9c0d-2a1b3c4d5e6f', }, body: JSON.stringify({ lead_id: '66f1a2b3c4d5e6f7a8b9c0d1', text: 'On se confirme pour demain 14h ?', }),});const { data: message } = await res.json();import requests
r = requests.post( "https://app.whatsetter.com/api/v1/messages", headers={ "Authorization": "Bearer ws_live_…", "Idempotency-Key": "8c3d2f9e-7b1a-4e6f-9c0d-2a1b3c4d5e6f", }, json={ "lead_id": "66f1a2b3c4d5e6f7a8b9c0d1", "text": "On se confirme pour demain 14h ?", },)message = r.json()["data"]{ "data": { "id": "66f1a2b3c4d5e6f7a8b9c0e5", "message_id": "true_33612345678@c.us_3EB0C8F2A1D4B5E6F7A8", "lead_id": "66f1a2b3c4d5e6f7a8b9c0d1", "text": "On se confirme pour demain 14h ?", "sent_at": "2026-09-19T08:12:04.000Z", "humanized": true, "typing_ms": 3210, "quota": { "limit": 250, "used": 41, "remaining": 209, "resets_at": "2026-09-20T00:00:00.000Z" } }}| Champ | Signification |
|---|---|
id |
L’id de la ligne de conversation dans WhatSetter, ou null si la ligne n’a pas pu être écrite après l’envoi. |
message_id |
L’id du message WhatsApp. |
lead_id |
Le lead à qui tu as écrit. |
text |
Le texte envoyé. |
sent_at |
Quand le moteur l’a accepté. |
humanized |
Si la chorégraphie humaine a été jouée. |
typing_ms |
Le délai de frappe simulé, 0 quand humanize vaut false. |
quota |
Le budget quotidien du numéro après cet envoi. |
201 signifie que le message a été accepté par le moteur WhatsApp. La livraison finale est asynchrone, comme sur toute API WhatsApp. Le message apparaît dans la conversation du dashboard comme une réponse manuelle.
Les règles
Section intitulée « Les règles »Pas de prospection à froid
Section intitulée « Pas de prospection à froid »Le lead doit déjà avoir une conversation : au moins un message échangé. Sinon tu reçois 409 lead_not_contacted. Le premier contact passe par une liste et une campagne ou un lead opt-in.
Quota quotidien par numéro
Section intitulée « Quota quotidien par numéro »Chaque envoi consomme le même budget quotidien que le moteur de campagne pour ce numéro WhatsApp (250 messages par jour quand aucun quota spécifique n’est réglé). Une fois atteint : 429 quota_exceeded, remise à zéro à minuit UTC.
Chaque 201 et chaque 429 porte le budget dans les en-têtes :
X-Quota-Limit: 250X-Quota-Remaining: 209X-Quota-Reset: 2026-09-20T00:00:00.000ZLe numéro doit être connecté
Section intitulée « Le numéro doit être connecté »Si la campagne n’a pas de numéro WhatsApp, ou si le numéro est déconnecté, tu reçois 503 agent_disconnected. Vérifie whatsapp_connected sur GET /campaigns et reconnecte-le depuis le dashboard.
Envoi humanisé
Section intitulée « Envoi humanisé »Par défaut (humanize: true), l’envoi rejoue une chorégraphie humaine : accusé de lecture, indicateur « en train d’écrire », un délai de frappe proportionnel à la longueur du message, puis l’envoi. Compte 3 à 15 secondes par requête. C’est le mode recommandé.
"humanize": false envoie instantanément, toujours sous quota. Réserve-le aux confirmations urgentes.
Le texte
Section intitulée « Le texte »text est obligatoire, 1 à 4096 caractères, texte seulement.
Réessayer sans risque avec Idempotency-Key
Section intitulée « Réessayer sans risque avec Idempotency-Key »Une API de messages doit pouvoir être rejouée sans danger : un timeout ne doit jamais se transformer en second vrai message WhatsApp. Passe un en-tête Idempotency-Key à chaque envoi, unique par intention (un UUID, un id de commande…), 200 caractères maximum. WhatSetter s’en souvient 15 minutes.
| Situation | Ce qu’obtient un retry avec la même clé |
|---|---|
Le premier appel a répondu 201 |
Le même corps 201, avec l’en-tête X-Idempotent-Replay: true. Pas de second message. |
| Le premier appel est encore en cours | 409 request_in_flight. Attends et réessaie. |
Le premier appel a échoué proprement (422, 409, 429, 503) |
La clé est libérée : ton retry est une vraie nouvelle tentative. |
Le premier appel a expiré (502 send_failed) |
409 request_in_flight jusqu’à la fin de la fenêtre : le message est peut-être parti. Vérifie la transcription avant de renvoyer. |
Les erreurs que tu rencontreras
Section intitulée « Les erreurs que tu rencontreras »| HTTP | code |
Quand |
|---|---|---|
| 401 | missing_api_key, invalid_api_key |
Clé absente, inconnue ou révoquée. |
| 403 | insufficient_scope |
La clé n’a pas messages:send. |
| 404 | not_found |
Lead inconnu, ou lead d’un autre espace de travail. |
| 409 | lead_not_contacted |
Le lead n’a jamais été contacté. Passe par une liste ou un lead opt-in. |
| 409 | request_in_flight |
Même Idempotency-Key encore en traitement, ou une tentative précédente avec cette clé a expiré. |
| 422 | validation_error |
lead_id absent, text vide ou trop long, humanize qui n’est pas un booléen, clé de plus de 200 caractères. |
| 429 | quota_exceeded |
Budget quotidien du numéro atteint. Voir X-Quota-Reset. |
| 429 | rate_limited |
Trop de requêtes cette minute. Respecte Retry-After. |
| 500 | internal_error |
Moteur de messagerie non configuré de notre côté. |
| 502 | send_failed |
Le moteur a refusé ou n’a pas confirmé l’envoi. Réessaie avec la même clé. |
| 503 | agent_disconnected |
Pas de numéro sur la campagne, ou numéro hors ligne. |

