Consola: hazlo aquí mismo
Pega tu llave y sigue los tres pasos: crear la conexión, escanear el QR y mandar un mensaje de prueba. A la derecha ves cada respuesta tal cual.
0 Tu llave
Se queda solo en esta pestaña: al cerrarla se borra. No la pegues en un computador ajeno.
1 Conexión
2 Escanear
En el teléfono: WhatsApp → Dispositivos vinculados → Vincular un dispositivo. El código se renueva solo cada 20 s.
3 Mensaje de prueba
Escríbele primero a ese número desde el teléfono de destino: a quien nunca te escribió no se le puede escribir (ver «Cuidar el número»).
// Aquí aparece lo que contesta la API.
Tu llave, y cómo encaja todo
¿Qué es la llave? Es una contraseña larga que empieza por hk_ y que identifica a tu sistema ante la API. No es de una persona ni se usa para iniciar sesión en ninguna pantalla: la manda tu servidor en cada llamada, en la cabecera Authorization: Bearer hk_…, y con eso la API sabe quién eres.
La llave es también tu espacio: solo ve tus números, tus conversaciones y tus archivos, y nadie más ve los tuyos. Con una sola llave puedes conectar varios números de WhatsApp. Te la entrega Zamir; si se pierde o se filtra, se cambia por otra y la anterior deja de servir.
Tu página → tu servidor → la API → WhatsApp. Tu servidor guarda la llave y hace las llamadas: crear la conexión, pedir el QR para pintarlo en tu pantalla, enviar mensajes.
WhatsApp → la API → tu webhook. Cada mensaje que le escriben al número te llega a una dirección tuya (el webhook) en menos de un segundo. No tienes que estar preguntando.
Es una API HTTP normal: sirve desde cualquier lenguaje (Node, Python, PHP, .NET…) y no hay nada que instalar. Tu página puede tener su propia pantalla de «Conectar WhatsApp», su propio chat y sus propias reglas; la API solo lleva y trae los mensajes.
Todo es JSON en los dos sentidos, y las fechas van en ISO 8601.
Conectar un número con QR
-
Crea la conexión
POST/api/sessionscurlcurl -X POST /api/sessions \ -H "Authorization: Bearer hk_…" -H "Content-Type: application/json" \ -d '{ "nombre": "Asistente principal", "webhook_url": "https://tu-sistema.com/whatsapp", "webhook_token": "un-secreto-que-inventas-tu", "capturar": "todo" }'
Responde
201consession_idyvincular_url. Concapturar: "todo", en cuanto escaneen te llegan todas las conversaciones al webhook, sin más pasos. Elwebhook_tokente lo devolvemos en cada entrega comoBearer, para que sepas que viene de aquí.Tu
webhook_urltiene que abrirse desde internet.localhostno sirve; un túnel temporal sirve para probar, pero cuando se cae dejan de llegarte mensajes. -
Muestra el QR
Lo más rápidoAbre el
vincular_url. Es una pantalla lista, sin marca, que pinta el QR y se actualiza sola. Se le puede mandar a quien tenga el teléfono.En tu propia páginaGET /api/sessions/:id/qrdevuelveqr_image, un PNG listo para<img src>. Caduca cada ~20 s: vuelve a pedirlo cada 2 s y repinta hasta queestadosea"conectada".javascript · tu página// Llama a TU servidor, que es quien tiene la llave y le pregunta a la API. async function pintarQr(img) { const r = await fetch('/mi-servidor/whatsapp/qr').then((x) => x.json()); if (r.estado === 'conectada') return mostrarListo(r.numero); if (r.qr_image) img.src = r.qr_image; setTimeout(() => pintarQr(img), 2000); }
-
Escanea
En el teléfono del número que va a contestar: WhatsApp → Dispositivos vinculados → Vincular un dispositivo.
-
Comprueba
GET /api/sessions/:id→estado: "conectada". Ya entra y sale WhatsApp por tu sistema.No fijes el
session_ida mano para siempre: al arrancar, pideGET /api/sessionsy usa la que venga conectada.
archivada; pedir otra vez su QR la revive. Si la cerraron desde el teléfono, POST /api/sessions/:id/relink da un QR nuevo. En los dos casos conservas el mismo id y el historial.Recibir mensajes
Por cada mensaje que le llega al número hacemos un POST a tu webhook:
{
"evento": "mensaje",
"session_id": "…",
"chat": { "id": "151883633164487@lid", "nombre": "Camila", "es_grupo": false },
"de": { "nombre": "Camila", "numero": "573005550104", "yo": false },
"mensaje": {
"id": "3EB0C05CEC9E064DA2B280",
"tipo": "texto",
"texto": "Hola, vengo de parte de Daniel",
"fecha": "2026-09-17T15:10:04.000Z",
"responde_a": null
},
"archivo": null
}- Contesta
200de inmediato y procesa después. Si fallas o tardas, reintentamos con espera creciente; si contestas4xxentendemos que lo rechazaste y no se reintenta. - La cabecera
X-Entrega-Ides la misma en todos los reintentos: si la ves dos veces es el mismo mensaje, no lo proceses dos veces.X-Entrega-Edad-Segdice cuánto lleva esperando. de.numeroes el teléfono real: úsalo para saber quién es.chat.ides a donde se contesta (suele ser un identificador…@lid, no un teléfono).- Las notas de voz y los archivos llegan con
archivo: { url, mimetype, tamano }: pide esaurlcon tu llave y te llegan los bytes, por ejemplo para transcribir. Se guardan 90 días; el historial de mensajes, un año. - También llegan eventos
"estado"(ver abajo) y"conexion"(el número se cayó, volvió o lo desvincularon). Contéstalos con200aunque no los uses.
const express = require('express'); const app = express(); const vistos = new Set(); app.post('/whatsapp', express.json({ limit: '1mb' }), (req, res) => { if (req.headers.authorization !== 'Bearer ' + process.env.WEBHOOK_TOKEN) return res.sendStatus(401); res.sendStatus(200); // primero contestar const entrega = req.headers['x-entrega-id']; if (vistos.has(entrega)) return; // un reintento: ya lo tengo vistos.add(entrega); const e = req.body; if (e.evento === 'mensaje' && !e.de.yo) atender(e); // despues, con calma if (e.evento === 'estado') marcarChulos(e.mensaje.wa_id, e.mensaje.estado); });
¿No puedes exponer una URL? Lo mismo se lee por consulta: GET /api/sessions/:id/conversaciones y GET /api/sessions/:id/mensajes?chat_id=… (hasta 200 por página; antes=<id> para ir hacia atrás).
Enviar
async function enviar(sessionId, cuerpo) { const r = await fetch(`${API}/api/sessions/${sessionId}/send`, { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.WHATSAPP_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify(cuerpo), }); return r.json(); // { ok, mensaje: { wa_id, chat_id, estado } } } await enviar(id, { para: chatId, texto: 'Listo, ¿qué duda tienes?' }); await enviar(id, { para: chatId, texto: '¿Ya quedó esto?', citar: waIdOriginal }); await enviar(id, { para: chatId, tipo: 'audio', archivo: { url: 'https://tu-sistema.com/respuesta.ogg', ptt: true } });
para: elchat.idque te llegó (recomendado) o un teléfono con indicativo.citar: elwa_idde un mensaje, para responder sobre él.tipo:imagen,video,audio,documento,sticker,ubicacion. El archivo va comourlobase64, hasta 16 MB. Conptt: trueel audio sale como nota de voz, con su onda.- Guarda el
wa_idque te devuelve: es la llave para saber si lo leyeron y qué te contestaron.
La API atiende como una persona: muestra «escribiendo…» (o «grabando…») y tarda lo que se tarda en escribir eso, entre 1 y 3 segundos. Por eso la llamada no vuelve al instante; no hace falta simularlo de tu lado.
¿Lo leyó?
Cada mensaje que envías tiene un estado que avanza solo, igual que los chulos de WhatsApp:
enviadoWhatsApp lo aceptóentregadollegó al teléfonoleidolo abrióreproducidoescuchó la nota de vozCuando avanza, te llega al webhook un evento "estado" con el wa_id:
{ "evento": "estado", "chat": { "id": "…" },
"mensaje": { "wa_id": "3EB0F48F5E6D535A061AC2", "estado": "leido",
"entregado_at": "2026-09-17T15:11:10.000Z", "leido_at": "2026-09-17T15:13:42.000Z" } }Quédate siempre con el más avanzado y nunca retrocedas. También se puede consultar: GET /api/sessions/:id/mensajes?wa_id=…. Si la persona tiene apagadas las confirmaciones de lectura, nunca llega leido: te quedas en entregado, igual que le pasa a cualquiera con el teléfono. En un grupo, leido significa que al menos una persona lo leyó.
A qué mensaje responde
Si alguien usa Responder sobre un mensaje tuyo, el suyo llega con responde_a:
"responde_a": { "wa_id": "3EB0F48F5E6D535A061AC2", // el wa_id que guardaste al enviar "texto": "Camila quiere preguntarle a tu asistente. ¿La admites?", "es_reaccion": false, // true = puso un emoji encima (👍) "encontrado": true }
Así se empareja sin adivinar: envías, guardas el wa_id, y cuando llegue una respuesta con ese mismo wa_id sabes exactamente a qué contestaron, aunque haya cinco preguntas abiertas a la vez.
El enlace solo existe si la persona usa Responder. Un «sí» escrito suelto no está unido a nada: si hay una sola cosa pendiente, úsala; si hay varias, pregunta cuál.
Patrón: acceso con aprobación
Un asistente que habla en nombre de alguien, y al que solo entra quien esa persona aprueba. La API lleva y trae los mensajes; quién es quién, las aprobaciones y las tareas viven en tu sistema.
El dueño activa su asistente, una vez
Daniel le escribe al número desde su WhatsApp: «Activar DZ-7F3K», un código que le da tu plataforma. Guardas número de Daniel ↔ su asistente ↔ su
chat.id.No es opcional: WhatsApp bloquea los números que le escriben primero a quien nunca les escribió. Como Daniel escribió primero, después puedes pedirle aprobaciones sin riesgo.
Llega alguien nuevo
Dale a Daniel un enlace para compartir que ya trae el texto escrito. Camila lo toca y envía:
enlacehttps://wa.me/57XXXXXXXXXX?text=Hola%2C%20vengo%20de%20parte%20de%20DZ-7F3K
Lees el código del texto. Si no trae código, pregúntale de parte de quién viene.
Pides la aprobación
Le envías a Daniel: «Camila (+57 321…) quiere preguntarle a tu asistente. ¿La admites? Responde sí o no sobre este mensaje». Guardas el
wa_idjunto a la solicitud de Camila. A ella: «Le avisé a Daniel; en cuanto apruebe te escribo».Daniel responde
Su «sí» llega con
responde_a.wa_idigual al que guardaste: sabes exactamente a quién aprobó. Un 👍 sobre el mensaje también llega, cones_reaccion: true.Conversación
Cada pregunta de Camila → tu motor →
/sendcon la respuesta. Si manda nota de voz: descargas el audio, transcribes, respondes (en texto o en nota de voz).Tareas y seguimiento
Si la respuesta crea un compromiso («te lo entrego el 10»), lo guardas y programas el seguimiento. El «¿ya quedó lo que hablamos?» es un
/sendnormal citando el mensaje original: como esa persona ya te escribió, es seguro, y si contesta sobre él,responde_ate dice de qué tarea habla.
Cuidar el número
Todo esto evita una sola cosa: que WhatsApp tome el número por un robot o un emisor masivo y lo bloquee. Un número bloqueado no se recupera.
No escribas primero
A quien nunca te escribió no se le escribe: la API lo vigila y puede rechazarlo con 403. Para invitar gente, que sea ella quien escriba (enlace wa.me).
Sin prisa
Máximo 20 mensajes por minuto y 60 destinatarios nuevos por hora, por número. Al pasarte llega un 429 con Retry-After: espera y sigue.
Nada de difusiones
El mismo texto a muchos números es justo lo que WhatsApp persigue. Y mejor una o dos respuestas que seis seguidas.
Para crecer: el canal oficial
Un número público atendiendo a mucha gente debe ir por la API oficial, con aprobación de Meta. La misma llamada /send sale por ahí cuando esté configurado; pasadas 24 h sin respuesta, ese canal solo permite plantillas aprobadas.
Errores y estados
| Código | Qué pasa | Qué hacer |
|---|---|---|
400 | Falta un campo o va mal. | El error dice cuál. |
401 | La llave no vale. | Revisa Authorization: Bearer hk_…. |
403 | No se le puede escribir a ese chat. | Mira codigo: casi siempre es que nunca te escribió. |
404 | Esa conexión no existe para tu llave. | GET /api/sessions. |
409 | El número no está conectado ahora. | Mira su estado. |
429 | Vas muy rápido. | Espera lo que diga Retry-After. |
504 | No se sabe si salió. | No reintentes a ciegas: GET …/mensajes?wa_id=<mensaje_id>; si aparece, salió. |
| Estado | Qué significa |
|---|---|
conectada | Funcionando. |
desconectada | Se cayó y se está reconectando sola. Es normal que pase unos segundos varias veces al día. |
qr · esperando_qr | Esperando a que alguien escanee. |
deslogueada | La cerraron desde el teléfono: POST …/relink y escanear de nuevo. |
archivada | Nadie la atendió a tiempo. Pedir su QR la revive, con el mismo id. |
bloqueada | WhatsApp bloqueó el número. No se arregla con otro QR. |
Antes de salir en vivo
- La llave
hk_…está solo en el servidor (si se filtró, pídele otra a Zamir). - El webhook contesta
200de inmediato a todo (mensaje,estado,conexion) y procesa después. - Se descartan duplicados por
X-Entrega-Ido pormensaje.id. - Se identifica a la persona por
de.numeroy se contesta achat.id. - Se guarda el
wa_idde cada envío. - Quien va a recibir mensajes tuyos te escribió primero.
- Se manejan el
429(esperar) y el504(consultar antes de reintentar).