> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waspytech.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Messages

> Enviar mensajes de WhatsApp y consultar su estado.

## POST /messages

Enviar un mensaje de WhatsApp. Soporta texto, imagen, video, audio, documento, template e interactivos.

**Scope requerido:** `messages:write`

### Body

| Campo           | Tipo          | Requerido        | Descripción                                                                                                                                                                                                                      |
| --------------- | ------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `phoneNumberId` | string (UUID) | Sí               | ID del canal (de `GET /channels`)                                                                                                                                                                                                |
| `to`            | string        | Sí               | Número del destinatario en formato E.164 (`+5491126032641`)                                                                                                                                                                      |
| `type`          | string        | Sí               | `text`, `image`, `video`, `audio`, `document`, `template`, `interactive`                                                                                                                                                         |
| `text`          | object        | Si type=text     | `{ body: string, preview_url?: boolean }`                                                                                                                                                                                        |
| `image`         | object        | Si type=image    | `{ link?: string, id?: string, caption?: string }`                                                                                                                                                                               |
| `video`         | object        | Si type=video    | `{ link?: string, id?: string, caption?: string }`                                                                                                                                                                               |
| `audio`         | object        | Si type=audio    | `{ link?: string, id?: string }`                                                                                                                                                                                                 |
| `document`      | object        | Si type=document | `{ link?: string, id?: string, filename?: string, caption?: string }`                                                                                                                                                            |
| `template`      | object        | Si type=template | `{ name: string, language: { code: string }, components?: array }`                                                                                                                                                               |
| `clientRef`     | string        | No               | Identificador propio (máx 256 chars) que se devuelve **verbatim** en cada webhook `message.status_changed` y en `GET /messages/:id`. Útil para reconciliar contra tus propios IDs (ej. número de pedido) sin guardar un mapping. |

### Enviar texto

```bash theme={null}
curl -X POST https://api.waspytech.com/api/v2/messages \
  -H "Authorization: Bearer wspy_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "channel-uuid",
    "to": "+5491126032641",
    "type": "text",
    "text": { "body": "Hola! Tu pedido fue despachado." }
  }'
```

<Warning>
  Los mensajes de texto libre requieren una ventana de servicio activa (24hs desde el último mensaje del contacto). Si la ventana expiró, usá un template. Ver [Ventana de servicio](/concepts/service-window).
</Warning>

### Enviar template

```bash theme={null}
curl -X POST https://api.waspytech.com/api/v2/messages \
  -H "Authorization: Bearer wspy_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "channel-uuid",
    "to": "+5491126032641",
    "type": "template",
    "template": {
      "name": "confirmacion_pedido",
      "language": { "code": "es_AR" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "María" },
            { "type": "text", "text": "#1847" }
          ]
        }
      ]
    }
  }'
```

### Enviar imagen

```bash theme={null}
curl -X POST https://api.waspytech.com/api/v2/messages \
  -H "Authorization: Bearer wspy_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumberId": "channel-uuid",
    "to": "+5491126032641",
    "type": "image",
    "image": { "link": "https://ejemplo.com/producto.jpg", "caption": "Vista frontal" }
  }'
```

### Response `201`

```json theme={null}
{
  "data": {
    "id": "msg-uuid",
    "conversationId": "conv-uuid",
    "direction": "outbound",
    "type": "text",
    "status": "queued",
    "waMessageId": null,
    "createdAt": "2026-04-13T15:00:00.000Z"
  },
  "meta": { "requestId": "..." }
}
```

<Warning>
  **`waMessageId` es `null` en el response inicial.** El envío a Meta es asíncrono — `status: "queued"` significa que aceptamos la request y la encolamos. Para obtener el `waMessageId` real (el `wamid.HBgL...` de Meta) tenés dos opciones:

  1. **Recomendado:** suscribite al webhook `message.status_changed`. Cuando el status pasa a `sent`, el payload trae `waMessageId` poblado. Es la forma confiable.
  2. **Polling:** `GET /messages/{data.id}` después de 1-2 segundos.

  Para correlacionar tu mensaje con el de Meta inmediatamente, guardá el `data.id` (UUID interno) que sí está disponible al instante.
</Warning>

### Idempotencia

Incluí el header `Idempotency-Key` para evitar envíos duplicados. Ver [Idempotencia](/idempotency).

```bash theme={null}
curl -X POST https://api.waspytech.com/api/v2/messages \
  -H "Authorization: Bearer wspy_..." \
  -H "Idempotency-Key: pedido-1847-confirmacion" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
```

### Errores frecuentes

| Código                   | Descripción                                   |
| ------------------------ | --------------------------------------------- |
| `SERVICE_WINDOW_EXPIRED` | La ventana de 24hs expiró, usá un template    |
| `INVALID_CHANNEL`        | El canal no existe o no pertenece a tu cuenta |
| `CHANNEL_NOT_CONNECTED`  | El canal no está conectado                    |
| `INVALID_MESSAGE_TYPE`   | Tipo de mensaje no válido                     |
| `INVALID_PHONE`          | Número de teléfono con formato inválido       |

### Comportamiento

* Si el contacto no existe, se crea automáticamente con el número.
* Si no hay conversación previa, se crea una nueva.
* El mensaje se encola y se envía de forma asíncrona. El status inicial es `queued`.
* Consultá el estado final con `GET /messages/:id`.

***

## POST /messages/batch

Encolar **hasta 100 mensajes** en una sola llamada. Pensado para envíos masivos (avisos, solicitudes de reseña, recordatorios). Cada item se acepta, saltea o rechaza **de forma independiente** — un destinatario inválido nunca tumba el lote.

**Scope requerido:** `messages:write` (el mismo que `POST /messages` — cualquier API key que ya envía mensajes sirve sin cambios).

Responde **`202 Accepted`** apenas persiste y encola; la entrega ocurre de forma asíncrona al ritmo de envío de tu cuenta. Los envíos masivos usan prioridad de campaña, así que nunca le quitan turno al chat en vivo.

### Body

| Campo                  | Tipo          | Requerido | Descripción                                                                                                                                                                                                                                   |
| ---------------------- | ------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messages`             | array         | Sí        | Hasta **100** items. Cada item tiene la misma forma que el body de `POST /messages` (`to`, `type`, `text`/`template`/`image`/…, `clientRef`), con `phoneNumberId` opcional.                                                                   |
| `defaultPhoneNumberId` | string (UUID) | No        | Canal aplicado a cada item que **no** trae su propio `phoneNumberId`.                                                                                                                                                                         |
| `respectOptOut`        | boolean       | No        | Default `true`. Cuando es `true`, los destinatarios en la lista de opt-out se devuelven como `result: "skipped", reason: "opted_out"` en vez de enviarse. Poné `false` solo para envíos transaccionales que el usuario espera explícitamente. |

<Note>
  El campo raíz para el canal compartido se llama **`defaultPhoneNumberId`** (no `phoneNumberId`). Por item, el campo sí es `phoneNumberId` y, si lo omitís, hereda el `defaultPhoneNumberId`.
</Note>

### Ejemplo — solicitudes de reseña

```bash theme={null}
curl -X POST https://api.waspytech.com/api/v2/messages/batch \
  -H "Authorization: Bearer wspy_..." \
  -H "Content-Type: application/json" \
  -d '{
    "defaultPhoneNumberId": "channel-uuid",
    "respectOptOut": true,
    "messages": [
      {
        "to": "+5491122334455",
        "type": "template",
        "template": {
          "name": "resena_maps",
          "language": { "code": "es_AR" },
          "components": [
            { "type": "body", "parameters": [{ "type": "text", "text": "Sofía" }] }
          ]
        },
        "clientRef": "resena_12345"
      }
    ]
  }'
```

### Response `202`

Trae un detalle **por item**, en el mismo orden en que los mandaste:

```json theme={null}
{
  "data": {
    "accepted": 1,
    "skipped": 0,
    "failed": 0,
    "items": [
      {
        "index": 0,
        "clientRef": "resena_12345",
        "result": "queued",
        "message": {
          "id": "msg-uuid",
          "status": "queued",
          "clientRef": "resena_12345",
          "type": "template",
          "waMessageId": null,
          "createdAt": "2026-06-16T21:00:00.000Z"
        }
      }
    ]
  },
  "meta": { "requestId": "..." }
}
```

Cada item del array `items` tiene uno de tres resultados:

| `result`  | Campos                         | Significado                                                                       |
| --------- | ------------------------------ | --------------------------------------------------------------------------------- |
| `queued`  | `message` (objeto del mensaje) | Aceptado y encolado. `message.id` es el UUID para polling.                        |
| `skipped` | `reason` (ej. `opted_out`)     | No se envió por una regla (opt-out).                                              |
| `failed`  | `error` (`{ code, message }`)  | Item inválido (ej. `INVALID_PHONE`, `INVALID_CHANNEL`, `MISSING_REQUIRED_FIELD`). |

### Reconciliación recomendada

Mandá `clientRef` con tu propio identificador (número de pedido, ID de solicitud) en cada item. Después suscribite al webhook [`message.status_changed`](/webhooks/events#message-status-changed): cada cambio de estado (`sent` → `delivered` → `read` o `failed`) trae tu `clientRef` de vuelta en `data.clientRef`, así matcheás los eventos contra tus registros **sin guardar ningún mapping**.

<Note>
  Para volúmenes mayores a 100, mandá varias requests (por ejemplo en tandas de 100). El rate limit de la API v2 aplica por request, no por mensaje — ver [Rate limits](/rate-limits).
</Note>

### Errores frecuentes

| Código             | Descripción                                           |
| ------------------ | ----------------------------------------------------- |
| `VALIDATION_ERROR` | El array `messages` está vacío o supera los 100 items |

Los errores **por item** (teléfono o canal inválido, campo faltante) no devuelven un código de error global: vienen dentro de `items[].error` con `result: "failed"`.

***

## GET /messages/:id

Consultar un mensaje por ID. Útil para verificar el estado de envío.

**Scope requerido:** `messages:read`

```bash theme={null}
curl https://api.waspytech.com/api/v2/messages/MSG_ID \
  -H "Authorization: Bearer wspy_..."
```

### Response `200`

```json theme={null}
{
  "data": {
    "id": "msg-uuid",
    "conversationId": "conv-uuid",
    "direction": "outbound",
    "type": "text",
    "content": { "text": "Hola! Tu pedido fue despachado." },
    "status": "delivered",
    "waMessageId": "wamid.HBgLNT...",
    "errorCode": null,
    "errorMessage": null,
    "createdAt": "2026-04-13T15:00:00.000Z"
  },
  "meta": { "requestId": "..." }
}
```

Para entender los estados posibles, ver [Estados de mensajes](/concepts/message-status).

***

## POST /messages/:waMessageId/read

Marcar un mensaje entrante como leído **por su `waMessageId`** (el mismo que llega en el webhook `message.received`). Conveniente cuando tenés a mano el `waMessageId` en lugar del `conversationId`.

**Scope requerido:** `conversations:write`

```bash theme={null}
curl -X POST https://api.waspytech.com/api/v2/messages/wamid.HBgLNT.../read \
  -H "Authorization: Bearer wspy_..."
```

Equivalente a `POST /conversations/:id/read` — manda el read receipt a Meta y resetea el `unreadCount` de la conversación padre.

```json theme={null}
{
  "data": {
    "conversationId": "conv-uuid",
    "markedMessageId": "wamid.HBgLNT...",
    "deliveredToMeta": true
  },
  "meta": { "requestId": "..." }
}
```

404 si el `waMessageId` no existe o no es inbound.
