Integración WhatsApp
La integración de WhatsApp permite a los miembros interactuar con su club directamente desde WhatsApp. Utiliza KAPSO como plataforma de mensajería para gestionar webhooks, envío de mensajes y el cifrado de WhatsApp Flows.
Todos los webhooks entrantes son verificados mediante firma HMAC-SHA256 proporcionada por KAPSO en el header X-Webhook-Signature. La clave de verificación se configura en la variable de entorno KAPSO_WEBHOOK_SECRET.
Arquitectura
Configuracion
Variables de Entorno
- Name
KAPSO_API_KEY- Type
- string
- Description
API key de KAPSO para enviar mensajes.
- Name
KAPSO_WEBHOOK_SECRET- Type
- string
- Description
Secret para verificar firmas HMAC-SHA256 de webhooks entrantes.
- Name
KAPSO_PHONE_NUMBER_ID- Type
- string
- Description
ID del numero de telefono registrado en KAPSO/Meta.
- Name
WHATSAPP_FLOW_ID- Type
- string
- Description
ID del WhatsApp Flow para vinculacion de cuentas.
- Name
WHATSAPP_WABA_ID- Type
- string
- Description
ID de la WhatsApp Business Account.
Configuracion (config/whatsapp.ts)
export const whatsappConfig = {
kapsoApiKey: process.env.KAPSO_API_KEY,
webhookSecret: process.env.KAPSO_WEBHOOK_SECRET,
phoneNumberId: process.env.KAPSO_PHONE_NUMBER_ID,
flowId: process.env.WHATSAPP_FLOW_ID,
wabaId: process.env.WHATSAPP_WABA_ID,
}
export const WHATSAPP_TENANTS = [
{
tenantId: '9dcb1d33-...',
name: 'Medicann',
slug: 'medicann',
},
{
tenantId: '4c02b989-...',
name: 'High Up',
slug: 'high-up',
},
]
Recibir webhook
Recibe eventos de webhook enviados por KAPSO. Este endpoint procesa mensajes entrantes de WhatsApp y los delega al handler de conversacion.
Headers de KAPSO
- Name
X-Webhook-Signature- Type
- string
- Description
Firma HMAC-SHA256 del body en formato hex. Se verifica contra
KAPSO_WEBHOOK_SECRET.
- Name
X-Webhook-Event- Type
- string
- Description
Tipo de evento. Solo se procesan eventos
whatsapp.message.received.
- Name
X-Idempotency-Key- Type
- string
- Description
Clave de idempotencia para evitar procesamiento duplicado.
Payload del Evento
- Name
message- Type
- object
- Description
Objeto del mensaje entrante.
- Name
id- Type
- string
- Description
ID unico del mensaje (ej.
wamid.123).
- Name
from- Type
- string
- Description
Numero de telefono del remitente en formato E.164.
- Name
type- Type
- string
- Description
Tipo de mensaje:
text,interactive,button.
- Name
text- Type
- object
- Description
Contenido de mensaje de texto. Contiene
body.
- Name
interactive- Type
- object
- Description
Respuesta interactiva (
button_replyolist_reply).
- Name
button- Type
- object
- Description
Quick reply de template. Contiene
payloadytext.
- Name
conversation- Type
- object
- Description
Metadata de la conversacion en KAPSO.
- Name
phone_number_id- Type
- string
- Description
ID del numero de telefono receptor.
Este endpoint siempre retorna 200 independientemente de errores internos, para evitar que KAPSO reintente el envio.
Request
curl -X POST https://api.cannahub.tech/api/whatsapp/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Event: whatsapp.message.received" \
-H "X-Webhook-Signature: a1b2c3d4..." \
-H "X-Idempotency-Key: evt_abc123" \
-d '{
"message": {
"id": "wamid.HBgNNTQ5...",
"from": "5491155551234",
"type": "text",
"text": { "body": "Hola" }
},
"conversation": {
"id": "conv_123",
"phone_number": "5491155551234"
},
"phone_number_id": "123456789"
}'
Response (siempre 200)
{
"success": true
}
Ejemplo: Interactive button reply
{
"message": {
"id": "wamid.HBgNNTQ5...",
"from": "5491155551234",
"type": "interactive",
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "btn_ver_menu",
"title": "Ver Menu"
}
}
}
}
Flow endpoint
Endpoint de datos para WhatsApp Flows. KAPSO maneja el cifrado/descifrado y llama a este endpoint con el payload ya descifrado. Gestiona las pantallas del flow de vinculacion de cuenta.
Pantallas del Flow
| Pantalla | Accion | Siguiente |
|---|---|---|
CLUB_SELECT | Seleccionar club/tenant | LOGIN |
LOGIN | Autenticacion con Medusa | SUCCESS |
COMPLETE | Accion de finalizacion | SUCCESS |
Payload de Entrada
- Name
data_exchange- Type
- object
- Description
Datos del intercambio de flow.
- Name
screen- Type
- string
- Description
Pantalla actual:
CLUB_SELECT,LOGIN.
- Name
data- Type
- object
- Description
Datos de la pantalla (varia segun la pantalla).
- Name
flow_token- Type
- string
- Description
Token del flow (numero de telefono del usuario).
- Name
action- Type
- string
- Description
COMPLETEcuando el flow finaliza.
Logica por Pantalla
CLUB_SELECT: Recibe club_id en data, resuelve el nombre del tenant y navega a LOGIN.
LOGIN: Recibe email y password en data. Autentica contra Medusa (/auth/customer/emailpass), obtiene el perfil del cliente, y almacena la cuenta vinculada en KV con expiracion de token de 7 dias.
COMPLETE: Retorna respuesta de exito sin procesamiento adicional.
Request
curl -X POST https://api.cannahub.tech/api/whatsapp/flow-endpoint \
-H "Content-Type: application/json" \
-d '{
"source": "whatsapp_flow",
"data_exchange": {
"screen": "CLUB_SELECT",
"data": { "club_id": "9dcb1d33-..." },
"flow_token": "5491155551234"
},
"signature_valid": true
}'
Response (CLUB_SELECT -> LOGIN)
{
"version": "3.0",
"screen": "LOGIN",
"data": {
"club_name": "High Up",
"error_message": " ",
"has_error": false
}
}
Response (LOGIN -> SUCCESS)
{
"version": "3.0",
"screen": "SUCCESS",
"data": {
"member_name": "Juan Perez",
"club_name": "High Up"
}
}
Response (LOGIN error)
{
"version": "3.0",
"screen": "LOGIN",
"data": {
"club_name": "High Up",
"error_message": "Email o contraseña incorrectos. Intenta de nuevo.",
"has_error": true
}
}
Crear codigo de vinculacion
Crea un codigo de vinculacion pendiente. Se invoca desde la pagina de vinculacion web despues de un login exitoso. El miembro luego envia LINK-{codigo} al bot para completar la vinculacion.
Campos Requeridos
- Name
customerId- Type
- string
- Description
ID del cliente en Medusa.
- Name
tenantId- Type
- string
- Description
ID del tenant/club.
- Name
email- Type
- string
- Description
Email del miembro (debe ser un email valido).
- Name
name- Type
- string
- Description
Nombre del miembro.
- Name
token- Type
- string
- Description
Token JWT de Medusa del miembro.
Comportamiento
- Genera un UUID como codigo de vinculacion
- El codigo expira en 10 minutos
- Retorna status
201con el codigo generado
Request
curl -X POST https://api.cannahub.tech/api/whatsapp/link \
-H "Content-Type: application/json" \
-d '{
"customerId": "cus_01HQ8ABC456",
"tenantId": "9dcb1d33-fd1b-4ff7-b1de-d5c118476fa1",
"email": "miembro@ejemplo.com",
"name": "Juan Perez",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI..."
}'
Response (201)
{
"code": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Error: Validacion (400)
{
"error": "Invalid request body",
"details": {
"errors": {
"email": ["Invalid email"]
}
}
}
Validar codigo de vinculacion
Valida y consume un codigo de vinculacion pendiente. Invocado por el bot cuando recibe un mensaje LINK-{codigo} de un miembro. El codigo se elimina despues de ser consumido (uso unico).
Parametros de Ruta
- Name
code- Type
- string
- Description
Codigo de vinculacion UUID. Minimo 10 caracteres.
Response
- Name
customerId- Type
- string
- Description
ID del cliente en Medusa.
- Name
tenantId- Type
- string
- Description
ID del tenant/club.
- Name
tenantName- Type
- string
- Description
Nombre del club.
- Name
email- Type
- string
- Description
Email del miembro.
- Name
name- Type
- string
- Description
Nombre del miembro.
- Name
token- Type
- string
- Description
Token JWT de Medusa.
Request
curl https://api.cannahub.tech/api/whatsapp/link/a1b2c3d4-e5f6-7890-abcd-ef1234567890
Response (200)
{
"customerId": "cus_01HQ8ABC456",
"tenantId": "9dcb1d33-fd1b-4ff7-b1de-d5c118476fa1",
"tenantName": "High Up",
"email": "miembro@ejemplo.com",
"name": "Juan Perez",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI..."
}
Error: Codigo expirado (404)
{
"error": "Link code not found"
}
Error: Codigo invalido (400)
{
"error": "Invalid link code"
}
Flujo de Vinculacion de Cuenta
El sistema soporta dos metodos de vinculacion entre una cuenta de Cannahub y un numero de WhatsApp:
Metodo 1: WhatsApp Flow (dentro de WhatsApp)
Metodo 2: Codigo de Vinculacion (desde la web)
Tipos de Mensaje Soportados
| Tipo | Campo | Descripcion |
|---|---|---|
text | message.text.body | Mensaje de texto plano del usuario |
interactive (button) | message.interactive.button_reply | Respuesta a boton interactivo (id + title) |
interactive (list) | message.interactive.list_reply | Seleccion de lista interactiva (id + title) |
button | message.button.payload | Quick reply de template (payload + text) |
Estados de Conversacion
El bot mantiene un estado de conversacion por telefono para gestionar el flujo de interaccion:
- Name
UNLINKED- Type
- inicial
- Description
Sin cuenta vinculada. Se solicita vinculacion.
- Name
AWAITING_LINK_CODE- Type
- vinculacion
- Description
Esperando que el miembro envie un codigo LINK.
- Name
AWAITING_TENANT_SELECTION- Type
- vinculacion
- Description
Seleccion de club (multi-tenant).
- Name
MAIN_MENU- Type
- activo
- Description
Menu principal del bot.
- Name
BROWSING_PRODUCTS- Type
- compra
- Description
Navegando el catalogo de productos.
- Name
AWAITING_QUANTITY- Type
- compra
- Description
Esperando cantidad para un producto seleccionado.
- Name
VIEWING_CART- Type
- compra
- Description
Viendo el carrito de compras.
- Name
CONFIRMING_ORDER- Type
- compra
- Description
Confirmando pedido antes de procesar.
Maquina de estados
type ConversationState =
| 'UNLINKED'
| 'AWAITING_LINK_CODE'
| 'AWAITING_TENANT_SELECTION'
| 'MAIN_MENU'
| 'BROWSING_PRODUCTS'
| 'AWAITING_QUANTITY'
| 'VIEWING_CART'
| 'CONFIRMING_ORDER'
Sesion de conversacion
interface ConversationSession {
phone: string
state: ConversationState
tenantId?: string
selectedProductId?: string
selectedVariantId?: string
cart: CartItem[]
cartId?: string
lastActivity: number
}
Arquitectura del Dominio
src/
├── app/api/whatsapp/
│ ├── webhook/route.ts # Webhook de KAPSO
│ ├── flow-endpoint/route.ts # WhatsApp Flows data endpoint
│ └── link/
│ ├── route.ts # POST - Crear codigo de vinculacion
│ └── [code]/route.ts # GET - Validar/consumir codigo
├── config/
│ └── whatsapp.ts # Configuracion y tenants
└── features/WhatsApp/
├── conversation/
│ └── handler.ts # Message handler (state machine)
├── services/
│ └── linked-accounts.service.ts # KV storage para cuentas
├── utils/
│ └── signature.ts # Verificacion HMAC-SHA256
└── types.ts # Tipos del dominio