# Errores

> Qué significa cada código de respuesta y cómo manejarlo.

La API usa los códigos HTTP de siempre: `2xx` salió bien, `4xx` hay algo para corregir en la llamada o en la cuenta, `5xx` falló algo de nuestro lado o de Meta.

## Formato

Casi todos los errores tienen esta forma:

```json
{
  "error": "Pasaron más de 24 horas desde el último mensaje del cliente. Para escribirle, usá una plantilla.",
  "cause": null
}
```

- `error`: explica el problema en español. Está pensado para mostrarlo tal cual a tu equipo.
- `cause`: detalle para tu código. Es `null` salvo en los errores de plan (`402`) y de cupo mensual (`429`).

<Note>
  Los mensajes de error usan el español de Argentina (“usá”, “elegí”), igual que la app de MINDO.
</Note>

### Errores de validación

Cuando al cuerpo o a los parámetros les falta algo o tienen un tipo equivocado, la respuesta `400` tiene otra forma: `error.message` es un JSON (en texto) con la lista de problemas. `path` dice qué campo y `message` por qué.

```json
{
  "success": false,
  "error": {
    "name": "ZodError",
    "message": "[\n  {\n    \"expected\": \"string\",\n    \"code\": \"invalid_type\",\n    \"path\": [\n      \"phone\"\n    ],\n    \"message\": \"Invalid input: expected string, received undefined\"\n  }\n]"
  }
}
```

Para leerlo:

```javascript
const body = await res.json()
const problems = body.success === false ? JSON.parse(body.error.message) : []
// [{ path: ["phone"], message: "Invalid input: expected string, received undefined", ... }]
```

## Códigos

<AccordionGroup>
  <Accordion title="400 · Datos inválidos" icon="circle-x">
    Algo del pedido no es válido. Los casos más comunes:

    - Falta un campo obligatorio (`phone` en contactos, `to` en mensajes).
    - Mandaste `text` y `template` a la vez, o ninguno de los dos: _“Mandá `text` o `template`, uno de los dos.”_
    - El teléfono no es válido: _“El teléfono “123” no es válido. Usá formato internacional, por ejemplo +5491123456789.”_ Revisa [Formato de teléfonos](/conceptos/formato-de-telefonos).
    - `limit` fuera de rango (1 a 200) o `after` que no es un UUID.

    **Qué hacer:** corrige el pedido. Reintentar igual va a fallar igual.
  </Accordion>

  <Accordion title="401 · Sin autenticación" icon="lock">
    Falta la clave, está mal escrita o fue revocada.

    ```json
    { "error": "La clave de API no es válida o fue revocada.", "cause": null }
    ```

    **Qué hacer:** revisa que el encabezado sea `Authorization: Bearer etk_live_…` y que la clave siga activa en **Ajustes › API y webhooks**.
  </Accordion>

  <Accordion title="402 · Tu plan no lo permite" icon="credit-card">
    La cuenta no tiene un plan que permita esto. `cause.reason` te dice por qué:

    | `reason` | Qué pasó |
    | --- | --- |
    | `feature` | El plan no incluye la función: `cause.feature` es `"api"` o `"automations"`. |
    | `paused` | La prueba gratis terminó o la cuenta está pausada. |
    | `limit` | Llegaste a un límite del plan: contactos, claves de API o URLs de webhooks (`cause.resource`). `cause.limit` y `cause.used` traen los números. |

    `cause.suggested` es el plan que lo resolvería.

    ```json
    {
      "error": "Tu plan incluye hasta 50.000 contactos. Pasate a Enterprise para sumar más.",
      "cause": {
        "type": "upgrade",
        "reason": "limit",
        "resource": "contacts",
        "plan": "escala",
        "limit": 50000,
        "used": 50000,
        "suggested": "enterprise"
      }
    }
    ```

    **Qué hacer:** avisa a quien administra la cuenta. Se resuelve en **Ajustes › Plan y uso**. El límite de contactos solo se aplica al crear contactos nuevos (con `POST /contacts` o al mandar un mensaje a un teléfono que no estaba).
  </Accordion>

  <Accordion title="404 · No existe" icon="search-x">
    El recurso no existe en esta cuenta: _“Mensaje no encontrado.”_ o _“La plantilla ya no existe.”_

    **Qué hacer:** revisa el id. Los ids son de cada cuenta: una clave no ve lo de otra cuenta.
  </Accordion>

  <Accordion title="409 · No se puede mandar ahora" icon="clock">
    El pedido es válido, pero WhatsApp no deja hacerlo en este momento:

    - _“Pasaron más de 24 horas desde el último mensaje del cliente. Para escribirle, usá una plantilla.”_ → manda una plantilla en vez de `text`.
    - _“La plantilla todavía no está aprobada por Meta.”_ → espera la aprobación o usa otra.
    - _“No hay un número de WhatsApp conectado con ese id.”_ → revisa `phoneNumberId`.
    - _“Tu WhatsApp está desconectado.”_ → vuelve a conectar el número en **Ajustes › Números y canales**.
    - _“La automatización no está activa.”_ → publícala o actívala en **Automatizaciones**.
    - _“Esta conversación ya tiene una automatización en curso.”_ → espera a que termine o detenla desde el chat.

    **Qué hacer:** no reintentes a ciegas; cambia el pedido o resuelve el estado de la cuenta.
  </Accordion>

  <Accordion title="429 · Demasiadas solicitudes" icon="gauge">
    Llegaste a un límite de tu plan. Hay tres casos:

    - **Tope por minuto** de solicitudes o de mensajes. Viene con los encabezados `Retry-After: 60` y `X-RateLimit-Limit`:

      ```json
      { "error": "Superaste el tope de solicitudes por minuto de tu plan. Esperá un minuto." }
      ```

    - **Cupo mensual** de llamadas a la API. Trae `cause` con `type: "upgrade"`, como un `402`, y no trae `Retry-After`:

      ```json
      {
        "error": "Usaste las 300.000 llamadas a la API de este mes. Se renueva el 1° del mes, o pasate a Enterprise.",
        "cause": { "type": "upgrade", "reason": "limit", "resource": "apiCalls", "plan": "escala", "limit": 300000, "used": 300000, "suggested": "enterprise" }
      }
      ```

    - **Demasiados intentos con claves inválidas** desde la misma IP: la IP queda bloqueada un minuto.

    **Qué hacer:** con el tope por minuto, espera lo que diga `Retry-After` y reintenta con espera creciente. Con el cupo mensual, deja de reintentar hasta el día 1 o hasta cambiar de plan. Los números de cada plan y un ejemplo de código están en [Límites](/api/limites).
  </Accordion>

  <Accordion title="502 · Meta rechazó el mensaje" icon="cloud-off">
    Meta no aceptó el envío. El mensaje incluye el motivo que dio Meta, por ejemplo:

    ```json
    { "error": "WhatsApp no aceptó el mensaje: (#131030) Recipient phone number not in allowed list", "cause": null }
    ```

    **Qué hacer:** lee el motivo. Algunos son temporales (reintenta en unos minutos) y otros son de la cuenta de Meta, como que falte el medio de pago. Mira [Cargar el medio de pago](/guias/medio-de-pago).
  </Accordion>

  <Accordion title="500 · Error inesperado" icon="server-crash">
    ```json
    { "error": "Algo salió mal. Volvé a intentar en unos segundos." }
    ```

    **Qué hacer:** reintenta con espera creciente (1 s, 2 s, 4 s…). Si se repite, escríbenos a [soporte@mindo.ar](mailto:soporte@mindo.ar) con la hora y el endpoint.
  </Accordion>
</AccordionGroup>

## Mensajes que se aceptan pero después fallan

Un `201` en `POST /messages` quiere decir que **Meta aceptó** el mensaje, no que ya llegó. La entrega puede fallar después: por ejemplo, si el número no tiene WhatsApp. En ese caso el mensaje queda con `status: "failed"` y un código de Meta:

```json
{
  "status": "failed",
  "errorCode": 131026,
  "errorTitle": "Message undeliverable"
}
```

Lo ves con [`GET /messages/{id}`](/api-reference/mensajes/consultar) o con el webhook [`message.status`](/api/webhooks/eventos#message-status).

Códigos de Meta frecuentes:

| Código | Qué significa |
| --- | --- |
| `131026` | No se pudo entregar: el número no tiene WhatsApp o no acepta mensajes de empresas. |
| `131049` | Meta no lo entregó para cuidar la experiencia del usuario (demasiados mensajes de marketing recibidos). Prueba otro día. |
| `131050` | El usuario dejó de aceptar mensajes de marketing de tu empresa. |
| `131047` | Pasaron más de 24 horas: hacía falta una plantilla. |
| `131042` | Hay un problema con el medio de pago de tu cuenta de Meta. |

## Reintentos seguros

- Reintenta solo `500`, `502` temporales, `429` por tope por minuto y errores de red.
- `POST /contacts` se puede reintentar sin problema: si el contacto ya existe, lo actualiza.
- `POST /messages` **no** es idempotente: si reintentas después de un timeout, el cliente podría recibir el mensaje dos veces. Antes de reintentar, fíjate si el mensaje ya aparece en el chat o guarda el `id` de la primera respuesta.
