Documentación
API para desarrolladores
Conecta tu bot, CRM o tienda en línea a los chats de WhatsApp de tu organización: lee conversaciones, responde con texto, botones, listas, plantillas o Flows, pasa el chat a una persona y recibe cada mensaje en tu servidor por webhook.
1. Primeros pasos
- Entra a wabalink como propietario o administrador y abre Bots y API.
- Pulsa Conectar un bot: indica la URL de tu webhook, los eventos y el modo (solo cuando se lo pasen o todos los chats libres).
- Copia la llave de API (
wl_live_…) y el secreto de firma. Se muestran una sola vez; guárdalos en tu gestor de secretos. - Prueba la llave:
curl https://wabalink.desolutec.com/api/v1/me \ -H "Authorization: Bearer $WABALINK_KEY"
2. Autenticación y límites
- URL base:
https://wabalink.desolutec.com/api/v1 - Cabecera
Authorization: Bearer wl_live_…en cada petición. Solo guardamos un hash de la llave: si la pierdes, revócala y crea otra. - Cuerpos y respuestas en JSON (
Content-Type: application/json). Fechas en ISO 8601 (UTC). - Límite: 600 peticiones por minuto por llave. Si lo superas recibes
429con las cabecerasRateLimit-*. - Cada llave pertenece a una sola organización; no puede ver datos de otras.
3. Errores
Los errores responden con un código HTTP y un cuerpo como este:
{
"error": "Datos inválidos",
"details": [ { "path": "buttons.0.title", "message": "String must contain at most 20 character(s)" } ]
}| Código | Significado |
|---|---|
| 400 | Datos inválidos (revisa details), ventana de 24 h cerrada para mensajes libres o WhatsApp rechazó el envío. |
| 401 | Falta la llave, está revocada o no es válida. |
| 404 | La conversación o el contacto no existen en tu organización. |
| 429 | Demasiadas peticiones: espera y reintenta. |
| 5xx | Error nuestro o de Meta: reintenta con espera exponencial. |
4. Endpoints
GET/api/v1/me
Organización y números. Devuelve la organización dueña de la llave y sus números de WhatsApp. Úsalo para validar la llave y obtener el phoneId con el que se inician chats.
Ejemplo
curl https://wabalink.desolutec.com/api/v1/me \ -H "Authorization: Bearer $WABALINK_KEY"
Respuesta
{
"organization": { "id": "7d1c…", "name": "Tienda Demo", "kind": "company", "country": "CO" },
"phones": [
{ "id": "b2f0…", "display_phone_number": "+57 300 111 2233", "verified_name": "Tienda Demo",
"label": "Ventas", "is_coexistence": true, "status": "active" }
],
"integration_id": "5a9e…"
}GET/api/v1/conversations
Listar conversaciones. Conversaciones ordenadas de la más reciente a la más antigua.
| Campo | Tipo | Descripción |
|---|---|---|
status | query · open | closed | Filtra por estado. |
phoneId | query · uuid | Solo las de un número. |
limit | query · 1–200 | Por defecto 50. |
Ejemplo
curl https://wabalink.desolutec.com/api/v1/conversations \ -H "Authorization: Bearer $WABALINK_KEY"
Respuesta
[
{
"id": "c41a…", "status": "open",
"contact": { "id": "e8b2…", "name": "María Gómez", "wa_id": "573105550101", "tags": ["vip"] },
"phone": { "id": "b2f0…", "display": "+57 300 111 2233", "label": "Ventas" },
"assigned_user": "Carlos Méndez", "handled_by_bot": null, "window_open": true,
"last_message_at": "2026-09-28T14:03:11.000Z", "last_inbound_at": "2026-09-28T14:03:11.000Z", "unread_count": 1
}
]GET/api/v1/conversations/:id
Ver una conversación. Mismo formato que cada elemento del listado. window_open indica si la ventana de 24 h está abierta (si no, solo puedes enviar plantillas).
Ejemplo
curl https://wabalink.desolutec.com/api/v1/conversations/ID_CONVERSACION \ -H "Authorization: Bearer $WABALINK_KEY"
Respuesta
{ "id": "c41a…", "status": "open", "window_open": false, … }GET/api/v1/conversations/:id/messages
Mensajes de una conversación. Historial del chat, del más reciente hacia atrás. origin dice quién lo envió: customer, api (un agente), business_app (desde el celular en coexistencia), automation o bot.
| Campo | Tipo | Descripción |
|---|---|---|
before | query · ISO 8601 | Paginación: mensajes anteriores a esta fecha. |
limit | query · 1–200 | Por defecto 50. |
Ejemplo
curl https://wabalink.desolutec.com/api/v1/conversations/ID_CONVERSACION/messages \ -H "Authorization: Bearer $WABALINK_KEY"
Respuesta
[
{ "id": "9f3d…", "wamid": "wamid.HBgM…", "direction": "in", "origin": "customer", "type": "text",
"text": "¿Tienen envío a Pasto?", "status": "received", "created_at": "2026-09-28T14:03:11.000Z", "interactive": null }
]POST/api/v1/messages
Enviar un mensaje. Envía a una conversación existente (conversationId) o a un número (phoneId + to; si no existe el chat, se crea). Fuera de la ventana de 24 h WhatsApp solo acepta plantillas aprobadas.
| Campo | Tipo | Descripción |
|---|---|---|
type | text | buttons | list | template | flow | image | Obligatorio. |
conversationId | uuid | O bien phoneId + to. |
phoneId, to | uuid, string | Número desde el que envías y celular del cliente con indicativo (573001234567). |
text | string ≤ 4096 | Texto (o cuerpo del menú, o pie de la imagen). |
buttons | [{ id, title ≤ 20 }] × 1–3 | Para type = buttons. |
button, sections | string ≤ 20, [{ title, rows: [{ id, title ≤ 24, description ≤ 72 }] }] | Para type = list (máx. 10 opciones). |
header, footer | string ≤ 60 | Opcionales en buttons y list. |
template | { name, language, components? } | Para type = template. |
flowId, cta | uuid, string ≤ 30 | Para type = flow. |
imageUrl | URL https | Para type = image. |
Ejemplo
curl -X POST https://wabalink.desolutec.com/api/v1/messages \
-H "Authorization: Bearer $WABALINK_KEY" \
-H "Content-Type: application/json" \
-d '{
"conversationId": "ID_CONVERSACION",
"type": "buttons",
"text": "¿Cómo quieres recibir tu pedido?",
"buttons": [
{ "id": "domicilio", "title": "A domicilio" },
{ "id": "recoger", "title": "Recoger en tienda" }
]
}'Respuesta
{ "conversation_id": "c41a…", "message": { "id": "0b7e…", "wamid": "wamid.HBgM…", "status": "pending", "error": null } }POST/api/v1/conversations/:id/handoff
Pasar a una persona o tomar el chat. to = human devuelve el chat al equipo y avisa a los agentes (con una nota opcional). to = bot hace que tu integración atienda el chat.
Ejemplo
curl -X POST https://wabalink.desolutec.com/api/v1/conversations/ID_CONVERSACION/handoff \
-H "Authorization: Bearer $WABALINK_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "human", "note": "Quiere pagar con tarjeta de crédito" }'Respuesta
{ "id": "c41a…", "handled_by_bot": null, … }POST/api/v1/conversations/:id/close
Cerrar una conversación. La marca como cerrada y la libera del bot. Si el cliente vuelve a escribir, se reabre sola.
Ejemplo
curl -X POST https://wabalink.desolutec.com/api/v1/conversations/ID_CONVERSACION/close \ -H "Authorization: Bearer $WABALINK_KEY"
Respuesta
{ "id": "c41a…", "status": "closed", … }PATCH/api/v1/contacts/:id
Actualizar un contacto. Cambia nombre, correo, notas o etiquetas. tags reemplaza la lista completa; las etiquetas nuevas se crean solas y aparecen para todo el equipo.
Ejemplo
curl -X PATCH https://wabalink.desolutec.com/api/v1/contacts/ID_CONTACTO \
-H "Authorization: Bearer $WABALINK_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "María Fernanda Gómez", "tags": ["vip", "cotizado"], "notes": "Prefiere entrega en la tarde" }'Respuesta
{ "id": "e8b2…", "display_name": "María Fernanda Gómez", "tags": ["vip", "cotizado"], … }GET/api/v1/tags
Etiquetas del equipo. Las etiquetas definidas en la organización, en el orden del equipo, con su color y cuántos contactos la tienen.
Ejemplo
curl https://wabalink.desolutec.com/api/v1/tags \ -H "Authorization: Bearer $WABALINK_KEY"
Respuesta
[ { "name": "cotizado", "color": "#f97316", "contacts": 12 }, { "name": "vip", "color": "#eab308", "contacts": 4 } ]5. Webhooks
wabalink hace POST a la URL de tu integración con los eventos que elegiste. Responde 2xx en menos de 8 segundos (procesa en segundo plano si tardas más). Si falla, reintentamos 2 veces con espera (1 s y 2 s). Todos los envíos tienen este sobre:
{
"id": "f1c9b6e2-…", // único por envío
"event": "message.received",
"created_at": "2026-09-28T14:03:12.114Z",
"tenant_id": "7d1c…",
"data": { … } // depende del evento
}Cabeceras: X-Wabalink-Event, X-Wabalink-Timestamp (segundos Unix) y X-Wabalink-Signature. El id es único por envío: úsalo para ignorar duplicados.
message.received
Un cliente escribió. Llega a la integración que atiende el chat, o a todas las que estén en modo «todos los chats libres» si nadie lo atiende.
{
"conversationId": "c41a…",
"contact": { "id": "e8b2…", "wa_id": "573105550101", "name": "María Gómez" },
"phone": { "id": "b2f0…", "display": "+57 300 111 2233", "label": "Ventas" },
"message": { "id": "9f3d…", "wamid": "wamid.HBgM…", "type": "interactive", "text": "A domicilio",
"interactive": { "type": "button_reply", "button_reply": { "id": "domicilio", "title": "A domicilio" } },
"created_at": "2026-09-28T14:03:11.000Z" },
"handledBy": "bot"
}message.status
Cambió el estado de un mensaje que envió tu bot. Al aceptarlo Meta queda en pending; luego llegan sent, delivered, read o failed (con error).
{ "conversationId": "c41a…", "messageId": "0b7e…", "wamid": "wamid.HBgM…", "status": "read", "error": null }conversation.handoff
El chat cambió de manos: to = human cuando un agente escribe o lo toma, o una automatización lo pasa a una persona; to = bot cuando te lo pasan.
{ "conversationId": "c41a…", "to": "human", "reason": "takeover", "agent": "Carlos Méndez" }flow.response
Un cliente envió un WhatsApp Flow.
{ "flowId": "31c2…", "flowName": "Agendar cita", "conversationId": "c41a…", "answers": { "fecha": "2026-10-02", "sede": "Chapinero" } }ping
Prueba enviada con el botón «Probar» de Bots y API.
{ "message": "Prueba de wabalink" }6. Verificar la firma
La firma es sha256= + HMAC-SHA256 en hexadecimal de "<timestamp>.<cuerpo crudo>" con tu secreto. Calcúlala sobre el cuerpo tal como llegó (antes de parsear el JSON), compárala en tiempo constante y rechaza timestamps de más de 5 minutos.
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/wabalink', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Wabalink-Timestamp');
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.WABALINK_SECRET)
.update(`${ts}.${req.body}`)
.digest('hex');
const received = req.get('X-Wabalink-Signature') || '';
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!fresh || received.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
return res.status(401).end();
}
res.status(200).end(); // responde rápido…
const { event, data } = JSON.parse(req.body);
handle(event, data); // …y procesa después
});Python (Flask)
import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["WABALINK_SECRET"].encode()
@app.post("/wabalink")
def wabalink():
ts = request.headers.get("X-Wabalink-Timestamp", "0")
body = request.get_data() # cuerpo crudo
expected = "sha256=" + hmac.new(SECRET, ts.encode() + b"." + body, hashlib.sha256).hexdigest()
received = request.headers.get("X-Wabalink-Signature", "")
if abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(received, expected):
abort(401)
event = request.get_json()
# … encola event["event"], event["data"]
return "", 2007. Buenas prácticas
- Ventana de 24 h. WhatsApp solo permite mensajes libres hasta 24 h después del último mensaje del cliente. Revisa
window_openy, si está cerrada, envía una plantilla aprobada. - Pasa a una persona a tiempo. Si el cliente pide un asesor o tu bot no entiende dos veces seguidas, usa
handoffconto: "human"y una nota. - Respeta al agente. Cuando un agente escribe en el chat, el bot deja de atenderlo y recibes
conversation.handoffconto: "human". - Consentimiento. Envía marketing solo a quien lo aceptó (Ley 1581 de 2012 y políticas de WhatsApp) y respeta la marca «No desea recibir marketing».
- Seguridad. La URL del webhook debe ser
https://. No publiques la llave en código del navegador ni en repositorios.
¿Dudas? Revisa las preguntas frecuentes o escríbenos a wabalink@desolutec.com.