Vender productos digitales como licencias de software, plantillas, cursos o membresías suele requerir una infraestructura que muchos pequeños negocios no desean gestionar. Sin embargo, con las herramientas adecuadas es posible automatizar la entrega sin tener que mantener un servidor permanentemente activo. En Q2BSTUDIO, como empresa especializada en aplicaciones a medida, sabemos que la clave está en simplificar procesos sin comprometer la seguridad ni la experiencia del comprador.
Stripe Payment Links permite crear un enlace de pago en menos de dos minutos. Maneja la entrada de tarjeta, la autenticación 3D Secure, los recibos, la conversión de divisas y la recaudación de impuestos. Cuando el comprador paga, recibes el dinero y un objeto checkout.session con estado 'complete'. Lo que no obtienes es la entrega del producto. Ese paso es tuyo y determina si necesitas infraestructura adicional.
La solución más común es un webhook: configurar un endpoint HTTPS verificar la firma en checkout.session.completed y cumplir con la entrega. Funciona, pero implica tener un servicio desplegado, TLS, un secreto de firma, manejo de reintentos y una postura de guardia para un endpoint que se dispara unas pocas veces a la semana. Para productos digitales de bajo volumen, existe una alternativa más ligera: eliminar el endpoint y usar un proceso de polling. Un trabajo programado lista las sesiones de pago recientes, selecciona las pagadas que no ha procesado antes y realiza la entrega. No hay superficie de red entrante, ni secreto de webhook, ni servidor.
Lo interesante no es el polling en sí, sino los cuatro problemas de corrección que el polling te obliga a resolver explícitamente, mientras que un webhook te permite ignorarlos hasta que pierde un evento. A continuación, desglosamos cómo implementar este patrón usando GitHub Actions como orquestador.
Componentes del sistema
El sistema tiene cuatro partes móviles: un enlace de pago con un campo personalizado para recoger el identificador de entrega (por ejemplo, un nombre de usuario de GitHub); una tabla de concesiones (grant) que asigna cada enlace o precio al producto que se entrega; un trabajo programado que lista sesiones, filtra y cumple con la entrega; y un estado comprometido que incluye un cursor y un conjunto de identificadores de sesiones procesadas.
En este ejemplo la entrega es una invitación a un repositorio privado de GitHub como colaborador. Pero el patrón se aplica a cualquier producto digital: enviar una clave de licencia, aprovisionar un inquilino SaaS, descargar un ebook, etc.
Creación del enlace de pago
Primero, crea el producto y el precio en Stripe. Luego genera un Payment Link y añade un campo personalizado con clave 'github_username', tipo texto y obligatorio. En el mensaje de confirmación posterior al pago, indica el método de entrega y la latencia real: 'Tu invitación al repositorio suele llegar en minutos, siempre en unas pocas horas'.
Un detalle importante: el enlace te da dos valores distintos: la URL (que usas en el botón de compra) y el id (plink_...). Cuando configures la concesión, usa el id, no la URL. Si usas la URL, nunca coincidirá con el objeto de sesión y las entregas fallarán en silencio, con un build verde.
Listado de sesiones
Todo el motor se basa en dos llamadas GET al recurso /v1/checkout/sessions de Stripe. La primera obtiene una página de sesiones con paginación y expansión de line_items para poder emparejar por precio. El código es sencillo: una función que itera hasta que no haya más páginas.
Es fundamental fijar la versión de la API de Stripe (Stripe-Version) en la petición, porque la versión por defecto de tu cuenta puede cambiar y romper el código de parseo sin que te des cuenta. Un trabajo de cumplimiento (fulfillment) es justo el tipo de cosa que nadie vuelve a probar después de un cambio de cuenta.
Decidir qué es una venta
No todas las sesiones completadas son pagadas. Una sesión con estado 'complete' y payment_status 'paid' es una venta, pero también existe 'no_payment_required', que ocurre cuando se usa un código de promoción al 100%. Si filtras solo por 'paid', tu propio cupón de lanzamiento fallará en la entrega y te enterarás por un cliente. La función de verificación debe incluir ambos casos.
Además, hay que emparejar la sesión con el producto. Las sesiones creadas mediante Payment Link tienen el campo payment_link; las creadas por servidor no, así que se debe recurrir al precio. Por eso la llamada de listado expande line_items.
Validación de la entrada del comprador
El campo personalizado contiene texto no confiable escrito por un extraño. Antes de usarlo para una llamada API (por ejemplo, invitar a un colaborador de GitHub), hay que validarlo rigurosamente. En el caso de nombres de usuario de GitHub, el formato es: 1 a 39 caracteres, alfanumérico y guiones, sin guion al inicio ni al final, y sin guiones dobles. Cualquier valor que no cumpla debe ser rechazado y marcado para revisión humana, nunca enviado a una URL.
También es útil normalizar: quitar una arroba inicial (@), aceptar una URL de perfil simple (github.com/usuario), y rechazar cualquier otra cosa como rutas profundas que podrían invitar a la cuenta equivocada.
El cursor y la ventana de 25 horas
Este es el problema que hace que el polling sea sutil. El cursor obvio es 'la fecha de creación de la sesión más reciente que he visto', y la consulta obvia es created > cursor. Para absorber la desviación del reloj y ejecuciones superpuestas, se resta una ventana de seguridad. Si configuras esa ventana en 6 horas, pierdes ventas. ¿Por qué? Las sesiones de Checkout pueden completarse hasta 24 horas después de su creación (por defecto). Un comprador puede abrir el checkout a las 09:00, cerrar la pestaña, volver a las 20:00 y pagar. La fecha de creación sigue siendo las 09:00. Mientras tanto, el cursor avanza cuando aparece una sesión nueva no relacionada, por ejemplo a las 15:00. Con una ventana de 6 horas, el suelo del escaneo se sitúa en 09:00 y sube. Cuando la sesión rezagada se completa a las 20:00, su creación queda permanentemente fuera de la ventana. Nunca se ve. La solución: ventana de 25 horas (24 horas de vida de la sesión más 1 hora de margen). Re-escanear sesiones ya procesadas cuesta una página extra de API y una búsqueda en conjunto; perder una venta cuesta un cliente. La asimetría es la clave: dimensiona la ventana para el peor caso y deja que la capa de idempotencia absorba el coste.
Dos advertencias: si alargas la vida de la sesión (expires_at), amplía la ventana en consecuencia. Y mantén el cursor monótono: la siguiente ejecución debe usar el máximo entre el cursor anterior y la sesión más nueva vista.
Idempotencia en dos capas
Una ventana de 25 horas significa que el trabajo relee las mismas sesiones pagadas aproximadamente un centenar de veces. Cada relectura debe ser un no-op. La primera capa es un conjunto de IDs de sesión procesados almacenados en el estado comprometido (un archivo JSON en el repo). La segunda capa maneja la concurrencia: dos ejecuciones simultáneas pueden leer el mismo estado y ver la misma sesión como nueva. Para ello, al escribir en el libro de contabilidad (ledger) se usa un hash del ID de sesión como referencia única; si ya existe, se ignora. Además, un bloqueo de concurrencia a nivel de workflow evita que dos runs se pisen.
Manejo de fallos transitorios vs permanentes
La entrega es una llamada a la API de GitHub. Errores como 429 (rate limit) o 500 son transitorios: se debe reintentar en el próximo ciclo, sin marcar la sesión como procesada. Errores como 404 (usuario no existe) son permanentes: no se reintenta, se registra para intervención humana. El reintento debe estar acotado por tiempo (por ejemplo, 6 horas) y no por número de intentos, porque en un ciclo de 15 minutos cinco intentos se consumen en una hora, pero un incidente del proveedor puede durar más.
Credenciales con mínimos privilegios
La objeción natural a este diseño es '¿vas a poner tu clave secreta de Stripe en un GitHub Action?' La respuesta es que ninguna credencial necesita ser poderosa. Para Stripe, una clave restringida (rk_...) con solo permiso de lectura en Checkout Sessions (nada más). Para GitHub, un PAT de ámbito fino con solo permisos de administración en el repositorio del producto. El propio estado del job se escribe usando el GITHUB_TOKEN integrado del workflow, no el PAT. Verifica los permisos ejecutando el trabajo una vez y leyendo el log.
El workflow de GitHub Actions
El archivo YML define un cron cada 15 minutos con posibilidad de ejecución manual. Usa concurrencia para evitar carreras, pines de SHA para las acciones (no tags movedizos) y un paso final que commitea el estado (cursor, sesiones procesadas, ledger) en el repo. Ese commit es el que hace que todo funcione: el estado es duradero, tienes una traza de auditoría y un diff por venta sin necesidad de base de datos.
Es importante ejecutar en un repositorio privado porque contiene claves vivas y datos de ventas. Además, el cron es un esfuerzo best-effort: puede retrasarse cuando la plataforma está ocupada. Por eso el mensaje de confirmación debe prometer minutos y comprometerse a horas. La ventana de 25 horas garantiza que un retraso nunca pierda una venta, solo la retrase.
¿Cuándo no es adecuado este patrón?
Si tu producto necesita entrega instantánea (segundos), usa webhooks. Si vendes a compradores que no tienen cuenta de GitHub (por ejemplo, un ebook para no desarrolladores), cambia la entrega a otro método (enviar email con enlace). Si eres comerciante registrado, recuerda que eres responsable de IVA y otros impuestos; Stripe Tax ayuda a recaudarlos, pero debes conocer las reglas de tu jurisdicción. Y no es un motor de suscripciones; para pagos recurrentes, los webhooks son más apropiados por su ciclo de vida (renovaciones, fallos, dunning, cancelaciones).
Resumen del bucle principal
El trabajo lee el estado, calcula el cursor con la ventana de 25 horas, lista las sesiones, filtra las nuevas pagadas que coinciden con las concesiones, extrae el nombre de usuario, valida, intenta la entrega, registra el resultado (éxito, fallo transitorio, fallo permanente) y actualiza el estado. En nuestra implementación, esto son unas 190 líneas de E/S más 164 líneas de lógica pura, con dependencia cero, usando fetch y crypto nativos de Node.js. La separación permite testear unitariamente cada regla sin red.
Lo que más valor aporta es probar los casos límite: una sesión que se completa 23 horas después de su creación, un fallo transitorio que supera la ventana de reintento se convierte en una fila marcada, una segunda ejecución sobre los mismos datos no hace nada, y una concesión configurada con URL en lugar de plink_ se reporta ruidosamente.
Antes de lanzar, crea un código de promoción al 100% con límite de una redención, compra tu propio producto y observa el flujo completo. Cuesta cero, ejercita la rama no_payment_required y es la única forma de descubrir que tu concesión está mal configurada.
En Q2BSTUDIO aplicamos patrones similares para automatizar la entrega de software a medida, integrando agentes IA que gestionan el cumplimiento, y desplegando en entornos cloud como AWS o Azure con ciberseguridad integrada. La filosofía es la misma: simplificar la operación sin sacrificar robustez.
Este patrón de polling con ventana de 25 horas y estado en git es una alternativa sólida para vendedores de productos digitales de bajo volumen que quieren evitar la complejidad de un webhook y un servidor permanente. Con las herramientas adecuadas y un poco de código, puedes tener tu propio sistema de fulfillment funcionando en una tarde.





