Vincular un número
Una instancia es un número de WhatsApp conectado al motor como un dispositivo vinculado, igual que WhatsApp Web. Esta página amplía los pasos 2 y 3 del Inicio rápido: las variantes de la vinculación, sus estados, sus eventos y qué hacer cuando algo sale mal.
1. Crea la instancia
Sección titulada «1. Crea la instancia»POST /v1/instances (scope instances:create) con un nombre (de 1 a 120 caracteres) y, si
quieres, un owner_ref: una referencia opaca tuya de hasta 200 caracteres (la sucursal, el
vendedor). El motor no la interpreta; te la devuelve en cada lectura.
| Respuesta | Cuándo |
|---|---|
| 201 | Instancia nueva, en estado nuevo y con generacion: 0. |
| 200 | Repetiste el pedido con la misma Idempotency-Key y los mismos datos: es la misma instancia. |
| 403 | La key está restringida a instancias: no puede crear. |
| 409 | La misma Idempotency-Key con otros datos. |
| 429 | La app llegó a su cupo de instancias (3 de fábrica). No es temporal: no trae Retry-After. |
La Idempotency-Key es opcional aquí, pero úsala: si la red se corta durante el alta, repetirla
no crea una segunda instancia ni gasta cupo. Las instancias archivadas no cuentan para el cupo.
2. Elige cómo vincular
Sección titulada «2. Elige cómo vincular»| Modo | Cómo se usa | Cuándo conviene |
|---|---|---|
codigo |
El motor pide a WhatsApp un código de 8 caracteres para el número que indicas. En el teléfono: Dispositivos vinculados → Vincular un dispositivo → Vincular con el número de teléfono. | Cuando quien vincula no puede escanear una pantalla (vincula desde el mismo teléfono, o a distancia). |
qr |
El motor genera un QR; se escanea desde Dispositivos vinculados. | Cuando la persona tiene delante la pantalla de tu app. |
| passkey | No se elige: WhatsApp la puede exigir durante un intento con QR o código. | Ver La passkey. |
3. Pide el reto
Sección titulada «3. Pide el reto»POST /v1/instances/{id}/pairing (scope instances:pair) con modo, telefono (solo en modo
codigo) y, opcional, generacion_esperada. Responde 202 en cuanto el intento quedó
guardado: el reto llega unos segundos después.
{ "instancia_id": "8a1d2c3e-0000-4000-8000-0000000000a1", "generacion": 1, "modo": "qr", "fase": "iniciando", "estado": "esperando_qr"}Lee el reto con GET /v1/instances/{id}/pairing (mismo scope) hasta que traiga reto, y después
sigue leyendo la instancia hasta que llegue a conectado:
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/pairing \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"modo": "qr"}'
# Repite hasta que reto no sea null (y antes de reto.expira_en):curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/pairing \ -H "Authorization: Bearer TU_API_KEY"// El SDK todavía no trae el emparejamiento: el pedido y el reto son HTTP directo.type reto struct { Tipo string `json:"tipo"` Valor string `json:"valor"` ExpiraEn time.Time `json:"expira_en"`}type emparejamiento struct { Generacion int64 `json:"generacion"` Fase string `json:"fase"` Reto *reto `json:"reto"` Error string `json:"error"`}
func leerReto(ctx context.Context, c *motor.Cliente, id string) (emparejamiento, error) { req, _ := http.NewRequestWithContext(ctx, http.MethodGet, c.Base+"/v1/instances/"+id+"/pairing", nil) req.Header.Set("Authorization", "Bearer "+c.Token) res, err := c.HTTP.Do(req) if err != nil { return emparejamiento{}, err } defer res.Body.Close() var e emparejamiento return e, json.NewDecoder(res.Body).Decode(&e)}
// Muestra cada reto nuevo hasta que la instancia se conecte.var ultimo stringfor { e, err := leerReto(ctx, c, inst.ID) if err != nil { return err } if e.Reto != nil && e.Reto.Valor != ultimo { ultimo = e.Reto.Valor mostrar(e.Reto) // dibuja el QR o muestra el código; nunca lo registres } if inst, err = c.Instancia(ctx, inst.ID); err != nil { return err } switch inst.Estado { case "conectado": return nil case "intento_vencido", "error": return fmt.Errorf("no se vinculó: %s %s", inst.Estado, inst.Motivo) } time.Sleep(2 * time.Second)}const base = `https://motor.ejemplo.com/v1/instances/${instanciaId}`;const auth = { Authorization: `Bearer ${apiKey}` };
await fetch(`${base}/pairing`, { method: 'POST', headers: { ...auth, 'Content-Type': 'application/json' }, body: JSON.stringify({ modo: 'qr' }),}); // 202
let ultimo = '';for (;;) { const { reto } = await (await fetch(`${base}/pairing`, { headers: auth })).json(); if (reto && reto.valor !== ultimo) { ultimo = reto.valor; mostrar(reto); // dibuja el QR o muestra el código; nunca lo registres } const { estado, motivo } = await (await fetch(base, { headers: auth })).json(); if (estado === 'conectado') break; if (estado === 'intento_vencido' || estado === 'error') throw new Error(`${estado}: ${motivo}`); await new Promise((r) => setTimeout(r, 2000));}{ "generacion": 1, "modo": "qr", "fase": "esperando_qr", "reto": { "tipo": "qr", "valor": "…", "expira_en": "2026-10-24T15:03:05Z" }, "creado_en": "2026-10-24T15:02:05Z", "terminado_en": null}reto.valores el texto del QR, no una imagen: dibújalo con tu biblioteca de QR. En modocodigo, es el código de 8 caracteres.- El QR se renueva. WhatsApp entrega varios QR seguidos (el primero dura 60 s; después, cinco
más de 20 s):
reto.valoryreto.expira_encambian. Vuelve a leer el reto y redibújalo, como hace el ejemplo. El eventopairing.availablesolo avisa del primero. - El intento vive unos 160 segundos en los dos modos. En la prueba A12 (16/10/2026), un código que no se usó venció a los 2 min 40 s.
retoesnullmientras no hay uno vigente, y también desde que el teléfono escaneó.- Antes del primer intento,
GET …/pairingresponde 404.
Las fases del intento
Sección titulada «Las fases del intento»fase |
Qué significa |
|---|---|
iniciando |
El motor está abriendo la conexión de login. |
esperando_qr |
Hay un QR vigente. |
esperando_codigo |
Hay un código vigente: falta escribirlo en el teléfono. |
accion_requerida |
WhatsApp pidió una passkey. |
emparejando |
El teléfono aceptó; el motor está registrando el dispositivo. |
completado |
Vinculado. |
vencido |
Se acabó el tiempo sin vincular. |
fallido |
WhatsApp o el motor rechazaron el intento: error dice por qué. |
reemplazado |
Pediste otro intento antes de terminar este. |
La generación
Sección titulada «La generación»Cada POST …/pairing abre una generación nueva (generacion sube de uno en uno) y cierra la
anterior como reemplazado, sin desvincular nada. Por eso un QR vencido se resuelve pidiendo
otro. La generación sirve para tres cosas:
- Retos viejos. Un reto o un aviso de una generación anterior se descarta: nunca se muestra.
- Comandos viejos. Un mensaje aceptado antes de volver a vincular no sale por el número nuevo:
termina
fallidocongeneracion_cambiada. - Carreras entre pantallas. Manda
generacion_esperadacon la generación que tu app está mostrando. Si otra persona pidió otro intento mientras tanto, la respuesta es 409 («La generación no coincide») en vez de reemplazar su intento.
Cada evento trae la generation de la instancia en el sobre: un evento de una generación vieja
queda en el ledger, pero no cambia el estado.
Los estados de la instancia
Sección titulada «Los estados de la instancia»estado es lo que el motor observa; estado_deseado (activo, pausado, desvinculado o
archivado) es lo que pidieron. motivo trae un código tipificado (nunca un texto del proveedor).
estado |
Qué significa | Qué hacer |
|---|---|---|
nuevo |
Creada, nunca vinculada. | Pide un reto. |
esperando_qr, esperando_codigo |
Hay un intento con su reto. | Muéstralo. |
accion_requerida |
WhatsApp pidió una passkey. | Ver La passkey. |
emparejando |
El teléfono aceptó; se registra el dispositivo. | Esperar unos segundos. |
emparejado |
Vinculada, todavía sin autenticar la sesión. | Esperar conectado. |
conectado |
Lista: numero trae el teléfono en formato +E.164. |
— |
desconectado |
Sin conexión. motivo dice por qué (sin_conexion, pausada, apagado…). |
Si no la pausaste, el motor reintenta solo. |
reconectando |
El motor está volviendo a conectar. | Esperar. |
intento_vencido |
El reto venció sin usarse (qr_vencido o codigo_vencido). |
Pedir otro reto. |
error |
El intento falló (motivo) o WhatsApp rechazó la conexión. |
Ver Recuperarse. |
cerrado_por_telefono |
Alguien cerró la sesión desde el teléfono. | Volver a vincular. |
cerrado_por_operador |
Se desvinculó desde el motor. | Volver a vincular, si hace falta. |
reemplazado |
Otra conexión tomó la sesión de este dispositivo. | Reconectar a pedido. |
bloqueado |
WhatsApp la bloqueó por un tiempo; bloqueada_hasta dice hasta cuándo, si lo informó. |
Esperar; no envía mientras tanto. |
archivado |
Archivada: sin sesión, fuera del cupo. Su ledger se conserva. | Es definitivo: crea otra instancia. |
Un vínculo con código que entró a la primera, en la prueba A1 (15/10/2026): nuevo →
esperando_codigo → emparejando → emparejado → conectado, en unos 4 segundos desde que se
escribió el código.
Los eventos de la vinculación
Sección titulada «Los eventos de la vinculación»Todo queda en el ledger de la instancia y llega por tus webhooks o streams:
| Evento | Cuándo | data |
|---|---|---|
state.changed |
En cada cambio de estado. | de, a, motivo, generacion. |
pairing.available |
Llega el primer reto de cada tipo de una generación. Sin el reto: léelo con GET …/pairing. |
modo, reto (el tipo), expira_en. |
pairing.expired |
El intento venció. | motivo: qr_vencido o codigo_vencido. |
pairing.failed |
El intento falló. | motivo (abajo). |
paired |
El teléfono aceptó y el dispositivo quedó registrado. | numero y plataforma. |
connected |
La sesión se autenticó. | — |
motivo de pairing.failed |
Qué hacer |
|---|---|
telefono_rechazado |
WhatsApp rechazó el número: revisa que esté completo, con el código de país. |
sin_multidispositivo |
El teléfono no tiene los dispositivos vinculados activos. |
cliente_desactualizado |
WhatsApp pide una versión más nueva del motor: hay que actualizarlo. |
conexion |
El motor no pudo conectar con WhatsApp: revisa su salida a internet o su proxy. |
error_emparejamiento, estado_inesperado, vinculo_rechazado |
Pide otro intento. |
passkey |
La ceremonia de la passkey no se completó. |
credencial_revocada, credencial_vencida, sin_scope, sin_acceso_a_la_instancia, app_suspendida |
La key que pidió el intento dejó de valer antes de terminar: pide otro con una key vigente. |
El motor revalida la key que pidió el intento justo antes de registrar el dispositivo: si la revocaron mientras tanto, el vínculo se rechaza.
La passkey
Sección titulada «La passkey»Si WhatsApp exige una passkey, el intento pasa a accion_requerida y el reto cambia de tipo:
reto.tipo: "passkey":reto.valortrae las opciones WebAuthn en JSON. Tu app tiene que resolverlas con un autenticador válido para el origen de WhatsApp y mandar el resultado conPOST /v1/instances/{id}/pairing/passkeyy{"generacion": N, "respuesta": {…}}(elPublicKeyCredentialserializado).reto.tipo: "passkey_confirmacion":reto.valores un código para comparar con el que muestra el teléfono. Si coinciden, confirma conPOST …/pairing/passkey/confirmy{"generacion": N}.
Si WhatsApp rechaza la respuesta, el motor responde 502 sin el detalle del servidor. El panel no puede completar esta ceremonia (el autenticador es de WhatsApp) y lo dice.
Recuperarse
Sección titulada «Recuperarse»Las acciones sobre una instancia vinculada son comandos de control:
POST /v1/instances/{id}/commands con {"type": "…"}. Responden 202 con el comando
(aceptado); el resultado (completado o fallido) se lee en GET /v1/commands/{id} y en el
estado de la instancia.
type |
Scope | Qué hace |
|---|---|---|
instance.reconnect |
instances:manage |
Cierra y vuelve a abrir el socket. |
instance.disconnect |
instances:manage |
Lo cierra hasta que pidas reconectar (o hasta que el motor reinicie). |
instance.pause |
instances:manage |
Lo cierra y la deja pausada, también después de reiniciar el motor. |
instance.resume |
instances:manage |
La vuelve a activar. |
instance.logout |
instances:logout |
Desvincula el número: avisa a WhatsApp si la sesión está autenticada. |
instance.archive |
instances:logout |
Desvincula y archiva. Es lo mismo que DELETE /v1/instances/{id}. |
# Reconectar a pedido (por ejemplo, después de «reemplazado»):curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/commands \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: reconectar-sucursal-centro-1" \ -d '{"type": "instance.reconnect"}'
# Volver a vincular después de un cierre desde el teléfono:curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/pairing \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"modo": "codigo", "telefono": "+TU_NUMERO_CON_CODIGO_DE_PAIS"}'// El SDK no trae los comandos de control: es HTTP directo. El resultado se lee con c.Comando.cuerpo := strings.NewReader(`{"type": "instance.reconnect"}`)req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.Base+"/v1/instances/"+inst.ID+"/commands", cuerpo)req.Header.Set("Authorization", "Bearer "+c.Token)req.Header.Set("Content-Type", "application/json")req.Header.Set("Idempotency-Key", "reconectar-sucursal-centro-1")res, err := c.HTTP.Do(req) // 202if err != nil { return err}defer res.Body.Close()var cmd motor.Comandoif err := json.NewDecoder(res.Body).Decode(&cmd); err != nil { return err}cmd, err = c.Comando(ctx, cmd.ID) // completado o fallido (ErrorCodigo)const res = await fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}/commands`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'reconectar-sucursal-centro-1', }, body: JSON.stringify({ type: 'instance.reconnect' }),});const comando = await res.json(); // 202: { command_id, status: "aceptado", … }| Situación | Qué pasó | Qué hacer |
|---|---|---|
intento_vencido |
Nadie usó el reto a tiempo. | Pide otro reto: abre otra generación. |
error tras un intento |
pairing.failed con su motivo. |
Corrige la causa (tabla de motivos) y pide otro reto. |
desconectado o reconectando |
Se cortó la red o el socket. | Nada: el motor reconecta solo (si config.reconexion_automatica no está apagada). Para forzarlo, instance.reconnect. |
cerrado_por_telefono |
Cerraron la sesión desde Dispositivos vinculados. | Vuelve a vincular con POST …/pairing; no hace falta reiniciar el motor. |
reemplazado |
Otra conexión tomó la sesión. El motor no reconecta solo, ni al reiniciar. | instance.reconnect cuando sepas que la otra conexión ya no corre. |
bloqueado |
Un bloqueo temporal de WhatsApp. | Esperar a bloqueada_hasta. Los envíos responden 409 mientras tanto. |
| Cambiar el número | La instancia ya está vinculada: un reto nuevo da 409 («ya está vinculada»). | instance.logout y después un reto nuevo. |
En la prueba A3 (15/10/2026), después de cerrar la sesión desde el teléfono, la instancia volvió
a conectado con un QR nuevo en unos 6 segundos desde el escaneo, sin reiniciar el motor. Al
desvincular desde el motor (A4), el teléfono mostró la sesión como «Cierre de sesión pendiente» y
la quitó en menos de un minuto.
Volver a vincular el mismo número conserva lo que la instancia ya sabía: en la prueba del
21/10/2026 (falla F23), un iPhone desvinculado y vuelto a vincular con código quedó conectado
en la generación 2, con sus chats, sus contactos y sus mensajes anteriores, sin duplicados.
Errores comunes
Sección titulada «Errores comunes»| Respuesta | Causa | Qué hacer |
|---|---|---|
| 404 «No existe esa instancia» | Es de otra app, está fuera del alcance de la key o el ID está mal. | Revisa whoami. |
| 409 «La instancia ya está vinculada» | Pediste un reto con un número conectado. | Desvincula antes, si de verdad quieres cambiarlo. |
| 409 «La instancia está archivada» | Archivar es definitivo. | Crea otra instancia. |
| 409 «La generación no coincide» | generacion_esperada vieja: alguien pidió otro intento. |
Vuelve a leer el emparejamiento. |
422 body.telefono |
El número no tiene + y un código de país válido. |
Escríbelo en formato internacional. |
422 body.modo |
El modo no es qr ni codigo. |
— |
| 503 | El transporte de WhatsApp no está disponible, o este proceso del motor está en espera. | Reintenta en unos segundos; si sigue, avisa al operador. |
Límites
Sección titulada «Límites»- El cupo de instancias es por app: 3 de fábrica, configurable en Ajustes o por app. Las archivadas no cuentan.
- Una key restringida no crea instancias.
- Un dispositivo, una instancia. El motor no deja que el mismo dispositivo vinculado quede en dos instancias. El mismo número sí puede vincularse en dos instancias, como dos dispositivos.
- Lo que cambia según el teléfono y la cuenta está en Limitaciones de WhatsApp.
Siguiente paso
Sección titulada «Siguiente paso»Con la instancia en conectado, manda el primer mensaje.