Errores, estados y límites
Ciclo de vida de un mensaje
Todo mensaje (individual o de broadcast) avanza por estos estados, visibles en el portal (sección Mensajes):
| Estado | Significado |
|---|---|
pending |
Recibido por Arplyx, esperando que el worker lo procese. |
queued |
En cola para salir al canal. Si hubo un fallo transitorio, queda acá entre reintentos. |
sent |
Entregado al proveedor del canal (Meta o WhatsApp Direct). |
delivered |
El proveedor confirmó entrega en el dispositivo del destinatario. |
read |
El destinatario lo leyó (cuando el canal lo informa). |
failed |
Falló de forma definitiva: se agotaron los reintentos o el error no es recuperable. El detalle del mensaje muestra el código y la descripción del último error. |
Reintentos: los errores transitorios (problemas de red, 5xx del proveedor, rate limit) se reintentan automáticamente con backoff exponencial, hasta 5 intentos por defecto. Los errores permanentes (número inválido, cuenta desvinculada, credenciales rechazadas) marcan el mensaje failed sin reintentar.
De dónde salen delivered y read
- Meta Official: llegan por el webhook de tu app de Meta. Si no lo configuraste, los mensajes quedan en
sent. Ver el paso 6 de la guía Meta. - WhatsApp Direct: llegan por la sesión vinculada, típicamente a los pocos segundos, mientras la sesión esté conectada. El
readdepende de que el destinatario tenga activadas las confirmaciones de lectura en WhatsApp.
Para reaccionar a estas transiciones en tiempo real desde tu sistema, usá webhooks.
Vida útil de un mensaje: ttlSeconds, retención e idempotencia
Estos tres conceptos están relacionados — un mensaje vive ttlSeconds (default 24 h, recortado a la retención de tu plan) desde que se crea:
- Al expirar, el mensaje se elimina (el registro completo: estados, historial de intentos). No pasa a
failedni dispara webhook: simplemente deja de existir en la próxima pasada de la purga (corre cada pocas horas). - Por eso
GET /messages/{id}responde404para mensajes purgados: diseñá tu reconciliación asumiendo que los mensajes viejos desaparecen según la retención del plan (3 días en Free, 30 en el resto). - La deduplicación por
externalIdvive lo mismo que el mensaje: mientras el registro exista, repetir elexternalIddevuelve el original (o409si el payload difiere). Una vez purgado, eseexternalIdqueda reutilizable y un request repetido crearía (y enviaría) un mensaje nuevo. Si tu sistema puede reintentar operaciones viejas, agregale alexternalIdun componente temporal (ej.turno-4812-2026-07-04).
En la práctica los reintentos de envío se agotan mucho antes del TTL (5 intentos con backoff, minutos), así que un mensaje que no pudo salir va a estar failed — con su webhook y su error.code — bastante antes de expirar.
Rate limits
Hoy la API pública no aplica límite de requests por segundo: el único límite es la cuota mensual del plan (429 quota_exceeded, sin header Retry-After; usá el campo resetsAt del body). Dimensioná tus workers con criterio — si más adelante agregamos un límite operativo, lo vamos a anunciar en la documentación con antelación.
Catálogo de errores HTTP
Todas las respuestas de error tienen un campo error; algunas agregan message o campos extra.
| Status | error |
Cuándo |
|---|---|---|
| 400 | validation_error |
El payload no pasa la validación. El cuerpo incluye details: una lista de { "path": "campo.afectado", "message": "..." } por cada problema. |
| 401 | Missing x-api-key header / Invalid API key |
Falta la clave, es inválida o fue revocada. |
| 409 | conflict |
El externalId ya existe con un payload distinto. Elegí otro externalId o repetí el payload original. |
| 422 | invalid_whatsapp_account |
El whatsappAccountId no existe, no es tuyo, o su tipo no coincide con el channel (ej.: cuenta Direct con canal whatsapp_meta). |
| 403 | templates_not_available |
Intentaste enviar una plantilla en el plan Free. El envío de plantillas (proactivo) está disponible desde el plan Starter. |
| 403 | feature_not_available |
La función no está disponible en tu plan (ej.: GET /inbound-messages fuera de Pro, webhooks fuera de Basic/Pro). El cuerpo incluye plan. |
| 403 | key_scope_violation |
La API key está scopeada a una cuenta y pediste operar sobre otra. |
| 404 | account_not_found |
El whatsappAccountId indicado en una consulta no existe o no es tuyo. |
| 422 | whatsapp_account_required |
La consulta necesita whatsappAccountId (tenés más de una cuenta vinculada, o ninguna) — ver remitentes entrantes. |
| 429 | quota_exceeded |
Cuota mensual agotada. El cuerpo incluye limit, used y resetsAt (cuándo se renueva: primer día del mes, UTC). |
| 500 | Internal server error |
Error inesperado de Arplyx. Reintentá con el mismo externalId (es seguro por idempotencia); si persiste, contactanos. |
Ejemplo de validation_error:
{
"error": "validation_error",
"details": [
{ "path": "to.phoneE164", "message": "Invalid E.164 phone number" }
]
}
Los errores exclusivos de broadcast (lista vacía, plan sin broadcasts, etc.) están en Broadcasts por API.
Cuotas y límites por plan
| Plan | Mensajes/mes | Cuentas WhatsApp | Retención de historial | Broadcasts |
|---|---|---|---|---|
| Free | 500 | 1 | 3 días | No |
| Starter | 2.000 | 1 | 30 días | No |
| Basic | 10.000 | 3 | 30 días | Sí |
| Pro | 20.000 | 5 | 30 días | Sí |
- La cuota es compartida entre la API, el portal y los broadcasts: todo mensaje individual cuenta.
- Se renueva el día 1 de cada mes (UTC). El cuerpo del error
429te dice exactamente cuándo (resetsAt). - Tope adicional por broadcast: 5.000 destinatarios.
ttlSecondsse recorta a la retención del plan (en Free, un mensaje no puede vivir más de 3 días en cola).- La retención aplica al contenido de los mensajes (salientes y entrantes). El agregado de remitentes entrantes (
hasWritten,firstInboundAt) no se purga: persiste mientras exista la cuenta de WhatsApp, en todos los planes.
¿Necesitás más volumen? Escribinos a info@arplyx.com.