Mensajes
POST /messages escribe a un lead desde el número de WhatsApp de su campaña, como si un compañero hubiera tomado el control en el dashboard. Las reglas anti-ban se aplican en el servidor y no se pueden saltar.
Endpoints
Sección titulada «Endpoints»| Método | Ruta | Scope |
|---|---|---|
| POST | /messages |
messages:send |
Enviar un mensaje
Sección titulada «Enviar un mensaje»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":"¿Confirmamos mañana a las 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: '¿Confirmamos mañana a las 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": "¿Confirmamos mañana a las 14h?", },)message = r.json()["data"]{ "data": { "id": "66f1a2b3c4d5e6f7a8b9c0e5", "message_id": "true_34612345678@c.us_3EB0C8F2A1D4B5E6F7A8", "lead_id": "66f1a2b3c4d5e6f7a8b9c0d1", "text": "¿Confirmamos mañana a las 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" } }}| Campo | Significado |
|---|---|
id |
El id de la fila de conversación en WhatSetter, o null si la fila no se pudo escribir tras el envío. |
message_id |
El id del mensaje de WhatsApp. |
lead_id |
El lead al que escribiste. |
text |
El texto enviado. |
sent_at |
Cuándo lo aceptó el motor. |
humanized |
Si se reprodujo la coreografía humana. |
typing_ms |
El retraso de escritura simulado, 0 cuando humanize es false. |
quota |
El presupuesto diario del número tras este envío. |
201 significa que el motor de WhatsApp aceptó el mensaje. La entrega final es asíncrona, como en cualquier API de WhatsApp. El mensaje aparece en la conversación del dashboard como una respuesta manual.
Las reglas
Sección titulada «Las reglas»Sin prospección en frío
Sección titulada «Sin prospección en frío»El lead ya debe tener una conversación: al menos un mensaje intercambiado. Si no, recibes 409 lead_not_contacted. El primer contacto pasa por una lista y una campaña o por un lead opt-in.
Cuota diaria por número
Sección titulada «Cuota diaria por número»Cada envío consume el mismo presupuesto diario que el motor de campaña para ese número de WhatsApp (250 mensajes al día cuando no hay una cuota específica configurada). Al alcanzarlo: 429 quota_exceeded, se reinicia a medianoche UTC.
Cada 201 y cada 429 llevan el presupuesto en las cabeceras:
X-Quota-Limit: 250X-Quota-Remaining: 209X-Quota-Reset: 2026-09-20T00:00:00.000ZEl número debe estar conectado
Sección titulada «El número debe estar conectado»Si la campaña no tiene número de WhatsApp, o el número está desconectado, recibes 503 agent_disconnected. Comprueba whatsapp_connected en GET /campaigns y reconéctalo desde el dashboard.
Envío humanizado
Sección titulada «Envío humanizado»Por defecto (humanize: true), el envío reproduce una coreografía humana: confirmación de lectura, indicador «escribiendo», un retraso de escritura proporcional a la longitud del mensaje, y después el envío. Cuenta con 3 a 15 segundos por petición. Es el modo recomendado.
"humanize": false envía al instante, siempre bajo cuota. Resérvalo para confirmaciones urgentes.
El texto
Sección titulada «El texto»text es obligatorio, de 1 a 4096 caracteres, solo texto.
Reintentar sin riesgo con Idempotency-Key
Sección titulada «Reintentar sin riesgo con Idempotency-Key»Una API de mensajes debe poder reintentarse sin peligro: un timeout nunca debe convertirse en un segundo mensaje real de WhatsApp. Pasa una cabecera Idempotency-Key en cada envío, única por intención (un UUID, un id de pedido…), hasta 200 caracteres. WhatSetter la recuerda 15 minutos.
| Situación | Qué obtiene un reintento con la misma clave |
|---|---|
La primera llamada respondió 201 |
El mismo cuerpo 201, con la cabecera X-Idempotent-Replay: true. Sin segundo mensaje. |
| La primera llamada sigue en curso | 409 request_in_flight. Espera y reintenta. |
La primera llamada falló limpiamente (422, 409, 429, 503) |
La clave se libera: tu reintento es un intento nuevo de verdad. |
La primera llamada caducó (502 send_failed) |
409 request_in_flight hasta el final de la ventana: puede que el mensaje haya salido. Comprueba la transcripción antes de volver a enviar. |
Errores que encontrarás
Sección titulada «Errores que encontrarás»| HTTP | code |
Cuándo |
|---|---|---|
| 401 | missing_api_key, invalid_api_key |
Clave ausente, desconocida o revocada. |
| 403 | insufficient_scope |
La clave no tiene messages:send. |
| 404 | not_found |
Lead desconocido, o lead de otro espacio de trabajo. |
| 409 | lead_not_contacted |
El lead nunca fue contactado. Pasa por una lista o un lead opt-in. |
| 409 | request_in_flight |
La misma Idempotency-Key sigue procesándose, o un intento anterior con ella caducó. |
| 422 | validation_error |
lead_id ausente, text vacío o demasiado largo, humanize que no es booleano, clave de más de 200 caracteres. |
| 429 | quota_exceeded |
Presupuesto diario del número alcanzado. Ver X-Quota-Reset. |
| 429 | rate_limited |
Demasiadas peticiones este minuto. Respeta Retry-After. |
| 500 | internal_error |
Motor de mensajería no configurado por nuestra parte. |
| 502 | send_failed |
El motor rechazó o no confirmó el envío. Reintenta con la misma clave. |
| 503 | agent_disconnected |
Sin número en la campaña, o número desconectado. |

