Errores que te dicen qué hacer
Cada error es un envelope JSON con un código estable, un mensaje que dice cómo resolverlo, y Retry-After en todo lo que se soluciona esperando. Esta página es la tabla que tu lógica de reintentos necesita.
La forma
Retry-After: 42
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Daily send limit reached (20/day on Free). It clears at midnight, or Starter sends 60/day",
"messageKey": "serverErrors.dailyCapReached",
"details": { "scope": "day", "limit": 20, "resetAt": "2026-08-25T03:00:00.000Z", "retryAfterSecs": 42 }
}
}messageKey es estable, para que lo traduzcas a tu manera. message es el texto en inglés, de respaldo. Los rechazos que piden un upgrade nombran el plan más barato que lo levanta, porque un error sin salida no es un mensaje de error.
Todos los códigos
| 400 VALIDATION_ERROR | Algún campo vino mal. `details` lista rutas y reglas, nunca tus valores. |
| 401 UNAUTHORIZED / INVALID_TOKEN | La clave falta, está mal o fue revocada. Manda `Authorization: Bearer wa_…`. |
| 402 PAYMENT_REQUIRED | Un límite que solo levanta una compra, como más números o envíos en lote. Esperar no lo resuelve. |
| 403 FORBIDDEN | La cuenta está suspendida. |
| 404 NOT_FOUND | Ese recurso no existe, o no es tuyo. |
| 409 NUMBER_NOT_CONNECTED | El número no tiene una sesión de WhatsApp que permita enviar. Abre Números, revisa WhatsApp en el teléfono y reconecta. Reintentar el envío ahora no sirve. |
| 409 CONFLICT | La petición necesita una acción o ya está en ese estado. Un número restringido requiere `recoveryConfirmed: true`, solo después de comprobar que WhatsApp funciona en el teléfono. Una petición con el mismo `Idempotency-Key` puede seguir en curso: espera el `Retry-After` y repítela con la misma clave. |
| 422 IDEMPOTENCY_KEY_REUSED | Este `Idempotency-Key` ya se usó en otra petición. Envía uno nuevo. Esperar no lo resuelve. |
| 429 RATE_LIMIT_EXCEEDED | Demasiado rápido. `Retry-After` dice exactamente cuánto esperar. |
| 429 QUOTA_EXCEEDED | Límite diario, cuota mensual, o el límite de conversaciones nuevas de un número. `details.scope` dice cuál: `day`, `month` o `newConversations`. `resetAt` dice cuándo se libera. |
| 429 PAIRING_COOLDOWN | Demasiadas solicitudes de código de vinculación para este número. `Retry-After` dice cuándo puedes pedir otro. No es un tiempo fijo de restricción. |
| 502 GATEWAY_ERROR | WhatsApp lo rechazó más arriba. Reintentar podría funcionar. |
| 504 GATEWAY_TIMEOUT | WhatsApp no confirmó a tiempo. Revisa el chat y, si aparece `details.messageId`, sigue ese id en el webhook `message.status` antes de volver a enviar. Un reintento con el mismo `Idempotency-Key` recibe la misma respuesta, sin un segundo envío. |
| 503 SERVICE_UNAVAILABLE | Una dependencia está caída. Reintenta en un momento. |
Cuando el número se cae
El teléfono puede perder la conexión y una cuenta nueva puede no tener número todavía. Los nuevos envíos responden 409 NUMBER_NOT_CONNECTED sin cobrar nada. Los mensajes ya en cola que aún no salieron se pausan hasta que el número reconecte. No se marcan como fallidos solo porque se desconectó, y vuelven a salir con el intervalo configurado. message.failed indica un envío que no puede continuar o cuyo resultado no se conoce, no una cola pausada.
- Ten un segundo canal. Lo que alguien necesita recibir, mándalo también por correo, o por correo cuando WhatsApp lo rechace.
- Antes de ofrecer algo que solo funciona por WhatsApp, como un código de acceso, comprueba que haya un número conectado. Guarda la respuesta un minuto y descártala cuando un envío vuelva con
NUMBER_NOT_CONNECTED. - Si no hay número conectado, oculta esa opción y lleva a la persona al correo. No muestres un botón que solo puede fallar.
- Si tienes webhook,
number.disconnectedllega cuando el número sigue caído tras 15 minutos de reintentos, ynumber.connectedcuando vuelve.
import { Whatsapi } from "@whatsapi.sh/sdk";
const wa = new Whatsapi({ apiKey: process.env.WHATSAPI_KEY! });
const numbers = await wa.listNumbers();
const whatsappReady = numbers.some((n) => n.status === "connected");Reglas para reintentar
Respeta Retry-After en un 429. Nunca reintentes un 402 de forma automática, porque se resuelve comprando, no esperando. En un 504, no reenvíes a ciegas: el id del mensaje en details va a aparecer en tu webhook message.status si llegó, y reenviar sin revisar es como una persona recibe el mismo mensaje dos veces. Para reintentar un envío sin riesgo, mándalo con el header Idempotency-Key y repítelo con el mismo valor: recibes la primera respuesta en lugar de un segundo mensaje. Los límites de tasa son protección, no burocracia, y cómo evitar el baneo explica cómo quedarte por debajo desde el principio.