Files
skill-whatsapp/endpoints.md

8.3 KiB

whatsapp-bridge — referencia rápida de endpoints

Base URL: https://whatsapp.nucleoriofrio.com Auth: Authorization: Bearer $WA_BRIDGE_TOKEN (excepto /healthz)

Diagnóstico

Método Ruta Respuesta
GET /healthz { ok: true, ts }
GET /api/auth/status { status, hasSession, user, pairingCode, lastError }

statusidle | connecting | open | closed | needs-pair.

Auth / pairing

Método Ruta Body / Headers
POST /api/auth/pair — → { code, hint }
POST /api/auth/logout Header X-Confirm-Wipe: yes (destructivo)

Chats

GET /api/chats
  ?type=group|individual|all        (default all)
  &q=<texto>                        (busca en name + jid, ILIKE)
  &limit=<1..500>                   (default 100)
  &offset=<int>                     (default 0)

Respuesta:

{
  "count": N,
  "chats": [
    { "jid": "...@g.us", "isGroup": true, "name": "...", "lastMessageAt": "ISO", "participants": 42 }
  ]
}

Grupo específico

GET /api/groups/:jid            (URL-encode el jid, termina en @g.us)

Respuesta: { jid, stored: {…}, live: GroupMetadata | null }.

GET /api/groups/:jid/messages
  ?since=<ISO>                    (timestamp >=)
  &until=<ISO>                    (timestamp <=)
  &q=<texto>                      (ILIKE en content)
  &sender=<jid>                   (filtra por participant)
  &order=asc|desc                 (default desc)
  &limit=<1..1000>                (default 100)
  &offset=<int>

Respuesta:

{
  "jid": "...@g.us",
  "count": N,
  "filters": { ... },
  "messages": [
    {
      "msgId": "...",
      "fromMe": false,
      "senderJid": "...@s.whatsapp.net",
      "pushName": "Don Fulano",
      "messageType": "extendedTextMessage",
      "content": "texto extraído",
      "contextInfo": { "quotedMsgId": "...", "mentionedJid": ["..."] } | null,
      "timestamp": "ISO",
      "media": null  |  {
        "type": "image"|"video"|"audio"|"document"|"sticker",
        "mimetype": "image/jpeg",
        "size": 12345,
        "fileName": "... (solo document)",
        "caption": "... (image/video/document)",
        "durationSec": 30 (video/audio),
        "ptt": true (solo audio = voice note),
        "width": 800 (image/video/sticker),
        "height": 600,
        "pages": 5 (solo document),
        "isAnimated": true (solo sticker),
        "downloadUrl": "/api/messages/media/<enc-chatJid>/<msgId>"
      }
    }
  ]
}

El campo media aparece en cada mensaje que contiene archivo; downloadUrl es la ruta relativa al endpoint que descarga + desencripta.

Mensajes individuales

GET /api/messages/individual/:jid       (termina en @s.whatsapp.net)
  ?since=&until=&q=&order=&limit=&offset=

Búsqueda cross-chat

GET /api/messages/search
  ?q=<texto>                      (requerido, min 1 char, ILIKE)
  &since=<ISO>
  &until=<ISO>
  &chatType=group|individual|all  (default all)
  &limit=<1..500>                 (default 100)

Respuesta:

{
  "query": "quintal",
  "count": N,
  "matches": [
    { "chatJid": "...", "msgId": "...", "senderJid": "...", "pushName": "...", "isGroup": true, "content": "...", "timestamp": "ISO" }
  ]
}

Envío de texto (gated por ENABLE_SEND)

POST /api/messages/send
Content-Type: application/json

{
  "jid": "...@g.us" | "...@s.whatsapp.net" | "...@lid",
  "text": "...",
  "quotedChatJid": "...",   // opcional; default = mismo jid destino
  "quotedMsgId": "...",     // opcional; responder a mensaje previo
  "mentions": ["jid1", ...] // opcional; notifica a esos JIDs
}

Respuesta: { ok: true, msgId: "..." } o 403 si ENABLE_SEND=false.

Reenvío (forward)

POST /api/messages/forward

{
  "sourceChatJid": "...",   // chat donde está el mensaje original
  "sourceMsgId": "...",     // id del mensaje
  "destJid": "..."          // destino
}

Requiere que el mensaje esté en la DB del bridge (escuchado mientras el socket estaba vivo).

Reacción (emoji)

POST /api/messages/react

{
  "chatJid": "...",
  "msgId": "...",
  "emoji": "👍"             // vacío "" = quitar reacción
}

Envío de multimedia

POST /api/messages/send-media

{
  "jid": "...",
  "url": "https://...",     // URL pública del archivo
  "type": "image" | "video" | "audio" | "document",
  "caption": "...",         // opcional (solo image/video/document)
  "fileName": "...",        // opcional (solo document)
  "mimetype": "...",        // opcional (override del content-type)
  "ptt": false,             // solo audio: true = voice note
  "quotedChatJid": "...",   // opcional
  "quotedMsgId": "..."      // opcional
}

Límites y guards:

  • URL debe ser HTTP/HTTPS pública — localhost, 127.0.0.1, 10.x, 192.168.x, 172.16-31.x son bloqueadas (SSRF protection).
  • Tamaño máximo ~100 MB (aunque WhatsApp limita más bajo según tipo).
  • Timeout del fetch: 60s.

Envío de ubicación

POST /api/messages/send-location

{
  "jid": "...",
  "latitude": 14.123,
  "longitude": -88.456,
  "name": "...",            // opcional (ej: "Beneficio Rio Frio")
  "address": "...",         // opcional (ej: "Santa Bárbara, Honduras")
  "quotedChatJid": "...",
  "quotedMsgId": "..."
}

Envío de encuesta (poll)

POST /api/messages/send-poll

{
  "jid": "...",
  "name": "Pregunta",
  "values": ["Opción A", "Opción B", "Opción C"],   // 2-12 opciones
  "selectableCount": 1,    // 1=single (default), 0=multi-choice, N=hasta N
  "quotedChatJid": "...",
  "quotedMsgId": "..."
}

Envío de contacto(s) (vCard)

POST /api/messages/send-contact

{
  "jid": "...",
  "contacts": [
    {
      "displayName": "Don Fulano",
      "firstName": "Fulano",       // opcional
      "lastName": "Pérez",         // opcional
      "phone": "50499999999",      // opcional, E.164 sin + → vinculable a WA
      "phoneDisplay": "+504 9999-9999",  // opcional, default = "+" + phone
      "email": "...",              // opcional
      "organization": "...",       // opcional
      "title": "...",              // opcional
      "vcard": "BEGIN:VCARD..."    // opcional, override total
    }
  ],
  "quotedChatJid": "...",
  "quotedMsgId": "..."
}

Hasta 20 contactos por mensaje. Si phone está en formato E.164 (sin +), WhatsApp lo detecta y permite iniciar chat con un tap.

Descarga de media (desencriptado server-side)

Los mensajes con media incluyen media.downloadUrl en las respuestas. Ese path apunta a un endpoint que, al llamarlo con el bearer, descarga el archivo del CDN de WhatsApp, lo desencripta con la mediaKey del mensaje, y devuelve el binario listo para usar.

GET /api/messages/media/:chatJid/:msgId[?disposition=inline|attachment]

Headers de respuesta:

  • Content-Type: MIME correcto (image/jpeg, video/mp4, etc.)
  • Content-Length: tamaño en bytes
  • Content-Disposition: inline (default) o attachment
  • X-Wa-Media-Type: image|video|audio|document|sticker

Solo metadata (sin bajar el binario):

GET /api/messages/media-info/:chatJid/:msgId
→ { chatJid, msgId, media: { type, mimetype, size, fileName, caption, ..., downloadUrl } }

Errores:

  • 404 si el mensaje no existe o no tiene media
  • 503 si el socket de WhatsApp está caído
  • 500 si el CDN de WA ya no tiene el archivo (muy viejo → no se puede recuperar)

Eventos (no soportado todavía)

Baileys 6.17 no expone una API estable para crear event messages (los puede recibir/parsear pero no enviar via sendMessage). Sería necesario construir proto.Message.IEventMessage manualmente y usar relayMessage low-level — todavía no implementado en el bridge. Si lo necesitás urgente, abrir issue.

Errores típicos: 400 con fetch failed: ... si la URL no responde bien o es privada.

Formatos de JID

JID Tipo Ejemplo
<phone>@s.whatsapp.net chat individual 50499999999@s.whatsapp.net
<phone>-<timestamp>@g.us grupo 50499999999-1614182736@g.us
status@broadcast estados (ignorado por el bridge)

URL-encoding en path params: @%40, . se deja tal cual.

Códigos de error

Status Significado
400 parámetros inválidos (ver body con .error)
401 bearer ausente o inválido
403 endpoint gated (ej send cuando ENABLE_SEND=false)
404 chat/grupo no existe en storage
500 error interno (revisar logs del contenedor)
503 socket no listo (status != open)