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
Adquiere tu token
Ve a exdoo.mx y contrata un plan. Una vez completada la compra, Exdoo te entregará por correo:
- Tu
tokenpersonal (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.
- Tu
-
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
Vincula tu WhatsApp con el QR
Funciona tanto con WhatsApp como con WhatsApp Business — el flujo es idéntico. En tu teléfono:
- Abre WhatsApp o WhatsApp Business.
- Toca el menú ⋮ (Android) o Configuración (iPhone).
- Selecciona Dispositivos vinculados → Vincular un dispositivo.
- 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
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
¡Ya puedes usar la API!
Con tu
tokenen mano, ya puedes hacer requestsHTTPcontra 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
toes E.164 sin+(en México:521+ 10 dígitos).
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
| Concepto | Valor |
|---|---|
| Rate limit | 30 requests/minuto por sesión (compartido entre envío y lectura) |
| Timeout upstream | 30 segundos |
| Reintentos en errores transitorios | 3 con backoff exponencial (1s → 2s → 4s) |
Errores
Forma general de un error:
{ "ok": false, "error": "descripción del problema" }
| HTTP | Causa |
|---|---|
400 | JSON inválido, campo requerido faltante, formato inválido (teléfono, chat_id, message_id, mimetype no permitido). |
401 | token inválido o X-API-Key incorrecta. |
404 | Mensaje no existe (no se encontró en el chat indicado). |
429 | Rate limit excedido para la sesión. |
502 | El proveedor de mensajería no respondió o devolvió un error inesperado. |
Conectar / arrancar sesión
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
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
| Status | Significado |
|---|---|
STARTING | Inicializando, espera 1-2 s. |
SCAN_QR_CODE | Listo para escanear; usa /qr. |
WORKING | Conectado; ya puede enviar/leer. |
FAILED | Falla del proveedor; reintenta /connect. |
STOPPED | Detenida; llama a /connect para arrancar otra vez. |
Obtener 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
| HTTP | Caso |
|---|---|
409 | La sesión no está en SCAN_QR_CODE (consulta /status primero). |
404 | Sesió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
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
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.
- Tu administrador del servicio te entrega un
token+ un link de onboarding. - Abres el link en tu navegador y escaneas el QR con tu WhatsApp.
- Cuando la página muestra ✅, ya puedes usar los endpoints de envío/lectura con tu token.
Enviar texto y/o adjuntos
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..."
}
]
}
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
token | string | sí | Resuelve la sesión. |
to | string | sí | E.164 sin + (10–15 dígitos). Acepta también con espacios/guiones — se normaliza. |
message.template | string | sí | Texto con variables {{nombre}}. Si no usas vars, manda el texto plano aquí. |
message.vars | object | no | Reemplazos para la plantilla. |
attachments | array | no | Lista de adjuntos. Omitir o lista vacía = solo texto. |
attachments[].filename | string | sí* | Si la extensión no coincide con el mimetype, se agrega automáticamente. |
attachments[].mimetype | string | sí* | Ver mimetypes permitidos abajo. |
attachments[].content_b64 | string | sí* | 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
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
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)
/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
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
}
| Campo | Tipo | Default | Notas |
|---|---|---|---|
token | string | — | Requerido. |
limit | int | 50 | Entre 1 y 200. |
offset | int | 0 | Para 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
| Campo | Notas |
|---|---|
chat_id | Opaco. Úsalo tal cual al pedir mensajes; no lo parsees. |
kind | "individual" o "group". |
phone | Sólo en chats individuales (10–15 dígitos). En grupos es null. |
picture | URL 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.preview | Recortado a 200 chars. Para media: [imagen], [audio], [video], [documento], [adjunto]. |
last_message.timestamp | Unix epoch en segundos. |
Mensajes de un chat
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
}
| Campo | Tipo | Default | Notas |
|---|---|---|---|
token | string | — | Requerido. |
chat_id | string | — | Requerido. Tal cual lo devolvió /chats. |
limit | int | 50 | Entre 1 y 200. |
offset | int | 0 | Para 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
| Campo | Notas |
|---|---|
id | Identificador del mensaje. Pásalo como message_id al endpoint de media. |
from_me | true si la sesión es la que envió el mensaje. |
author | En grupos: número del miembro que envió. null en individuales. |
type | "text", "image", "audio", "video", "document" u "other". |
text | Cuerpo o caption. null si es media sin texto. |
media | null en texto puro. Si hay adjunto, media.media_id = lo que va a /chats/media. |
has_more | true si quedan más mensajes hacia atrás (incrementa offset). |
Paginación (scroll infinito)
- Primera llamada:
offset=0, limit=50. - Al hacer scroll hacia arriba:
offset=50, limit=50. Etc. - Detener cuando
has_more == false.
Descargar adjunto
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..."
}
| Campo | Tipo | Notas |
|---|---|---|
token | string | Requerido. |
chat_id | string | Requerido. El chat al que pertenece el mensaje. |
message_id | string | Requerido. 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
| HTTP | Caso |
|---|---|
400 | message_id mal formado o el mensaje no tiene media (es texto puro). |
404 | chat_id+message_id no corresponde a ningún mensaje. |
502 | No 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
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
- Al cargar la app:
POST /v1/whatsapp/chats→ llenar sidebar de conversaciones. - Al clickear un chat:
POST /v1/whatsapp/chats/messagescon suchat_id→ panel de mensajes. - Al scrollear hacia arriba: misma llamada con
offset += limithastahas_more=false. - Al clickear un adjunto:
POST /v1/whatsapp/chats/mediaconchat_id+message_id. - Al enviar:
POST /v1/whatsapp/sendcon texto y/o adjuntos.
- 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
/chatsde 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.