Vendre productes digitals com ara llicències de programari, plantilles, cursos o membresies sol requerir una infraestructura que molts petits negocis no desitgen gestionar. No obstant, amb les eines adequades és possible automatitzar el lliurament sense mantenir un servidor permanentment actiu. A Q2BSTUDIO, com a empresa especialitzada en aplicacions a mida, sabem que la clau està a simplificar processos sense comprometre la seguretat ni l’experiència del comprador.
Stripe Payment Links permet crear un enllaç de pagament en menys de dos minuts. Gestiona l’entrada de targeta, l’autenticació 3D Secure, els rebuts, la conversió de divises i la recaptació d’impostos. Quan el comprador paga, reps els diners i un objecte checkout.session amb estat 'complete'. El que no obtens és el lliurament del producte. Aquest pas és teu i determina si necessites infraestructura addicional.
La solució més comuna és un webhook: configurar un endpoint HTTPS, verificar la signatura a checkout.session.completed i complir amb el lliurament. Funciona, però implica tenir un servei desplegat, TLS, un secret de signatura, gestió de reintents i una postura de guàrdia per a un endpoint que es dispara unes poques vegades a la setmana. Per a productes digitals de baix volum, hi ha una alternativa més lleugera: eliminar l’endpoint i utilitzar un procés de polling. Un treball programat llista les sessions de pagament recents, selecciona les pagades que no ha processat abans i realitza el lliurament. No hi ha superfície de xarxa entrant, ni secret de webhook, ni servidor.
El que és interessant no és el polling en si, sinó els quatre problemes de correcció que el polling t’obliga a resoldre explícitament, mentre que un webhook et permet ignorar-los fins que perd un esdeveniment. A continuació, desglossem com implementar aquest patró fent servir GitHub Actions com a orquestrador.
Components del sistema
El sistema té quatre parts mòbils: un enllaç de pagament amb un camp personalitzat per recollir l’identificador de lliurament (per exemple, un nom d’usuari de GitHub); una taula de concessions (grant) que assigna cada enllaç o preu al producte que es lliura; un treball programat que llista sessions, filtra i compleix amb el lliurament; i un estat compromès que inclou un cursor i un conjunt d’identificadors de sessions processades.
En aquest exemple el lliurament és una invitació a un repositori privat de GitHub com a col·laborador. Però el patró s’aplica a qualsevol producte digital: enviar una clau de llicència, aprovisionar un inquilí SaaS, descarregar un ebook, etc.
Creació de l’enllaç de pagament
Primer, crea el producte i el preu a Stripe. Després genera un Payment Link i afegeix un camp personalitzat amb clau 'github_username', tipus text i obligatori. Al missatge de confirmació posterior al pagament, indica el mètode de lliurament i la latència real: 'La teva invitació al repositori sol arribar en minuts, sempre en unes poques hores.'
Un detall important: l’enllaç et dóna dos valors diferents: l’URL (que fas servir al botó de compra) i l’id (plink_...). Quan configuris la concessió, utilitza l’id, no l’URL. Si utilitzes l’URL, mai coincidirà amb l’objecte de sessió i els lliuraments fallaran en silenci, amb un build verd.
Llistat de sessions
Tot el motor es basa en dues crides GET al recurs /v1/checkout/sessions de Stripe. La primera obté una pàgina de sessions amb paginació i expansió de line_items per poder emparellar per preu. El codi és senzill: una funció que itera fins que no hi hagi més pàgines.
És fonamental fixar la versió de l’API de Stripe (Stripe-Version) a la petició, perquè la versió per defecte del teu compte pot canviar i trencar el codi de parseig sense que te n’adonis. Un treball de compliment (fulfillment) és just el tipus de cosa que ningú torna a provar després d’un canvi de compte.
Decidir què és una venda
No totes les sessions completades són pagades. Una sessió amb estat 'complete' i payment_status 'paid' és una venda, però també existeix 'no_payment_required', que passa quan s’utilitza un codi de promoció al 100%. Si filtres només per 'paid', el teu propi cupó de llançament fallarà en el lliurament i t’assabentaràs per un client. La funció de verificació ha d’incloure tots dos casos.
A més, cal emparellar la sessió amb el producte. Les sessions creades mitjançant Payment Link tenen el camp payment_link; les creades per servidor no, així que s’ha de recórrer al preu. Per això la crida de llistat expandeix line_items.
Validació de l’entrada del comprador
El camp personalitzat conté text no fiable escrit per un estrany. Abans d’usar-lo en una crida API (per exemple, per invitar un col·laborador de GitHub), cal validar-lo rigorosament. En el cas de noms d’usuari de GitHub, el format és: 1 a 39 caràcters, alfanumèric i guions, sense guionet al inici ni al final, i sense guions dobles. Qualsevol valor que no compleixi ha de ser rebutjat i marcat per a revisió humana, mai enviat a una URL.
També és útil normalitzar: treure una arroba inicial (@), acceptar una URL de perfil simple (github.com/usuari), i rebutjar qualsevol altra cosa com a rutes profundes que podrien convidar el compte equivocat.
El cursor i la finestra de 25 hores
Aquest és el problema que fa que el polling sigui subtil. El cursor obvi és 'la data de creació de la sessió més recent que he vist', i la consulta òbvia és created > cursor. Per absorbir la desviació del rellotge i execucions superposades, es resta una finestra de seguretat. Si configures aquesta finestra a 6 hores, perds vendes. Per què? Les sessions de Checkout es poden completar fins a 24 hores després de la seva creació (per defecte). Un comprador pot obrir el checkout a les 09:00, tancar la pestanya, tornar a les 20:00 i pagar. La data de creació segueix sent les 09:00. Mentrestant, el cursor avanza quan apareix una sessió nova no relacionada, per exemple a les 15:00. Amb una finestra de 6 hores, el sòl de l’escaneig es situa a les 09:00 i puja. Quan la sessió retardada es completa a les 20:00, la seva creació queda permanentment fora de la finestra. Mai es veu. La solució: finestra de 25 hores (24 hores de vida de la sessió més 1 hora de marge). Reescanejar sessions ja processades costa una pàgina extra d’API i una cerca en conjunt; perdre una venda costa un client. L’asimetria és la clau: dimensiona la finestra per al pitjor cas i deixa que la capa d’idempotència absorbeixi el cost.
Dues advertències: si allargues la vida de la sessió (expires_at), amplia la finestra en conseqüència. I manté el cursor monòton: la següent execució ha d’usar el màxim entre el cursor anterior i la sessió més nova vista.
Idempotència en dues capes
Una finestra de 25 hores significa que el treball rellegeix les mateixes sessions pagades aproximadament un centenar de vegades. Cada relectura ha de ser un no-op. La primera capa és un conjunt d’IDs de sessió processats emmagatzemats a l’estat compromès (un fitxer JSON al repo). La segona capa gestiona la concurrència: dues execucions simultànies poden llegir el mateix estat i veure la mateixa sessió com a nova. Per gestionar-ho, en escriure al llibre de comptabilitat (ledger) s’utilitza un hash de l’ID de sessió com a referència única; si ja existeix, s’ignora. A més, un bloqueig de concurrència a nivell de workflow evita que dos runs es trepitgin.
Gestió de fallades transitòries vs permanents
El lliurament és una crida a l’API de GitHub. Errors com 429 (rate limit) o 500 són transitòries: s’ha de reintentar al proper cicle, sense marcar la sessió com a processada. Errors com 404 (usuari no existeix) són permanents: no es reintenta, es registra per a intervenció humana. El reintent ha d’estar acotat per temps (per exemple, 6 hores) i no per nombre d’intents, perquè en un cicle de 15 minuts cinc intents es consumeixen en una hora, però un incident del proveïdor pot durar més.
Credencials amb mínims privilegis
L’objecció natural a aquest disseny és 'vas a posar la teva clau secreta de Stripe en un GitHub Action?' La resposta és que cap credencial necessita ser poderosa. Per a Stripe, una clau restringida (rk_...) amb només permís de lectura a Checkout Sessions (res més). Per a GitHub, un PAT de àmbit fi amb només permisos d’administració al repositori del producte. El propi estat del treball s’escriu usant el GITHUB_TOKEN integrat del workflow, no el PAT. Verifica els permisos executant el treball una vegada i llegint el log.
El workflow de GitHub Actions
El fitxer YML defineix un cron cada 15 minuts amb possibilitat d’execució manual. Utilitza concurrència per evitar carreres, pines de SHA per a les accions (no tags movedissos) i un pas final que commiteja l’estat (cursor, sessions processades, ledger) al repo. Aquest commit és el que fa que tot funcioni: l’estat és durador, tens una traça d’auditoria i un diff per venda sense necessitat de base de dades.
És important executar en un repositori privat perquè conté claus vives i dades de vendes. A més, el cron és un esforç best-effort: pot retardar-se quan la plataforma està ocupada. Per això el missatge de confirmació ha de prometre minuts i comprometre’s a hores. La finestra de 25 hores garanteix que un retard no perdi mai una venda, només la retardi.
Quan no és adequat aquest patró?
Si el teu producte necessita lliurament instantani (segons), utilitza webhooks. Si vens a compradors que no tenen compte de GitHub (per exemple, un ebook per a no desenvolupadors), canvia el lliurament a un altre mètode (enviar correu amb enllaç). Si ets comerciant registrat, recorda que ets responsable d’IVA i altres impostos; Stripe Tax ajuda a recaptar-los, però has de conèixer les regles de la teva jurisdicció. I no és un motor de subscripcions; per a pagaments recurrents, els webhooks són més apropiats pel seu cicle de vida (renovacions, fallades, dunning, cancel·lacions).
Resum del bucle principal
El treball llegeix l’estat, calcula el cursor amb la finestra de 25 hores, llista les sessions, filtra les noves pagades que coincideixen amb les concessions, extreu el nom d’usuari, valida, intenta el lliurament, registra el resultat (èxit, fallada transitòria, fallada permanent) i actualitza l’estat. A la nostra implementació, això són unes 190 línies d’E/S més 164 línies de lògica pura, amb dependència zero, usant fetch i crypto natius de Node.js. La separació permet testejar unitàriament cada regla sense xarxa.
El que més valor aporta és provar els casos límit: una sessió que es completa 23 hores després de la seva creació, una fallada transitòria que supera la finestra de reintent es converteix en una fila marcada, una segona execució sobre les mateixes dades no fa res, i una concessió configurada amb URL en lloc de plink_ es reporta sorollosament.
Abans de llançar, crea un codi de promoció al 100% amb límit d’una redempció, compra el teu propi producte i observa el flux complet. Costa zero, exercita la branca no_payment_required i és l’única forma de descobrir que la teva concessió està mal configurada.
A Q2BSTUDIO apliquem patrons similars per automatitzar el lliurament de programari a mida, integrant agents IA que gestionen el compliment, i desplegant en entorns cloud com AWS o Azure amb ciberseguretat integrada. La filosofia és la mateixa: simplificar l’operació sense sacrificar robustesa.
Aquest patró de polling amb finestra de 25 hores i estat a git és una alternativa sòlida per a venedors de productes digitals de baix volum que volen evitar la complexitat d’un webhook i un servidor permanent. Amb les eines adequades i una mica de codi, pots tenir el teu propi sistema de fulfillment funcionant en una tarda.





