Ir al contenido

Interactivos

Un interactivo es un mensaje con algo que tocar: botones (responder, copiar un código, abrir un enlace, llamar o un pago PIX), una lista de opciones o un carrusel de tarjetas. Las encuestas son su pariente: el contacto vota y el motor cuenta. Las respuestas y los votos llegan como eventos propios, correlacionados con el mensaje que los originó, para que tu app sepa qué eligió cada contacto sin interpretar textos.

Operación Scope
POST /v1/instances/{id}/messages con tipo: interactive o tipo: poll messages:send
GET /v1/instances/{id}/interactive/{provider_id} (seguimiento) messages:read
GET /v1/instances/{id}/interactive-responses (respuestas) messages:read
GET /v1/instances/{id}/polls/{provider_id} (resultado de una encuesta) messages:read
GET /v1/instances/{id}/events (los eventos de respuesta) events:read

tipo: interactive con interactivo.variante: buttons. cuerpo es obligatorio; encabezado y pie son opcionales. Los botones son uno de estos grupos (no se mezclan):

  • hasta 3 de accion: reply (responder), cada uno con su id (único en el mensaje, hasta 200 caracteres) y su texto;
  • hasta 2 entre copy (copiar codigo), url (abrir un enlace http(s)) y call (abrir el marcador con telefono; no es una llamada del motor);
  • un botón pix, con pix.comercio, pix.clave y pix.tipo_clave (phone, email, cpf, cnpj o random). Es información de pago, no una pasarela: el motor nunca ve un pago.

Solo reply produce una respuesta. Un clic en copiar, en un enlace, en llamar o en PIX nunca llega: WhatsApp no lo informa, y el motor no lo inventa.

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/messages \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: confirmar-entrega-42" \
-d '{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "interactive",
"interactivo": {
"variante": "buttons",
"encabezado": "Tu pedido 42",
"cuerpo": "¿Confirmas la entrega para mañana?",
"pie": "Tienda Centro",
"botones": [
{"accion": "reply", "id": "confirmar-42", "texto": "Sí, confirmo"},
{"accion": "reply", "id": "cambiar-42", "texto": "Cambiar fecha"}
]
}
}'
202 Accepted
{
"command_id": "5c2e7a10-0000-4000-8000-0000000000c6",
"instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1",
"idempotency_key": "confirmar-entrega-42",
"type": "message.interactive",
"generation": 1,
"status": "en_cola",
"created_at": "2026-10-24T15:30:00Z",
"updated_at": "2026-10-24T15:30:00Z"
}

El SDK de Go trae los atajos motor.Responder, motor.CopiarCodigo, motor.AbrirEnlace, motor.Llamar y motor.BotonPIX para cada botón, y motor.Botones, motor.Lista y motor.Carrusel para cada variante.

variante: list: un botón (boton_lista, hasta 20 caracteres) que abre de 1 a 10 secciones, con 10 filas en total como máximo. Cada fila lleva id (único en la lista) y titulo, y puede llevar descripcion. El titulo de cada sección es obligatorio si hay más de una.

Con respaldo_texto: true, cuando WhatsApp acepta la lista el motor manda además al mismo chat un texto con el cuerpo y las filas numeradas («1. 09:00», «2. 11:00»…). Si alguien de ese chat contesta solo con uno de esos números en las 24 horas siguientes, llega su message.received normal y, además, un interactive.response con formato: texto, via: texto y la fila correlacionada. Cuenta la última lista con respaldo aceptada en ese chat.

Cuerpo de una lista con respaldo de texto
{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "interactive",
"interactivo": {
"variante": "list",
"encabezado": "Horarios",
"cuerpo": "Elige un horario para tu cita",
"boton_lista": "Ver horarios",
"respaldo_texto": true,
"secciones": [
{"titulo": "Mañana", "filas": [
{"id": "h-0900", "titulo": "09:00", "descripcion": "Sede Centro"},
{"id": "h-1100", "titulo": "11:00"}
]},
{"titulo": "Tarde", "filas": [{"id": "h-1600", "titulo": "16:00"}]}
]
}
}

variante: carousel: de 2 a 10 tarjetas. Cada tarjeta lleva cuerpo (hasta 160 caracteres), pie opcional, de 1 a 2 botones (reply, copy, url o call; PIX no va en un carrusel) y un medio.media_id opcional, una subida de esta instancia ya lista (ver Multimedia): una imagen JPEG o PNG o un vídeo MP4. O todas las tarjetas llevan medio o ninguna, y todas del mismo tipo (imágenes o vídeos). El motor sube cada medio a WhatsApp dentro del mismo envío.

Cuerpo de un carrusel
{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "interactive",
"interactivo": {
"variante": "carousel",
"cuerpo": "Nuestros planes",
"tarjetas": [
{"medio": {"media_id": "0b9d6c7e-0000-4000-8000-0000000000f4"}, "cuerpo": "Plan básico",
"botones": [{"accion": "reply", "id": "plan-basico", "texto": "Lo quiero"}]},
{"medio": {"media_id": "0b9d6c7e-0000-4000-8000-0000000000f5"}, "cuerpo": "Plan completo",
"botones": [{"accion": "reply", "id": "plan-completo", "texto": "Lo quiero"},
{"accion": "url", "texto": "Detalles", "url": "https://crm.ejemplo.com/planes"}]}
]
}
}

Los tres se arman como mensajes native flow de WhatsApp (estrategia mw10.2, que queda en el seguimiento de cada envío como version): reply sale como quick_reply, copy como cta_copy, url como cta_url, call como cta_call, pix como payment_info, y la lista como un single_select. No hay un envío de native flow libre: solo estas variantes. Las respuestas, en cambio, llegan en el formato que elige el teléfono que responde (ver Respuestas).

El motor los revisa al aceptar; un interactivo que no cumple es 422 interactivo_invalido, con el campo en errors (por ejemplo, body.interactivo.botones[1].url).

Campo Límite
cuerpo Obligatorio, hasta 1024 caracteres (160 en una tarjeta)
encabezado, pie Hasta 60 caracteres
botones[].texto, boton_lista Hasta 20 caracteres
botones[].id, filas[].id Hasta 200 caracteres, únicos en el mensaje
secciones De 1 a 10, con 10 filas en total; título de sección hasta 24
filas[].titulo / filas[].descripcion Hasta 24 / hasta 72 caracteres
tarjetas De 2 a 10, cada una con 1 o 2 botones
pix.clave En el formato de su tipo_clave (CPF y CNPJ con sus dígitos verificadores)

Ningún texto admite caracteres de control.

tipo: poll con pregunta (hasta 255 caracteres), opciones (de 2 a 12, distintas, de hasta 100 caracteres) y multiple (true: se puede elegir más de una). El motor guarda sellado el secreto de la encuesta: un reintento manda la misma y los votos se pueden descifrar aunque el motor se reinicie.

Ventana de terminal
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/messages \
-H "Authorization: Bearer TU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: encuesta-dia-entrega-42" \
-d '{
"destino": {"telefono": "+NUMERO_DEL_CONTACTO"},
"tipo": "poll",
"pregunta": "¿Qué día te queda mejor?",
"opciones": ["Lunes", "Miércoles", "Viernes"]
}'
# El resultado agregado, por el provider_id del mensaje
curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/polls/PROVIDER_MESSAGE_ID \
-H "Authorization: Bearer TU_API_KEY"
GET …/polls/{provider_id} · 200 OK
{
"provider_id": "PROVIDER_MESSAGE_ID",
"conocida": true,
"pregunta": "¿Qué día te queda mejor?",
"seleccionables": 1,
"opciones": [
{ "opcion": "Lunes", "votos": 0 },
{ "opcion": "Miércoles", "votos": 1 },
{ "opcion": "Viernes", "votos": 0 }
],
"votantes": 1,
"sin_descifrar": 0
}

Cuenta el voto vigente de cada votante: un cambio de voto reemplaza el anterior, y quitarlo lo deja vacío. seleccionables es 1 (una opción) o 0 (varias). Un voto que no se pudo descifrar se cuenta en sin_descifrar, nunca como una opción inventada. Una encuesta que llega (creada por el contacto) también se cuenta: su message.received trae contenido.tipo: encuesta y contenido.encuesta con pregunta, opciones y seleccionables. Si llegan votos de una encuesta que el motor no vio crear, conocida es false y solo se cuentan los votantes.

El type del comando es message.interactive o message.poll, y sigue el ciclo de cualquier envío: message.sent cuando WhatsApp lo acepta, message.updated con los recibos y command.failed o command.ambiguous si no (ver Comandos asíncronos y eventos).

Para un interactivo, GET /v1/instances/{id}/interactive/{provider_id} separa lo que el motor sabe de lo que no:

Campo Valores Qué dice
aceptacion pendiente, aceptado, rechazado, no_salio, desconocido Si el servidor de WhatsApp lo aceptó (desconocido si quedó ambiguo).
recibo no_observado, entregado, leido El mejor recibo que llegó.
render desconocido Siempre: el motor no puede saber si el teléfono lo dibujó.
respuesta no_observada, observada, no_observable observada solo con una respuesta real; no_observable si solo lleva copy, url, call o pix.

Trae además respuestas (los event_id de sus interactive.response), acciones (cada botón o fila, con su accion, id, titulo, seccion o tarjeta), variante, version y aviso.

Ventana de terminal
curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/interactive/PROVIDER_MESSAGE_ID \
-H "Authorization: Bearer TU_API_KEY"

Cada respuesta deja un evento durable, deduplicado por su provider_id:

Evento Cuándo
interactive.response Llegó una respuesta (un botón, una fila, una tarjeta o el número del respaldo de texto).
interactive.unknown Llegó algo con forma de respuesta que el motor no pudo leer; trae motivo y crudo (saneado y acotado).
interactive.response.updated Cambió la correlación de una respuesta ya publicada (ver abajo). Nunca es una segunda respuesta.
poll.vote Un voto de una encuesta, con el resultado agregado.
interactive.response · data
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"autor": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] },
"provider_id": "PROVIDER_MESSAGE_ID_DE_LA_RESPUESTA",
"from_me": false,
"formato": "buttons_response",
"mensaje": { "provider_id": "PROVIDER_MESSAGE_ID", "command_id": "5c2e7a10-0000-4000-8000-0000000000c6" },
"accion": { "id": "confirmar-42", "titulo": "Sí, confirmo", "accion": "reply" },
"correlacion": "correlacionada",
"momento": "2026-10-24T15:31:10Z"
}
  • formato es el que mandó el teléfono: buttons_response, list_response, template_button_reply, native_flow (con nombre_flujo y parametros), texto (el respaldo de una lista) o desconocida.
  • accion trae el id y el titulo elegidos y, si se correlacionó, la accion del interactivo (reply o list_row) con su seccion (una fila) o su tarjeta (un carrusel, desde 0). indice es el botón, si el formato lo informa.
  • mensaje es el mensaje al que responde, con el command_id del envío si se correlacionó, y su autor en un grupo.
  • correlacion:
correlacion Qué significa
correlacionada Responde a un interactivo de esta instancia y de este chat, con una acción que existe.
accion_desconocida El mensaje es de la instancia, pero el id elegido no está entre sus acciones.
sin_mensaje No responde a un interactivo que esta instancia haya mandado (por ejemplo, otra instancia del mismo número).
otro_chat El interactivo es de la instancia, pero de otro chat.
otro_autor El contexto dice que el mensaje citado es de otro autor, no de esta instancia.
no_respondible Responde a un interactivo de la instancia que no tiene nada que responder (solo copy, url, call o pix).
desconocida No se pudo leer: llegó como interactive.unknown.

otro_chat y otro_autor se revisan solos: si después la instancia comprueba el alias PN/LID que faltaba, la misma respuesta (mismo response_id y mismo event_id original, en response_event_id) se vuelve a correlacionar y llega interactive.response.updated con correlacion_anterior.

Responder citando un interactivo con un texto normal no es una respuesta interactiva: llega como message.received con su cita.

poll.vote · data
{
"chat": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": [] },
"votante": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": [] },
"provider_id": "PROVIDER_MESSAGE_ID_DEL_VOTO",
"encuesta": "PROVIDER_MESSAGE_ID",
"from_me": false,
"descifrado": true,
"opciones": ["Miércoles"],
"vigente": true,
"momento": "2026-10-24T15:33:00Z",
"resultado": {
"provider_id": "PROVIDER_MESSAGE_ID",
"conocida": true,
"pregunta": "¿Qué día te queda mejor?",
"seleccionables": 1,
"opciones": [{ "opcion": "Lunes", "votos": 0 }, { "opcion": "Miércoles", "votos": 1 }, { "opcion": "Viernes", "votos": 0 }],
"votantes": 1,
"sin_descifrar": 0
}
}

opciones vacío es un voto quitado. vigente: false es un voto más viejo que el que ya estaba: llega, pero no cambia el resultado.

GET /v1/instances/{id}/interactive-responses lista las respuestas, las más nuevas primero, con next_cursor y has_more. Filtros: provider_id (las de un interactivo), correlacion, cursor y limite.

Ventana de terminal
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/interactive-responses?provider_id=PROVIDER_MESSAGE_ID" \
-H "Authorization: Bearer TU_API_KEY"
HTTP Código Cuándo
404 medio_no_encontrado El medio de una tarjeta no existe en esta instancia (o es de otra): el campo va en errors[].location (por ejemplo, body.interactivo.tarjetas[0].medio.media_id).
422 interactivo_invalido No cumple los límites, o el medio de una tarjeta no está listo o no es JPEG, PNG o MP4: el campo va en errors[].location.
422 — Una encuesta sin pregunta o sin 2 a 12 opciones distintas de hasta 100 caracteres (body.opciones).
422 cita_ajena La cita del interactivo no es de este chat.
404 — GET …/interactive/{provider_id} o …/polls/{provider_id} de un mensaje que no es de esta instancia.

Un interactivo nunca se rechaza por ser de un número no oficial: si WhatsApp no lo acepta, el comando termina fallido (rechazado_por_whatsapp) y el seguimiento dice rechazado.

Lo comprobado con un iPhone (WhatsApp 26.40) y Android (2.26.40), del 19 al 21 de octubre de 2026:

Variante Android iPhone Respuesta que llega
Botones de respuesta (casos A72, A79) Se ven y se tocan Se ven y se tocan iPhone: buttons_response; Android: template_button_reply
Copiar, enlace y llamar (A73, A75, A76, A82) Se ven y funcionan Se ven y funcionan Ninguna (no_observable)
PIX (A74, A82) Llega y no se muestra Llega y no se muestra Ninguna
Lista (A77, A80) Se ve y se responde (list_row; probado en un Android Business) No se ve native_flow (en Android)
Lista con respaldo de texto (A77, 21/10) — Se ve el texto numerado texto, via: texto
Carrusel con imágenes (A78, A81) Se ve y se toca Se ve y se toca template_button_reply, con el índice de la tarjeta
Encuesta (A29) Se vota y se cambia Se vota y se cambia poll.vote descifrado
  • Listas en iOS (caso A77, falla F25): iOS no dibuja la lista native flow, la mande una cuenta normal o una Business. Cerrado como limitación de WhatsApp el 20/10/2026. Usa respaldo_texto o botones. Ver Listas en iOS.
  • La fila elegida en Android llegaba como interactive.unknown con el flujo menu_options; se corrigió y ahora llega list_row correlacionada (falla F32, repetida el 21/10/2026).
  • PIX llega a la sesión del destino pero ningún teléfono lo dibuja fuera de Brasil; otra instancia del motor lo recibe como no_soportado (casos A74 y A82). Ver Pagos PIX.
  • Clics que no se informan: tocar copiar, un enlace o llamar no produjo ningún evento ni ningún call.* (caso A82, 19/10/2026).
  • Con la red cortada: un botón tocado mientras el motor no tenía red llegó una sola vez al volver (caso A83, 19/10/2026).
  • En un grupo: las respuestas llegan correlacionada con el autor del grupo, con los mismos formatos que en un chat directo (caso A84, 19/10/2026).
  • Votos con demora: un voto desde el iPhone llegó unos 108 s después de tocarlo, a todas las instancias a la vez: la demora es del teléfono o de WhatsApp (caso A29, 17 y 18/10/2026).