Aplicación de referencia para integrar los flujos embebidos de Skip en un checkout de un prestador, clínica o plataforma aliada.
Este repositorio demuestra dos casos:
Spotestándar: el paciente autoriza a Skip a registrarlo y configurar el flujo de reembolso.CNPL / AAPD(Care Now, Pay Later/Atiéndete Ahora y Paga Después): además del registro en Skip, se crea una orden enSkipPayy se inicia el flujo de financiamiento.
El objetivo de este proyecto no es implementar toda la plataforma de Skip, sino mostrar cómo un integrador externo debe:
- reunir los datos mínimos del paciente,
- inicializar el widget correctamente,
- decidir dónde manejar secretos,
- incrustar el iframe,
- reaccionar a eventos del widget,
- y separar responsabilidades entre frontend del prestador, backend del prestador,
backend-skipygokeipay-api.
Este ejemplo simula un checkout médico con una cita y, después del pago o confirmación, inicializa el widget de Skip.
Incluye:
Next.jsconPages Router- un flujo de simulación para paciente nuevo o ya registrado,
- dos modos de render del widget:
modalydivembebido, - validación local de RUT para pruebas,
- consulta de estado de suscripción con
GET /spot/is_user_subscribed, - inicialización del widget con
POST /spot/widget, - y para CNPL, una función server-side (
/api/create-order) que crea la orden enSkipPay; en la simulación local el secreto también puede ingresarse desde la UI, por lo que no debe tomarse como patrón de producción.
No incluye:
- integración POS real del prestador,
- envío de documentos vía
gastosogastos-url, - recepción/validación completa de webhooks del prestador,
- autenticación de operadores internos,
- ni administración de tenants, credenciales o proveedores.
spot-integration-example: referencia para el integrador externo.backend-skip: backend principal de Spot, registro de usuarios, beneficiarios, cuentas de seguros, leads, gastos y notificaciones.gokeipay-api: API de pagos y órdenes para CNPL / SkipPay.spot-form: widget real embebible de Skip.master-docs: documentación consolidada del negocio y de los contratos observados en la plataforma.
Frontend del prestador
- recolecta o precarga RUT, nombre, email y teléfono del paciente,
- decide si ofrecer reembolso estándar o CNPL,
- abre el iframe,
- escucha eventos de
postMessage, - y actualiza su UX según el resultado.
Backend del prestador
- llama a
POST /spot/widgetusandopublic_key, - y en CNPL llama a
POST /ordersusandoclient_secret.
Skip (backend-skip)
- crea
TempWidgetToken, - clasifica si el paciente es nuevo, ya tiene plan, tiene prueba gratis o ya agotó beneficios,
- crea
User,BeneficiaryeInsuranceAccountcuando corresponde, - y orquesta notificaciones y automatizaciones posteriores.
SkipPay (gokeipay-api)
- crea la orden CNPL,
- maneja identidad y método de pago,
- crea pagos y capturas,
- y notifica por webhook al prestador cuando cambia el estado de la orden.
Hay dos tipos de credenciales y no deben confundirse:
public_key: identifica al proveedor en Spot. Se usa paraPOST /spot/widgety consultas comoGET /spot/is_user_subscribed.client_secret: credencial privilegiada deSkipPay. Se usa para crear órdenes CNPL. Debe permanecer del lado servidor.client_id: credencial limitada deSkipPay. Se usa en operaciones paciente-facing de la orden ya creada.
Reglas operativas:
- nunca expongas
client_secretal navegador, - no hardcodees llaves reales en el repo,
- valida
event.originenpostMessage, - y separa claramente credenciales de staging y producción.
Este ejemplo separa la llamada server-side, pero la simulación local no endurece completamente el manejo del secreto:
- variables públicas en
.env - secreto CNPL preferentemente en
.dev.vars - la UI de simulación también permite enviarlo en el body hacia la función server-side sólo para pruebas locales
- proxy server-side en
functions/api/create-order.ts
NEXT_PUBLIC_GOKEI_API_URL="https://staging.backend.getskip.ai/api/spot"
NEXT_PUBLIC_GOKEI_WIDGET_URL="https://staging.spot.getskip.ai"
NEXT_PUBLIC_SKIP_PAY_API="https://staging.pay.getskip.ai"
NEXT_PUBLIC_GOKEI_PUBLIC_KEY="PK_EXAMPLE"Uso:
NEXT_PUBLIC_GOKEI_API_URL: base URL del backend Spot.NEXT_PUBLIC_GOKEI_WIDGET_URL: dominio esperado del iframe para validarpostMessage.NEXT_PUBLIC_SKIP_PAY_API: base URL pública de SkipPay.NEXT_PUBLIC_GOKEI_PUBLIC_KEY: valor opcional por defecto para la demo;PK_EXAMPLEse ignora hasta que lo reemplaces.
NEXT_PUBLIC_SKIP_PAY_API="https://staging.pay.getskip.ai"
SKIPAY_CLIENT_SECRET="st_secret_..."Uso:
SKIPAY_CLIENT_SECRET: secreto del tenant/proveedor paraPOST /orders.- usar
.dev.varses la forma recomendada para pruebas locales; pasar el secreto desde la UI existe en este repo sólo como atajo de simulación.
pnpm install
cp .env.example .env
pnpm devAbrir http://localhost:3000.
pnpm install
cp .env.example .env
cp .dev.vars.example .dev.vars
pnpm dev
pnpm dev:pagesNotas:
next.config.jsreescribe/api/create-orderahttp://127.0.0.1:3001/api/create-ordersólo en desarrollo.wrangler pages devejecuta la función de Cloudflare que crea la orden en SkipPay.
Este es el flujo que un prestador debe implementar para el caso de reembolso clásico.
Normalmente después de:
- reservar una cita,
- confirmar una atención,
- o terminar un checkout donde quiere ofrecer el servicio de reembolso.
Datos mínimos recomendados:
rutnamesurnameemailphone_number
POST /api/spot/widget?public_key={provider_public_key}
Body típico:
{
"rut": "12345678-9",
"user_data": {
"name": "María",
"surname": "González",
"email": "maria@example.com",
"phone_number": "+56912345678"
}
}Respuesta típica:
{
"widget_token": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://spot.getskip.ai?widget_token=550e8400-e29b-41d4-a716-446655440000&public_key=PK_..."
}Qué ocurre internamente en Skip:
- se valida el
public_key, - se crea un
TempWidgetToken, - el token dura normalmente 1 hora,
- puede durar más si se usa
long_term_token=true, - y se registran side effects operativos como lead tracking y notificaciones internas.
Se puede hacer de dos formas:
modal: overlay encima del checkout,div: iframe incrustado de forma fija en la página.
Este repo soporta ambas.
Al cargar, el widget consulta GET /api/spot/widget/{token} y devuelve un event_type que determina la UX:
SPOT_USER_NEWGOKEI_PRO_USERPROVIDER_CHARGESREFUNDS_LEFTFREE_TRIAL_EXPIRED_USER
Esto es importante porque el integrador no debe asumir que todo paciente siempre verá el mismo formulario.
Si el paciente es nuevo, el widget puede pedir:
- datos personales,
- selección de isapre,
- credenciales de isapre,
- seguro complementario,
- y credenciales del seguro.
Luego el widget llama a POST /api/spot/user?widget_token=....
Si el flujo termina correctamente:
- el usuario queda registrado o asociado en Skip,
- se crea/actualiza su beneficiario,
- se guardan cuentas de seguro,
- y el widget emite eventos al contenedor padre.
Al depurar CNPL es importante no atribuir al ejemplo una llamada que en realidad pertenece al widget.
spot-integration-examplecrea la orden CNPL conPOST /ordersy luego abre el iframe del widget.- este repo no ejecuta
POST /orders/{order_hash}/paymentdesde su frontend, - y tampoco toma decisiones de UX a partir de los
400de ese endpoint.
En la implementación observada, la llamada a POST /orders/{order_hash}/payment ocurre dentro de spot-form, al entrar al paso /payment/start.
Ese intento temprano no significa necesariamente "pagar inmediatamente". En la práctica, el widget usa esa llamada como una verificación implícita de readiness:
- si la API responde éxito, ya existe un
Paymentpendiente y el widget pasa a capture, - si la API responde
400con códigos de negocio comoprocessor_customer_identity_not_validatedoprocessor_payment_method_not_found, el widget usa esos códigos para decidir qué subpaso mostrar.
Implicancias para un integrador:
- ver
400en consola durante CNPL no implica por sí solo una falla de integración del ejemplo, - el origen más probable está en la lógica interna de
spot-form, - y si se rediseña ese comportamiento, el cambio principal debe hacerse en
spot-form, no en este repo.
Riesgo de cambiarlo sin rediseño:
- si se elimina el intento temprano de
POST /paymentpero no se agrega otra fuente de verdad para readiness, el widget dejará de saber cuándo pedir validación de identidad y cuándo pedir medio de pago.
CNPL agrega una orden de pago y financiamiento al flujo Spot.
- El prestador crea una orden en
SkipPay. - El prestador crea un
widget_tokenen Spot con eseorder_token. - El iframe detecta
order_tokeny activa el flujo CNPL. - El paciente se registra o se reconoce como usuario existente.
- El widget resuelve identidad, método de pago y captura del pago.
SkipPaynotifica al prestador por webhook cuando la orden cambia de estado.
POST /orders
Auth:
Authorization: Bearer {client_secret}
Body típico:
{
"reference": "ORDER-12345",
"total_amount": "100000",
"customer": {
"rut": "12345678-9",
"first_name": "María",
"last_name": "González",
"email": "maria@example.com",
"phone_number": "+56912345678"
}
}Respuesta esperada:
hashreferencestatustotal_amount
Ese hash es el order_token.
POST /api/spot/widget?public_key={provider_public_key}
Body:
{
"rut": "12345678-9",
"user_data": {
"name": "María",
"surname": "González",
"email": "maria@example.com",
"phone_number": "+56912345678"
},
"order_token": "ord_abc123"
}La URL final debe contener:
widget_tokenpublic_keyorder_token
Ejemplo:
https://spot.getskip.ai?widget_token=...&public_key=PK_...&order_token=ord_abc123
En CNPL, el widget orquesta llamadas a SkipPay, incluyendo:
GET /orders/{order_hash}POST /orders/{order_hash}/validate_customer_identityPOST /orders/{order_hash}/add_payment_methodPOST /orders/{order_hash}/paymentGET /orders/{order_hash}/payment/{payment_hash}/available_payment_methodsPOST /orders/{order_hash}/payment/{payment_hash}/capture
Importante:
- la creación de la orden requiere
client_secret, - las operaciones paciente-facing usan
client_id, - y el integrador no debe replicar lógica interna del widget si su objetivo es sólo embeder la experiencia.
Este ejemplo crea la orden desde functions/api/create-order.ts, que llama a src/server/create-order.ts.
Secuencia local del ejemplo:
- el usuario configura
public_key, flujo y, en CNPL,client_secreten la UI de simulación, - al entrar a
/success, el ejemplo inicializa en paralelo:POST /api/create-ordersi el flujo es CNPL,POST /spot/widgetpara obtener la URL base del widget,
- si existe
order_token, se concatena awidgetData.url, - y se abre el iframe.
Esto es útil para un integrador porque demuestra una decisión correcta de arquitectura:
- la creación de orden no debe ocurrir directamente contra
SkipPaydesde el navegador conclient_secret.
Nota importante sobre este ejemplo:
- en la simulación CNPL el navegador puede enviar
client_secreta/api/create-orderpara acelerar pruebas locales, - pero una integración real debe guardar ese secreto sólo en backend o en variables server-side del runtime.
La integración embebida debe escuchar window.postMessage.
Eventos relevantes documentados en la plataforma:
WIDGET_FORM_READYWIDGET_FORM_MOUNTWIDGET_FORM_SUCCESSWIDGET_PAYMENT_SUCCESSpara CNPLWIDGET_FORM_CLOSE
En este ejemplo, el listener actual maneja principalmente:
- validación de
origin - cierre del modal con
WIDGET_FORM_CLOSE
Recomendación para integradores reales:
- registrar telemetría por evento,
- cerrar el modal al finalizar,
- redirigir a confirmación propia del prestador,
- y tratar
WIDGET_PAYMENT_SUCCESScomo señal de pago exitoso en CNPL.
Hay tres capas:
- experiencia embebida para el paciente,
- APIs del backend Spot,
- y procesos posteriores de rendición, claim y notificación.
El iframe resuelve la parte de onboarding del paciente, no toda la operación de reembolsos.
Una fuente común de confusión:
widgetregistra y clasifica al paciente,gastosygastos-urlcargan boletas/documentos y disparan el procesamiento de rendiciones.
Este repo demuestra widget, no demuestra gastos.
El flujo POS usa endpoints separados como:
POST /spot/pos/rutPOST /spot/pos/user
No usa iframe y está pensado para operadores o staff de clínica.
La regla operacional más importante:
- el
rutmanda, - el
emailpuede reutilizar un usuario existente, - pero un RUT ya asociado a otra cuenta puede generar conflicto.
Casos a considerar:
- paciente nuevo,
- email conocido con RUT nuevo,
- RUT ya existente,
- conflicto
RUT_OTHER_EMAIL, - token expirado,
- datos inválidos.
Según el estado del usuario, el widget puede detectar:
- cliente ya suscrito,
- usuario con prueba gratis disponible,
- prueba gratis agotada,
- proveedor que absorbe el costo,
- usuario nuevo.
La experiencia final no debe depender de una sola “pantalla de éxito”.
Si el cliente quiere vender AAPD/CNPL, además del iframe necesita:
- tenant y credenciales en
SkipPay, - webhook del prestador para órdenes,
- manejo de conciliación de pagos,
- y claridad comercial sobre 30% ahora, 70% después, comisiones y tiempos de cobro.
- el
public_keyno reemplaza controles duros de backend, - cualquier secreto de pago debe vivir en backend,
- y el prestador debe verificar firma de webhooks de
SkipPay.
- el token del widget expira,
- puede haber cierres del modal sin completar el flujo,
- y el usuario puede caer en ramas distintas según su estado previo.
- el repositorio incluye presets y simulaciones para pruebas,
- pero una integración real debe usar datos propios del paciente capturados con consentimiento,
- y evitar logs con PII o credenciales de seguros.
- Spot no garantiza por sí solo que ya exista una rendición procesada,
- CNPL no termina cuando se captura el 30%,
- y el éxito comercial real puede depender de aprobación de reembolso, claim y cobros posteriores.
GET /api/spot/is_user_subscribedPOST /api/spot/widgetGET /api/spot/widget/{token}POST /api/spot/userPOST /api/spot/pos/rutPOST /api/spot/pos/userPOST /api/spot/gastosPOST /api/spot/gastos-urlGET /api/spot/user/{user_id}/check-if-successful-lead
POST /ordersGET /orders/{order_hash}POST /orders/{order_hash}/validate_customer_identityPOST /orders/{order_hash}/add_payment_methodPOST /orders/{order_hash}/paymentPOST /orders/{order_hash}/payment/{payment_hash}/captureGET /orders/{order_hash}/payment/{payment_hash}/available_payment_methods- webhooks de estado hacia el
webhook_notification_urldel tenant
src/components/features/checkout.tsx: demo de checkout, cálculo de cobro y chequeo de suscripción.src/components/features/appointment-confirmation.tsx: inicialización del widget y, en CNPL, creación paralela de orden + widget token.src/components/features/modal-iframe.tsx: integración embebida modal y listener depostMessage.src/components/features/div-iframe.tsx: integración embebida endiv.functions/api/create-order.ts: proxy server-side para crear órdenes CNPL.src/server/create-order.ts: cliente server-side paraPOST /orders.docs/cnpl.md: explicación del flujo CNPL desde la perspectiva del widget.
Aunque se revisaron repos relacionados para consolidar esta guía, no deben exponerse en documentación para terceros:
- secretos reales,
client_secretde tenants,- tokens internos,
- claves de webhook,
- credenciales de proveedores de pago,
- credenciales de seguros de pacientes,
- ni URLs privadas o payloads internos con PII innecesaria.
Para un prestador que quiere salir a producción:
- implementar backend propio para
POST /spot/widget, - si usa CNPL, implementar backend propio para
POST /orders, - validar y almacenar configuración por ambiente y por tenant,
- abrir el widget en
modalodivsegún la UX deseada, - escuchar eventos del iframe,
- verificar firmas de webhook de
SkipPay, - definir reintentos, timeouts y estados de negocio,
- y separar claramente Spot estándar de POS y de cargas de
gastos.
Esta README fue consolidada a partir de:
master-docs/spot/spot.mdmaster-docs/spot/SPOT_Business_Process_Code_Review.mdmaster-docs/spot/email-rut-matrix.mdmaster-docs/SKIP.mdmaster-docs/cnpl/CNPL_INTEGRATION_GUIDE.mdspot-integration-example/docs/cnpl.md- implementación observada en
spot-integration-example,backend-skipygokeipay-api
La idea es que un proveedor o cliente técnico pueda entender este proyecto primero, y luego profundizar en los repos de plataforma sólo cuando realmente necesite ampliar el alcance más allá de este ejemplo.