Esta ampliación opcional usa Kapso/WhatsApp como ejemplo de transporte. Se trabaja sobre el kit extraído, no sobre un repositorio privado ni sobre una cuenta real. El objetivo es entender el código y proponer una configuración propia; los comandos de proveedor son tutoriales, no pasos verificados en vivo.
Objetivo: entender cómo se separan la entrada y la salida en el adaptador de Kapso y proponer una configuración propia.
Requisitos claros
Necesitas Python 3.10+, el kit, una cuenta/número elegible de Kapso y un endpoint HTTPS público que controles solo si decides hacer una integración real. Para leer los fixtures offline no necesitas cuenta, API key, red ni mensajes. No uses datos de clientes.
Un webhook recibe una solicitud entrante; su firma HMAC se calcula sobre los bytes exactos del body. Un worker posterior reclama un evento, consulta el núcleo y crea una intención. La allowlist de remitentes filtra quién puede entrar. dry-run procesa y muestra una intención sin POST de salida.
Tablas y transiciones reales
En kapso.py, inbound_events y outbound_intents son tablas distintas. claim_next() cambia el evento entrante a processing; ensure_intent() crea una intención saliente pending; _transition() actualiza ambas filas de forma atómica cuando hay resultado. release_preview() devuelve solo el evento entrante de processing a received para volver a previsualizarlo.
Estado de inbound_events:
received → processing → sent | failed | unknown
└─ preview → received
Estado independiente de outbound_intents:
pending → sent | failed | unknown
Las filas se alinean por message_id, pero no son una única máquina de estados: un evento puede estar received mientras no existe intención, o processing mientras su intención está pending. El ACK HTTP es una secuencia de request, no un estado de tabla:
HTTP request → verificar firma y payload → persistir → 200 OK
Un unknown significa que el proveedor no confirmó el resultado. resolve_unknown() solo acepta el par actual unknown/unknown: --status failed registra que no se continuará y --status sent --provider-id <ID> requiere confirmación externa; ninguno hace otro POST.
Ejercicio: recepción y estados locales
Usa fixtures/kapso-simple.json y fixtures/kapso-batch.json con las pruebas del kit. El batch puede contener dos mensajes y una identidad BSUID; eso prueba parsing y persistencia local, no entrega de WhatsApp.
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest \
test_kapso.KapsoTests.test_parse_simple_y_batch_incluye_bsuid \
test_kapso.KapsoTests.test_http_local_real_persiste_y_ack_200 \
test_kapso.KapsoTests.test_resultado_ambiguo_no_se_reintenta -v
python3 kapso.py inspect --db "$(mktemp -d)/kapso.sqlite3"
La última orden devuelve un estado vacío porque no hay evento persistido en esa base nueva: no la interpretes como respuesta preview. Cuando sí exista un evento local, python3 kapso.py process procesa como máximo uno y, sin --send, devuelve una preview y libera el mismo evento para otra revisión. No es un drenaje automático de la cola.
Tutorial de integración real (opcional, sin afirmar prueba)
Estos son pasos ejecutables para tu entorno, no una ejecución del curso. Necesitas Python 3.10+, el kit extraído, una cuenta/número elegible, KAPSO_PHONE_NUMBER_ID, KAPSO_WEBHOOK_SECRET, KAPSO_API_KEY y un destino HTTPS público que controles. Carga secretos desde un gestor; nunca pongas claves reales en el archivo, Git, URL o captura.
En una terminal Bash, carga los valores reales sin pegarlos en comandos guardados en el historial. No actives set -x. Este receptor se queda abierto hasta detenerlo con Ctrl+C:
read -r -p 'phone_number_id de tu cuenta: ' KAPSO_PHONE_NUMBER_ID
read -r -s -p 'Secreto del webhook (oculto): ' KAPSO_WEBHOOK_SECRET
printf '\n'
export KAPSO_PHONE_NUMBER_ID KAPSO_WEBHOOK_SECRET
export KAPSO_DB="$HOME/.local/share/agente-catalogo/kapso.sqlite3"
# KAPSO_ALLOWED_SENDERS filtra remitentes; en esta receta se exige para no
# exponer el receptor. Vacío significa aceptar cualquier remitente que pase
# los demás filtros, así que fija el identificador exacto de tu prueba.
read -r -p 'Identificador exacto del remitente de prueba: ' KAPSO_ALLOWED_SENDERS
export KAPSO_ALLOWED_SENDERS
: "${KAPSO_ALLOWED_SENDERS:?No expongas el receptor sin tu remitente de prueba}"
python3 kapso.py serve --host 127.0.0.1 --port 8080
En otra terminal, entra en la misma carpeta del kit y usa la misma ruta de base de datos. Las variables de la primera terminal no se copian automáticamente:
export KAPSO_DB="$HOME/.local/share/agente-catalogo/kapso.sqlite3"
python3 kapso.py inspect
python3 kapso.py process
Si todavía no registraste el webhook ni llegó un evento aceptado, process devuelve empty: no hay nada que previsualizar. Una vez configurada la recepción y revisados destinatario e intención, este bloque sí autoriza un envío real de un evento. No lo ejecutes como parte de la práctica offline:
read -r -s -p 'API key de Kapso (oculta): ' KAPSO_API_KEY
printf '\n'
export KAPSO_API_KEY
export KAPSO_ALLOW_SEND=1
python3 kapso.py process --send
unset KAPSO_API_KEY KAPSO_ALLOW_SEND
La recuperación se hace por separado, solo para un par de estados unknown. Sustituye los valores de ejemplo; elige una alternativa, no ambas:
# Cerrar sin volver a enviar:
python3 kapso.py resolve 'wamid.REEMPLAZAR' --status failed
# Registrar enviado solo con confirmación externa e ID real del proveedor:
python3 kapso.py resolve 'wamid.REEMPLAZAR' --status sent --provider-id 'ID_CONFIRMADO'
KAPSO_ALLOWED_SENDERS no es obligatorio para leer los fixtures offline: si queda vacío, parse_webhook acepta cualquier remitente que pase los demás filtros. La receta de arriba sí lo exige (:?) porque ese script sí queda escuchando recepción externa: antes de exponerla, fija el identificador exacto de tu prueba.
resolve es recuperación manual y no hace POST. process atiende como máximo un evento por invocación; sin --send deja la intención pendiente y libera el evento para otra preview, no drena la cola.
Para registrar el webhook, carga PUBLIC_WEBHOOK_URL con tu URL HTTPS terminada exactamente en /webhook/kapso y usa la receta segura del README.md del kit descargable (sección «Registrar un webhook sin exponer secretos»): crea un archivo temporal aleatorio con modo 600, rellénalo desde fixtures/kapso-webhook-registration.json, usa KAPSO_API_KEY solo en el header X-API-Key y bórralo con trap al terminar. No cambies el endpoint ni ejecutes curl hasta revisar la cuenta, el body raw/HMAC, el destinatario y el entorno.
El servidor incluido escucha loopback y no termina TLS; un proxy/túnel HTTPS es responsabilidad de tu entorno. Ningún paso de esta lección se ejecuta contra Kapso y no se afirma que un negocio real esté sincronizado.
Resultado observable
Puedes dibujar los dos estados, señalar dónde ocurre el ACK, por qué la firma usa raw-body y qué significa unknown. También puedes demostrar que los fixtures offline pasan. La cuenta, endpoint HTTPS, API de Kapso y entrega de mensajes quedan sin validar.
Error habitual y recuperación
empty puede indicar que consultas otra base: compara KAPSO_DB en las dos terminales. Si no llegan eventos, revisa primero URL pública, ruta, firma y remitente autorizado; no vacíes la allowlist para saltarte el filtro. Si recibes unknown, inspecciona y reconcilia: repetir process --send no es una estrategia de recuperación. SQLite contiene identificadores y textos; no publiques la base ni sus copias.