# WhatsApp Gateway — DOCS HTTP API v1

Guía operativa para copiar y pegar. El gateway admite hasta tres sesiones aisladas. Cada sesión tiene su propio WhatsApp, QR, URL de webhook, secret HMAC, checklist de eventos e historial de entregas.

## 1. Variables para todos los ejemplos

Cambie únicamente `API_KEY` y, cuando corresponda, `SESSION_ID`:

```bash
export BASE_URL="https://35-253-122-129.sslip.io"
export API_KEY="REEMPLAZAR_POR_LA_API_KEY"
export SESSION_ID="main"
```

Sesiones actualmente previstas:

```text
main
device-2
device-3
```

Todos los endpoints protegidos reciben:

```bash
-H "x-api-key: $API_KEY"
```

`GET /health`, `GET /v1/health` y `GET /v1/readiness` son públicos.

## 2. Salud y sesiones

### Comprobar que el proceso responde

```bash
curl -sS "$BASE_URL/health" | jq
```

### Listar las tres sesiones

```bash
curl -sS "$BASE_URL/v1/sessions" \
  -H "x-api-key: $API_KEY" | jq
```

Estados posibles:

```text
STOPPED STARTING QR_READY SYNCING READY RESETTING
LOGGED_OUT DISCONNECTED AUTH_FAILURE ERROR
```

Sólo `READY` permite enviar mensajes.

### Crear una sesión

```bash
curl -sS -X POST "$BASE_URL/v1/sessions" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -d '{"id":"ventas"}' | jq
```

Los IDs admiten letras, números, `_` y `-`. `MAX_SESSIONS=3` controla el límite.

### Iniciar una sesión

```bash
curl -sS -X POST "$BASE_URL/v1/sessions/$SESSION_ID/start" \
  -H "x-api-key: $API_KEY" | jq
```

La operación es idempotente: HTTP 202 si inicia y HTTP 200 si ya estaba ejecutándose.

### Obtener el QR

```bash
curl -sS "$BASE_URL/v1/sessions/$SESSION_ID/qr" \
  -H "x-api-key: $API_KEY" | jq
```

Respuesta cuando existe QR:

```json
{
  "status": "qr",
  "data": "data:image/png;base64,...",
  "version": 3,
  "issuedAt": "2026-08-09T15:00:00.000Z"
}
```

Al escanear, el gateway elimina el QR y cambia a `SYNCING`. Al terminar cambia a `READY`. Si WhatsApp llega al 100% pero omite el evento final, el gateway sondea el navegador y reinyecta una vez antes de reiniciar. Un timeout de `READY` nunca elimina la vinculación: las credenciales sólo se borran ante `LOGOUT`, `auth_failure` o una renovación solicitada con `confirm=true`.

### Renovar una vinculación

Esta operación cierra la vinculación y elimina únicamente las credenciales de `SESSION_ID`:

```bash
curl -sS -X POST "$BASE_URL/v1/sessions/$SESSION_ID/qr/refresh" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -d '{"confirm":true}' | jq
```

### Eliminar una sesión secundaria

```bash
curl -sS -X DELETE "$BASE_URL/v1/sessions/device-3" \
  -H "x-api-key: $API_KEY" | jq
```

`main` no se elimina; para ella se usa la renovación de QR.

## 3. Enviar mensajes

Use una `Idempotency-Key` diferente para cada operación real. Repetir la misma clave con el mismo body devuelve el resultado guardado sin enviar dos veces.

### Texto desde la sesión seleccionada

```bash
export SESSION_ID="main"
export DESTINATION="573502168807"

curl -sS -X POST "$BASE_URL/v1/sessions/$SESSION_ID/messages/send" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: test-$SESSION_ID-$(date +%s)" \
  -d "{\"to\":\"$DESTINATION\",\"text\":\"Hola desde $SESSION_ID\"}" | jq
```

El número debe incluir código de país, sin `+`, espacios ni guiones.

### Texto usando el endpoint general

```bash
curl -sS -X POST "$BASE_URL/v1/messages/send" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: order-42" \
  -d '{
    "sessionId":"device-2",
    "to":"573502168807",
    "text":"Hola desde device-2"
  }' | jq
```

También se puede seleccionar la cuenta con `x-session-id`. Si ruta y body contienen sesiones diferentes, la API responde HTTP 400.

### Imagen o archivo desde una URL

```bash
curl -sS -X POST "$BASE_URL/v1/sessions/$SESSION_ID/messages/send" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: media-$SESSION_ID-$(date +%s)" \
  -d '{
    "to":"573502168807",
    "mediaUrl":"https://example.com/image.jpg",
    "caption":"Archivo enviado por la API"
  }' | jq
```

### Archivo en base64

```bash
curl -sS -X POST "$BASE_URL/v1/sessions/$SESSION_ID/messages/send" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: base64-$SESSION_ID-$(date +%s)" \
  -d '{
    "to":"573502168807",
    "mimeType":"image/png",
    "fileName":"foto.png",
    "mediaBase64":"iVBORw0KGgoAAA...",
    "caption":"Foto"
  }' | jq
```

### Consultar el resultado de un envío

```bash
curl -sS "$BASE_URL/v1/messages/COMMAND_ID" \
  -H "x-api-key: $API_KEY" | jq
```

Si el proceso cae mientras WhatsApp aún no confirmó un envío, se registra `UNCERTAIN` y no se repite automáticamente.

### PDF en base64 desde Make

En **HTTP > Make a request**, use método `POST`, tipo de cuerpo
`application/json` y esta URL:

```text
https://35-253-122-129.sslip.io/v1/sessions/main/messages/send
```

Cabeceras:

```text
x-api-key: REEMPLAZAR_POR_LA_API_KEY
Idempotency-Key: pdf-ID_UNICO_DE_LA_OPERACION
```

Cuerpo JSON:

```json
{
  "to": "573502168807",
  "mimeType": "application/pdf",
  "fileName": "documento.pdf",
  "mediaBase64": "{{toString(DATA_DEL_ARCHIVO; base64)}}",
  "caption": "Documento enviado desde Make"
}
```

`DATA_DEL_ARCHIVO` debe mapear el campo binario `Data` del módulo que descargó
el PDF. Si el módulo anterior ya devuelve texto base64, mapéelo directamente sin
aplicar `toString` otra vez. Para `device-2` o `device-3`, cambie únicamente el ID
de sesión dentro de la URL.

## 4. Un webhook independiente por sesión

Todas estas rutas están bajo:

```text
/v1/sessions/:sessionId/webhook
```

Cambiar el webhook de `device-2` no modifica ni cancela entregas de `main` o `device-3`.

### Consultar la configuración de una sesión

El secret nunca se devuelve; `hasSecret` indica si existe:

```bash
curl -sS "$BASE_URL/v1/sessions/$SESSION_ID/webhook" \
  -H "x-api-key: $API_KEY" | jq
```

### Consultar el catálogo/checklist de eventos

```bash
curl -sS "$BASE_URL/v1/sessions/$SESSION_ID/webhook/events" \
  -H "x-api-key: $API_KEY" | jq
```

### Configurar URL, secret y eventos de `main`

```bash
export SESSION_ID="main"

curl -sS -X PUT "$BASE_URL/v1/sessions/$SESSION_ID/webhook" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "url":"https://tu-dominio.com/webhooks/whatsapp-main",
    "secret":"CAMBIAR_POR_UN_SECRET_LARGO_Y_ALEATORIO",
    "active":true,
    "events":[
      "message.received",
      "message.ack",
      "message.reaction",
      "call.received",
      "session.ready",
      "session.disconnected",
      "session.logged_out"
    ]
  }' | jq
```

### Configurar otro webhook para `device-2`

```bash
export SESSION_ID="device-2"

curl -sS -X PUT "$BASE_URL/v1/sessions/$SESSION_ID/webhook" \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "url":"https://tu-dominio.com/webhooks/whatsapp-device-2",
    "secret":"OTRO_SECRET_INDEPENDIENTE",
    "active":true,
    "events":["message.received","session.ready","session.disconnected"]
  }' | jq
```

Detalles del `PUT`:

- `events` acepta eventos exactos, categorías como `message.*`, o `*`.
- Omitir `secret` conserva el secret actual.
- Enviar `"secret":""` elimina el secret.
- Un webhook activo necesita URL y al menos un evento.
- Al cambiar la configuración sólo se cancelan pendientes de esa misma sesión y revisión anterior.

### Probar el webhook seleccionado

```bash
curl -sS -X POST "$BASE_URL/v1/sessions/$SESSION_ID/webhook/test" \
  -H "x-api-key: $API_KEY" | jq
```

### Desactivar y borrar sólo ese webhook

```bash
curl -sS -X DELETE "$BASE_URL/v1/sessions/$SESSION_ID/webhook" \
  -H "x-api-key: $API_KEY" | jq
```

### Listar entregas de una sesión

```bash
curl -sS "$BASE_URL/v1/sessions/$SESSION_ID/webhook/deliveries?limit=40" \
  -H "x-api-key: $API_KEY" | jq
```

Filtrar entregas fallidas de mensajes recibidos:

```bash
curl -sS "$BASE_URL/v1/sessions/$SESSION_ID/webhook/deliveries?status=DEAD&eventType=message.received&limit=100" \
  -H "x-api-key: $API_KEY" | jq
```

### Ver una entrega con su evento

```bash
export DELIVERY_ID="REEMPLAZAR"

curl -sS "$BASE_URL/v1/sessions/$SESSION_ID/webhook/deliveries/$DELIVERY_ID?includeEvent=true" \
  -H "x-api-key: $API_KEY" | jq
```

### Reenviar una entrega terminal

```bash
curl -sS -X POST "$BASE_URL/v1/sessions/$SESSION_ID/webhook/deliveries/$DELIVERY_ID/replay" \
  -H "x-api-key: $API_KEY" | jq
```

Sólo se reenvían entregas de la misma sesión y con el webhook actual activo.

Las rutas antiguas `/v1/webhook/*` siguen disponibles, pero son alias exclusivos de `main`.

## 5. Payload y cabeceras del webhook

Body recibido:

```json
{
  "eventId": "01234567-89ab-cdef-0123-456789abcdef",
  "eventType": "message.received",
  "sessionId": "device-2",
  "occurredAt": "2026-08-09T15:00:00.000Z",
  "data": {
    "from": "573502168807@c.us",
    "body": "Hola"
  }
}
```

Cabeceras:

```text
Content-Type: application/json
X-Webhook-Event: message.received
X-Webhook-Session: device-2
X-Event-Id: <uuid>
X-Delivery-Id: <uuid>
X-Delivery-Attempt: 1
X-Webhook-Signature: sha256=<64 caracteres hex>
```

`X-Webhook-Signature` sólo aparece si esa sesión tiene secret.

## 6. Verificar el secret HMAC correctamente

La firma se calcula así:

```text
sha256=HEX(HMAC_SHA256(secret_de_la_sesion, bytes_exactos_del_body_HTTP))
```

Importante: valide el body crudo recibido. No haga `JSON.parse` y luego `JSON.stringify` antes de calcular la firma, porque espacios u orden de propiedades podrían cambiar los bytes.

### Receptor Node.js/Express completo

```bash
mkdir webhook-receiver
cd webhook-receiver
npm init -y
npm install express
```

Guarde como `server.js`:

```js
const crypto = require('crypto');
const express = require('express');

const app = express();
const secret = process.env.WEBHOOK_SECRET;

// Esta ruta debe usar express.raw ANTES de cualquier express.json global.
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
  const received = req.get('X-Webhook-Signature') || '';
  const expected = `sha256=${crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex')}`;

  const receivedBuffer = Buffer.from(received, 'utf8');
  const expectedBuffer = Buffer.from(expected, 'utf8');
  const valid = receivedBuffer.length === expectedBuffer.length
    && crypto.timingSafeEqual(receivedBuffer, expectedBuffer);

  if (!valid) return res.status(401).json({ error: 'invalid_signature' });

  const event = JSON.parse(req.body.toString('utf8'));
  console.log({
    sessionId: event.sessionId,
    eventType: event.eventType,
    eventId: event.eventId,
  });

  // Responder 2xx rápidamente; procese tareas lentas en segundo plano.
  return res.sendStatus(204);
});

app.listen(3000, () => console.log('Webhook receptor en http://localhost:3000'));
```

Ejecutar:

```bash
export WEBHOOK_SECRET="EL_MISMO_SECRET_CONFIGURADO_EN_LA_SESION"
node server.js
```

La comparación usa `timingSafeEqual` para evitar filtraciones por tiempo. Cada sesión puede usar un secret distinto.

## 7. Reintentos y respuestas HTTP

- HTTP `2xx`: entrega exitosa.
- Error de red, `408`, `425`, `429` o `5xx`: reintento exponencial con jitter.
- Otros `4xx`: fallo definitivo.
- La cola SQLite/WAL sobrevive reinicios.
- Una entrega interrumpida vuelve como `RETRYING`.
- `PENDING`, `PROCESSING`, `RETRYING`, `SUCCESS`, `DEAD` y `CANCELED` son estados posibles.

## 8. Eventos disponibles

Mensajes:

```text
message.received message.created message.sent message.failed
message.ack message.edited message.revoked message.reaction media.uploaded
```

Sesión:

```text
session.created session.deleted session.start_requested session.qr
session.qr_refresh_requested session.authenticated session.sync_progress
session.ready session.state_changed session.disconnected session.logged_out
session.auth_failure session.error session.stopped
```

Otros:

```text
call.received
group.join group.leave group.updated group.admin_changed group.membership_request
contact.changed chat.archived chat.removed poll.vote
```

Los adjuntos sólo se descargan cuando `message.received` está activado en el webhook de esa sesión y `WEBHOOK_INCLUDE_MEDIA=true`.

## 9. Diagnóstico

```bash
curl -sS "$BASE_URL/v1/events?limit=120" \
  -H "x-api-key: $API_KEY" | jq
```

El estado de una sesión incluye progreso y diagnóstico de sincronización. Los logs en disco sanitizan secretos y contenido base64.
