WhatsApp Messaging API

Servicio HTTP para enviar y leer mensajes de WhatsApp desde tu propia integración — Odoo, n8n, Zapier, scripts a medida o cualquier cosa que pueda hacer un POST.

Base URL: https://messages.odoo.blue Content-Type: application/json UTF-8

Cómo empezar

El acceso a esta API se otorga por token. Sigue estos pasos para empezar a enviar y recibir mensajes desde tu número de WhatsApp:

  1. 1

    Adquiere tu token

    Ve a exdoo.mx y contrata un plan. Una vez completada la compra, Exdoo te entregará por correo:

    • Tu token personal (cadena de ~32 caracteres). No se vuelve a mostrar; guárdalo bien.
    • Un link de onboarding personalizado: https://messages.odoo.blue/onboard?token=…

    Nota: la página de compra en exdoo.mx está en construcción. Mientras tanto, escribe a Exdoo para obtener tu token manualmente.

  2. 2

    Abre tu link de onboarding

    Abre el link que te envió Exdoo en tu navegador (computadora o celular). Verás una página con un código QR.

  3. 3

    Vincula tu WhatsApp con el QR

    Funciona tanto con WhatsApp como con WhatsApp Business — el flujo es idéntico. En tu teléfono:

    1. Abre WhatsApp o WhatsApp Business.
    2. Toca el menú (Android) o Configuración (iPhone).
    3. Selecciona Dispositivos vinculados → Vincular un dispositivo.
    4. Apunta la cámara al código QR de la página.
    ¿Cuál usar? Si esperas mandar muchos mensajes al día (campañas, notificaciones masivas, atención a varios clientes), te recomendamos WhatsApp Business — está diseñado para uso comercial y reduce el riesgo de que WhatsApp marque tu número. Para uso ligero o personal, WhatsApp normal funciona igual.
  4. 4

    Espera la confirmación

    La página detecta el escaneo en pocos segundos y muestra ✅ WhatsApp conectado junto con tu número. A partir de ese momento, tu cuenta queda enlazada.

  5. 5

    ¡Ya puedes usar la API!

    Con tu token en mano, ya puedes hacer requests HTTP contra los endpoints documentados aquí abajo. Empieza por algo simple — un mensaje de prueba:

    curl -X POST https://messages.odoo.blue/v1/whatsapp/send-text \
      -H "Content-Type: application/json" \
      -d '{
        "token": "<TU_TOKEN>",
        "to": "521XXXXXXXXXX",
        "text": "¡Hola desde la API!"
      }'

    El número to es E.164 sin + (en México: 521 + 10 dígitos).

¿Perdiste tu token o tu número se desconectó? Escribe a Exdoo. Pueden revocar/reemitir tu token, y si solo necesitas reescanear el QR (por ejemplo cambiaste de celular), tu link de onboarding sigue funcionando — solo vuelve a abrirlo.

Autenticación

Todos los endpoints (excepto GET /health) requieren un campo token en el body JSON. El token resuelve la sesión a usar.

POST /v1/whatsapp/send HTTP/1.1
Host: messages.odoo.blue
Content-Type: application/json
X-API-Key: <opcional>

{ "token": "<TU_TOKEN>", ... }

Si la variable de entorno API_KEY está configurada en el servidor, hay que mandar además el header X-API-Key con su valor. Si no está configurada, ese header se ignora.

Rate limits

ConceptoValor
Rate limit30 requests/minuto por sesión (compartido entre envío y lectura)
Timeout upstream30 segundos
Reintentos en errores transitorios3 con backoff exponencial (1s → 2s → 4s)
Nota: el rate limit es por sesión, no por endpoint. Si una sesión envía 30 mensajes en un minuto, también queda bloqueada para listar chats o descargar media hasta que pase el minuto.

Errores

Forma general de un error:

{ "ok": false, "error": "descripción del problema" }
HTTPCausa
400JSON inválido, campo requerido faltante, formato inválido (teléfono, chat_id, message_id, mimetype no permitido).
401token inválido o X-API-Key incorrecta.
404Mensaje no existe (no se encontró en el chat indicado).
429Rate limit excedido para la sesión.
502El proveedor de mensajería no respondió o devolvió un error inesperado.

Conectar / arrancar sesión

POST /v1/sessions/connect

Arranca la sesión del cliente del lado del proveedor. Después de llamar esto, el estado se mueve a SCAN_QR_CODE y se puede pedir el QR con /sessions/qr. Idempotente: si ya está arrancando o funcionando, no falla.

Request

{ "token": "<TU_TOKEN>" }

Response 200

{
  "ok": true,
  "status": "SCAN_QR_CODE"
}

Estado de la sesión

POST /v1/sessions/status

Pollea esto durante el onboarding (cada 3-5 s) hasta que status === "WORKING". Cuando esté WORKING devuelve también el número conectado.

Request

{ "token": "<TU_TOKEN>" }

Response 200

{
  "ok": true,
  "status": "WORKING",
  "wa_number": "5215555555555",
  "push_name": "Maria - Acme"
}

Valores de status

StatusSignificado
STARTINGInicializando, espera 1-2 s.
SCAN_QR_CODEListo para escanear; usa /qr.
WORKINGConectado; ya puede enviar/leer.
FAILEDFalla del proveedor; reintenta /connect.
STOPPEDDetenida; llama a /connect para arrancar otra vez.

Obtener QR

POST /v1/sessions/qr

Devuelve el QR como PNG codificado en base64. Solo funciona cuando status es SCAN_QR_CODE. El QR rota aproximadamente cada 20 segundos — refrescarlo con esa cadencia.

Request

{ "token": "<TU_TOKEN>" }

Response 200

{
  "ok": true,
  "format": "png",
  "qr_b64": "iVBORw0KGgoAAAANSUhEUg..."
}

Errores

HTTPCaso
409La sesión no está en SCAN_QR_CODE (consulta /status primero).
404Sesión no existe en el proveedor (llama /connect primero).

Mostrar en HTML

const r = await fetch("/v1/sessions/qr", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ token })
});
const data = await r.json();
img.src = "data:image/png;base64," + data.qr_b64;

Cerrar sesión

POST /v1/sessions/logout

Desvincula el número de WhatsApp de la sesión. El token sigue siendo válido — el cliente puede reescanear su QR llamando a /connect + /qr de nuevo.

Request

{ "token": "<TU_TOKEN>" }

Response 200

{ "ok": true }

Página de onboarding

GET /onboard?token=<TOKEN>

Página web lista para usar que el cliente abre en su navegador. Muestra el QR, lo refresca automáticamente, polea el estado y muestra confirmación con el número conectado al terminar. No requiere que el cliente integre nada — solo abre el link.

Si abren /onboard sin ?token=, la página les pide pegar su token en un campo. El admin típicamente entrega un link directo con el token incrustado.

Flujo típico:
  1. Tu administrador del servicio te entrega un token + un link de onboarding.
  2. Abres el link en tu navegador y escaneas el QR con tu WhatsApp.
  3. Cuando la página muestra ✅, ya puedes usar los endpoints de envío/lectura con tu token.

Enviar texto y/o adjuntos

POST /v1/whatsapp/send

Endpoint principal de envío. Acepta un mensaje con plantilla ({{vars}}) y 0..N adjuntos. Si attachments está vacío o ausente, envía solo texto. Si hay adjuntos, el texto se manda como caption del primer adjunto.

Request

{
  "token": "<TU_TOKEN>",
  "to": "5215555555555",
  "message": {
    "template": "Hola {{nombre}}, tu factura {{folio}} está lista.",
    "vars": { "nombre": "Ana", "folio": "F-001" }
  },
  "attachments": [
    {
      "filename": "factura.pdf",
      "mimetype": "application/pdf",
      "content_b64": "JVBERi0xLjQK..."
    }
  ]
}
CampoTipoRequeridoNotas
tokenstringResuelve la sesión.
tostringE.164 sin + (10–15 dígitos). Acepta también con espacios/guiones — se normaliza.
message.templatestringTexto con variables {{nombre}}. Si no usas vars, manda el texto plano aquí.
message.varsobjectnoReemplazos para la plantilla.
attachmentsarraynoLista de adjuntos. Omitir o lista vacía = solo texto.
attachments[].filenamestringsí*Si la extensión no coincide con el mimetype, se agrega automáticamente.
attachments[].mimetypestringsí*Ver mimetypes permitidos abajo.
attachments[].content_b64stringsí*Base64 estándar. Acepta también data URLs (data:...;base64,).

Mimetypes permitidos

application/pdf · application/xml · text/xml · image/jpeg · image/png · image/gif · image/webp

Tip: las imágenes se envían como image (con preview inline en WhatsApp). Los PDF/XML como document.

Ejemplo curl

curl -X POST https://messages.odoo.blue/v1/whatsapp/send \
  -H "Content-Type: application/json" \
  -d '{
    "token": "<TU_TOKEN>",
    "to": "5215555555555",
    "message": {
      "template": "Hola {{nombre}}",
      "vars": { "nombre": "Ana" }
    },
    "attachments": []
  }'

Response 200 (solo texto)

{
  "ok": true,
  "mode": "text",
  "upstream": { /* respuesta cruda del proveedor (incluye id del mensaje) */ }
}

Response 200 (con adjuntos)

{
  "ok": true,
  "mode": "attachments",
  "count": 1,
  "results": [
    { "filename": "factura.pdf", "upstream": { /* ... */ } }
  ]
}

Enviar solo texto

POST /v1/whatsapp/send-text

Versión simplificada para mensajes de texto plano. No usa plantillas — el texto se manda tal cual.

Request

{
  "token": "<TU_TOKEN>",
  "to": "5215555555555",
  "text": "Mensaje sin variables, tal cual."
}

Ejemplo curl

curl -X POST https://messages.odoo.blue/v1/whatsapp/send-text \
  -H "Content-Type: application/json" \
  -d '{
    "token": "<TU_TOKEN>",
    "to": "5215555555555",
    "text": "Hola, prueba"
  }'

Response 200

{
  "ok": true,
  "mode": "text",
  "upstream": { /* respuesta cruda del proveedor */ }
}

Enviar un documento (legacy)

POST /v1/whatsapp/send-document
Legacy: mantenido por compatibilidad con integraciones anteriores. Internamente delega en /v1/whatsapp/send. Para código nuevo, usa /send directamente — soporta múltiples adjuntos.

Request

{
  "token": "<TU_TOKEN>",
  "to": "5215555555555",
  "message": {
    "template": "Aquí está tu factura {{folio}}.",
    "vars": { "folio": "F-001" }
  },
  "document": {
    "filename": "factura.pdf",
    "mimetype": "application/pdf",
    "content_b64": "JVBERi0xLjQK..."
  }
}

document.mimetype es opcional y por defecto es application/pdf.

Listar chats

POST /v1/whatsapp/chats

Devuelve los chats de la sesión, ordenados por última actividad (igual que la UI de WhatsApp). Ideal para construir un sidebar tipo WhatsApp Web.

Request

{
  "token": "<TU_TOKEN>",
  "limit": 50,
  "offset": 0
}
CampoTipoDefaultNotas
tokenstringRequerido.
limitint50Entre 1 y 200.
offsetint0Para paginar.

Response 200

{
  "ok": true,
  "limit": 50,
  "offset": 0,
  "chats": [
    {
      "chat_id": "5217772544205@c.us",
      "kind": "individual",
      "name": "Contabilidad Sambol",
      "phone": "5217772544205",
      "picture": "https://pps.whatsapp.net/...",
      "last_message": {
        "id": "true_226989390713028@lid_3EB02BD15B06950CB036BD",
        "timestamp": 1778624266,
        "from_me": true,
        "type": "text",
        "preview": "Natalia, ya notifiqué..."
      }
    }
  ]
}

Detalle de campos

CampoNotas
chat_idOpaco. Úsalo tal cual al pedir mensajes; no lo parsees.
kind"individual" o "group".
phoneSólo en chats individuales (10–15 dígitos). En grupos es null.
pictureURL de WhatsApp CDN. Puede ser null. Las URLs tienen TTL — no las guardes a largo plazo.
last_message.type"text", "image", "audio", "video", "document" u "other".
last_message.previewRecortado a 200 chars. Para media: [imagen], [audio], [video], [documento], [adjunto].
last_message.timestampUnix epoch en segundos.

Mensajes de un chat

POST /v1/whatsapp/chats/messages

Mensajes de un chat específico, ordenados por timestamp descendente (más reciente primero). No incluye los bytes de los adjuntos — para eso usa /chats/media.

Request

{
  "token": "<TU_TOKEN>",
  "chat_id": "5219999968561@c.us",
  "limit": 50,
  "offset": 0
}
CampoTipoDefaultNotas
tokenstringRequerido.
chat_idstringRequerido. Tal cual lo devolvió /chats.
limitint50Entre 1 y 200.
offsetint0Para paginar (scroll hacia atrás).

Response 200

{
  "ok": true,
  "chat_id": "5219999968561@c.us",
  "limit": 50,
  "offset": 0,
  "has_more": true,
  "messages": [
    {
      "id": "false_..._AC52E349...",
      "timestamp": 1778622898,
      "from_me": false,
      "author": null,
      "type": "text",
      "text": "Listo, ya pude aclararlo.",
      "media": null
    }
  ]
}

Detalle de campos

CampoNotas
idIdentificador del mensaje. Pásalo como message_id al endpoint de media.
from_metrue si la sesión es la que envió el mensaje.
authorEn grupos: número del miembro que envió. null en individuales.
type"text", "image", "audio", "video", "document" u "other".
textCuerpo o caption. null si es media sin texto.
medianull en texto puro. Si hay adjunto, media.media_id = lo que va a /chats/media.
has_moretrue si quedan más mensajes hacia atrás (incrementa offset).

Paginación (scroll infinito)

  1. Primera llamada: offset=0, limit=50.
  2. Al hacer scroll hacia arriba: offset=50, limit=50. Etc.
  3. Detener cuando has_more == false.

Descargar adjunto

POST /v1/whatsapp/chats/media

Descarga el archivo adjunto (imagen, audio, video, documento) de un mensaje específico y lo devuelve como base64. Pensado para llamar bajo demanda — sólo cuando el usuario abre el adjunto en la UI.

Request

{
  "token": "<TU_TOKEN>",
  "chat_id": "5219999968561@c.us",
  "message_id": "false_..._AC52E349..."
}
CampoTipoNotas
tokenstringRequerido.
chat_idstringRequerido. El chat al que pertenece el mensaje.
message_idstringRequerido. Debe empezar con true_ o false_.

Response 200

{
  "ok": true,
  "filename": "AC52E349153505DE4FFCA863C11B8BAC.jpeg",
  "mimetype": "image/jpeg",
  "size": 37701,
  "content_b64": "/9j/4AAQSkZJRgABAQAAAQABAAD..."
}

Errores específicos

HTTPCaso
400message_id mal formado o el mensaje no tiene media (es texto puro).
404chat_id+message_id no corresponde a ningún mensaje.
502No se pudo descargar el archivo del proveedor upstream.

Decodificar a archivo (Python)

import base64, requests

r = requests.post(
    "https://messages.odoo.blue/v1/whatsapp/chats/media",
    json={
        "token": "<TU_TOKEN>",
        "chat_id": "5219999968561@c.us",
        "message_id": "false_..._AC52E349...",
    },
)
data = r.json()
with open(data["filename"], "wb") as f:
    f.write(base64.b64decode(data["content_b64"]))

Guardar como ir.attachment (Odoo)

attachment = self.env["ir.attachment"].create({
    "name": data["filename"],
    "datas": data["content_b64"],     # Odoo guarda base64 directo
    "mimetype": data["mimetype"],
    "res_model": "whatsapp.message",
    "res_id": record.id,
})

Health check

GET /health

Verifica que el servicio responda y que pueda alcanzar el proveedor upstream. No requiere autenticación.

Ejemplo

curl https://messages.odoo.blue/health

Response 200

{
  "ok": true,
  "service": "exdoo-messaging",
  "upstream_reachable": true,
  "sessions_count": 4
}

Si el upstream no está alcanzable, devuelve 503 con "upstream_reachable": false.

Flujo típico — UI estilo WhatsApp Web

  1. Al cargar la app: POST /v1/whatsapp/chats → llenar sidebar de conversaciones.
  2. Al clickear un chat: POST /v1/whatsapp/chats/messages con su chat_id → panel de mensajes.
  3. Al scrollear hacia arriba: misma llamada con offset += limit hasta has_more=false.
  4. Al clickear un adjunto: POST /v1/whatsapp/chats/media con chat_id + message_id.
  5. Al enviar: POST /v1/whatsapp/send con texto y/o adjuntos.
Recordatorios:
  • Estos endpoints son de lectura — no marcan como leído en WhatsApp.
  • El rate limit (30/min) es por sesión y compartido entre todos los endpoints.
  • Las URLs de foto de perfil expiran (TTL de WhatsApp CDN); refrésca llamando a /chats de nuevo.
  • type: "other" en un mensaje significa que el media aún no se ha descargado en el servidor. Llamar a /chats/media "forza" la descarga.