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.


Arquitectura

Loading diagram...

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',
  },
]

POST/api/whatsapp/webhook

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_reply o list_reply).

    • Name
      button
      Type
      object
      Description

      Quick reply de template. Contiene payload y text.

  • Name
    conversation
    Type
    object
    Description

    Metadata de la conversacion en KAPSO.

  • Name
    phone_number_id
    Type
    string
    Description

    ID del numero de telefono receptor.

Request

POST
/api/whatsapp/webhook
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"
      }
    }
  }
}

POST/api/whatsapp/flow-endpoint

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

PantallaAccionSiguiente
CLUB_SELECTSeleccionar club/tenantLOGIN
LOGINAutenticacion con MedusaSUCCESS
COMPLETEAccion de finalizacionSUCCESS

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

      COMPLETE cuando 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

POST
/api/whatsapp/flow-endpoint
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
  }
}

POST/api/whatsapp/link

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 201 con el codigo generado

Request

POST
/api/whatsapp/link
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"]
    }
  }
}

GET/api/whatsapp/link/:code

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

GET
/api/whatsapp/link/:code
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)

Loading diagram...

Metodo 2: Codigo de Vinculacion (desde la web)

Loading diagram...

Tipos de Mensaje Soportados

TipoCampoDescripcion
textmessage.text.bodyMensaje de texto plano del usuario
interactive (button)message.interactive.button_replyRespuesta a boton interactivo (id + title)
interactive (list)message.interactive.list_replySeleccion de lista interactiva (id + title)
buttonmessage.button.payloadQuick 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

Proximos Pasos

Was this page helpful?