1. Qué es KYC en Structura
KYC (*Know Your Customer*) es la verificación de identidad del Owner del Tenant. Confirma
que la persona detrás de un comercio es quien dice ser, usando el proveedor externo Didit
(Hosted Session).
2. Por qué es opcional en esta versión
El MVP actual nunca bloquea ventas, pedidos, storefront ni ninguna operación del Tenant por
no tener KYC aprobado. Es un paso opcional del checklist "Configurá tu tienda" y un banner
informativo en el Home — nada más. Una versión futura podría atarlo a habilitar funcionalidades
puntuales de cobros, pero eso no está implementado todavía.
3. Quién puede iniciarlo
Solo el Owner del Tenant (o super_admin de plataforma). La autorización se valida
server-side en cada request — nunca confía en lo que declare el frontend.
4. Configurar las credenciales de Didit
Variables de entorno (nunca en el código, nunca en el frontend):
DIDIT_API_KEY— API key de Didit (headerx-api-keyen cada request al backend de Didit).DIDIT_WORKFLOW_ID— UUID del workflow de verificación elegido en el panel de Didit
(Workflows → copiar ID completo, no el truncado que se ve en la tabla).
KYC_WEBHOOK_SECRET— el "Webhook Secret Key" que Didit muestra una sola vez al crear
el destino del webhook en *Business Console → API & Webhooks*. Si se pierde, hay que borrar
el destino y crearlo de nuevo — Didit no lo vuelve a mostrar.
En local van en .env (nunca se commitea). En producción, como variables de entorno del sitio
en Netlify.
5. Configurar el workflow
En el panel de Didit (*Workflows*), cada workflow define qué se verifica (documento, biometría,
AML, etc.) y tiene su propio costo por verificación. El workflow_id se copia desde ahí y se
carga en DIDIT_WORKFLOW_ID — nunca se hardcodea en el código.
6. Cómo se crea una sesión
El Owner aprieta "Verificar identidad" en Mi Cuenta. El frontend llama a
POST /api/kyc/session (requiere ser Owner y que el workspace tenga habilitado el switch de
Legal+KYC). El backend llama a Didit (POST /v3/session/) pasando workflow_id,
vendor_data (el tenantId, para poder resolver el tenant en el webhook) y una URL de
callback para volver a Mi Cuenta al terminar.
7. Cómo se abre la experiencia de verificación
El backend devuelve una url de Didit. El frontend redirige la página completa a esa URL
(no un popup) — los popups suelen bloquear el acceso a la cámara en navegadores móviles, y la
mayoría de los workflows piden foto de documento y/o selfie.
8. Cómo funciona el webhook
Didit notifica cambios de estado a POST /api/kyc/webhook. Cada request trae:
X-Signature— firma HMAC-SHA256 sobre el body crudo.X-Timestamp— epoch en segundos del envío.- Body JSON con
event_id,session_id,status,vendor_data(el tenantId) y,
cuando corresponde, decision.
9. Cómo se valida la firma
Se recalcula HMAC-SHA256 del body crudo (antes de parsear JSON) con KYC_WEBHOOK_SECRET
y se compara contra X-Signature con timingSafeEqual (comparación a tiempo constante, evita
timing attacks). Si no hay secreto configurado, el webhook rechaza todo — nunca procesa sin
poder verificar la firma. También se rechaza si X-Timestamp está a más de 300 segundos del
reloj del servidor (anti-replay).
10. Cómo se persiste el resultado
En sm_kyc_records (una fila por tenant/provider): status, provider_session_id,
rejection_code, approved_at. La idempotencia por event_id vive en
sm_kyc_webhook_events — un event_id repetido (reintento de Didit) no reprocesa nada.
11. Qué datos se guardan y cuáles nunca
Se guarda: tenant, provider, session id, status, código de rechazo, timestamps.
Nunca se guarda: DNI/pasaporte, imágenes del documento, selfie, biometría, ni el payload
completo que manda Didit — solo se extraen los campos de status.
12. Qué pasa ante cada resultado
- Aprobado →
approved, se guardaapproved_at. El banner del Home desaparece. - Rechazado →
rejected+rejection_codesi Didit lo informó. El Owner puede reintentar. - Expirado / abandonado →
expired. Se puede iniciar una verificación nueva. - Error de configuración (falta
DIDIT_API_KEY/DIDIT_WORKFLOW_ID) →503, sin crear
sesión.
13. Cómo probar el flujo en sandbox
Didit permite simular resultados deterministas con sandbox_scenario en modo sandbox. Para el
flujo real end-to-end (Owner → Mi Cuenta → Didit → webhook → estado actualizado) conviene
probarlo desde un celular real o el simulador iOS/Android, porque la mayoría de los workflows
piden cámara.
14. Cómo diagnosticar una sesión que quedó pendiente
- Revisar Admin → Legal → KYC (
/admin/ops/legal/kyc): filtrar por tenant o status. - Si Didit no mandó webhook, se puede consultar el fallback
GET /v3/session/{session_id}/decision/ (implementado en getDiditSessionDecision).
- Si sigue sin resolverse, un super_admin puede hacer un override manual (aprobar/rechazar)
desde esa misma pantalla — queda auditado en sm_audit_logs como KYC_ADMIN_OVERRIDE.
15. Cómo rotar/revocar la API key
Se regenera desde el panel de Didit y se actualiza DIDIT_API_KEY en las variables de entorno
(local y Netlify). No requiere cambios de código. El KYC_WEBHOOK_SECRET es independiente —
rotarlo implica recrear el destino del webhook en Didit.
16. Variables de entorno requeridas por ambiente
| Variable | Local (.env) | Producción (Netlify) |
|---|---|---|
DIDIT_API_KEY | sí | sí |
DIDIT_WORKFLOW_ID | sí | sí |
KYC_WEBHOOK_SECRET | sí | sí |
17. Evidencia de pruebas
Cobertura unitaria en tests/unit/kyc-didit-system.test.ts: mapeo de estados, verificación de
firma, ventana anti-replay, idempotencia, minimización de datos y el switch de plataforma
legal_kyc_gate. El E2E manual (Owner → Didit → webhook → estado) se corre a mano por ahora
— no hay automation de cámara/documento en CI.