Stripe Connect y Facturación de Suscripciones
Dos flujos de dinero en una sola cuenta de Stripe. Un marketplace cobra al comprador una vez y distribuye los ingresos a muchos comerciantes (Connect, cargos y transferencias separados). Una suscripción de plataforma cobra a esos comerciantes una tarifa recurrente fuera de sesión (Billing). Comparten un cliente, un par de webhooks y un tipo de dinero.
La idea clave que lo define todo: el cargo del comprador y los pagos a los comerciantes fallan de forma independiente. Para cuando se ejecuta la liquidación, el dinero ya se ha movido, por lo que una transferencia rechazada nunca debe fallar el cargo ni el webhook, y debe poder reintentarse más tarde sin pagar a nadie dos veces.
Cuándo usarlo
- Un pago se divide entre varios vendedores, y la plataforma se queda con una comisión.
- Los vendedores se incorporan a Connect y reciben pagos con retraso (depósitos en garantía, reservas, KYC).
- La plataforma cobra a sus propios vendedores una tarifa recurrente con una tarjeta guardada.
- Una cuenta conectada parece activa pero nunca ha recibido dinero.
Cuándo NO usarlo
| No esto | Usa en su lugar |
|---|---|
| Pago de un solo vendedor, sin división | Stripe Checkout / Payment Element directamente |
| Incorporación y pagos de PayPal Marketplace | Una habilidad de PayPal; solo los puntos en común se comparten |
| UI de tarjeta, estilo de Payment Element | Tu sistema de diseño; esta habilidad es del lado del servidor |
Arquitectura
comprador ──cargo (cuenta de plataforma, transfer_group = orderId)──▶ balance de plataforma
│ liquidación (webhook O sincronización de lectura; reclamo arrendado)
┌────────────┴──┬────────────────────────┬────────────────┐
▼ ▼ ▼ ▼
transferencia:vendedor transferencia:socio depósito garantía reserva renovable
(source_transaction = el cargo) (release_at) (% retenido)
│ │ cron de liberación
▼ ▼
balance conectado ◀── reintento-transfs balance pagadero ──▶ pago (controlado por KYC/riesgo)
(tramos no financiados)
plataforma ──PaymentIntent off_session (customer + PM guardada)──▶ factura de suscripción
cobros: 3 intentos, 3 días de diferencia, luego suspender
Hechos críticos
- Cargos y transferencias separados, nunca cargos con destino. Un cargo tiene un único
destination; un carrito con varios vendedores necesita N transferencias bajo un mismotransfer_group, por lo queapplication_fee_amountes inutilizable. - Una transferencia sin
source_transactionse carga del balance disponible y se rechaza mientras el cargo aún se está liquidando; en una cuenta de plataforma joven, esto ocurre en cada transferencia. Esta única bandera es lo que permite dividir los fondos mientras el cargo aún se está liquidando. - Las transferencias de Stripe transfronterizas solo funcionan dentro de EE.UU./Canadá/Reino Unido/EEE/Suiza. Una plataforma en otro lugar paga a cuentas conectadas solo en su propio país. Stripe aplica esto al momento de la transferencia, no al de la incorporación, por lo que las plantillas aplican la regla de región en la incorporación.
- Los eventos de Connect usan un secreto de firma diferente al de los eventos de la plataforma. Dos endpoints, dos secretos, una URL — verifica contra ambos.
- El país de una cuenta conectada es inmutable — un país incorrecto significa volver a incorporar, no una corrección.
- El dinero nunca es un número de coma flotante — cadenas
numeric(19,4)de principio a fin, matemáticas conbigint, unidades menores solo en el límite del SDK.
Reglas estrictas
Nunca permitas que una transferencia fallida haga fallar la liquidación. Registra el tramo como no financiado y continúa. El cargo ya ha tenido éxito, por lo que el webhook también debe tener éxito; el cron de reintento financia el tramo más tarde.
Nunca trates una inserción duplicada de webhook como "ya manejada". La inserción es un reclamo; el reintento después de que una entrega fallara a medio camino vuelve a ejecutar los manejadores, por lo que cada evento se ejecuta hasta completarse exactamente una vez.
Nunca reenvíes una transferencia sin preguntar a Stripe si ya existe. Un tramo registrado como no financiado porque la respuesta se perdió paga dos veces una vez que pasa la ventana de idempotencia de 24 horas de Stripe.
Nunca derives exclusividad de liquidación de una bandera de estado escrita al final. Dos llamadas alcanzan la liquidación de forma rutinaria; un reclamo arrendado atómicamente es lo que hace que el inventario y las transferencias ocurran una vez.
Nunca reviertas más que el margen restante de una transferencia. Las reversiones apiladas (tarifa de pasarela, luego un reembolso) se rechazan más allá del monto original.
Inicio rápido
- Modela el dinero — money.md, data-model.md, store.md.
- Cliente, secretos duales de webhook, entorno — stripe-adapter.md.
- Incorpora cuentas; verifica la regla de región primero — connect-accounts.md.
- Recibe eventos de forma idempotente — webhooks.md.
- Liquida: reclama, distribuye, depósito en garantía, reserva — settlement.md.
- Convierge: reintenta tramos, concilia tarifas — reconciliation.md.
- Factura a los inquilinos — subscriptions.md; ejecuta con operations.md.
Adáptalo a tu aplicación con adaptation.md; el registro de la auditoría está en provenance.md.
Contrato de adaptación
| Punto de integración | La habilidad proporciona | El anfitrión proporciona |
|---|---|---|
| Entidades de dominio | Tenant (organización vendedora), Order, VendorOrder, Partner | Su propio vocabulario |
| Ámbito del inquilino | Un tenantId derivado del servidor | org / workspace / vendedor |
| Guardia de autenticación | Una firma de adaptador por ruta | Clerk, NextAuth, Supabase, personalizado |
| Acceso a datos | Un contrato PaymentsStore + SQL | Su ORM o SDK |
| Auditoría + notificaciones | Sitios de llamada, nombres de eventos, formas de carga útil | Su tabla de auditoría / bandeja de salida |
| Trabajo en segundo plano | Cuatro crons y su cadencia | Su planificador |
| Validación | Una forma de esquema por cuerpo de ruta | zod / valibot / yup |
| UI | Nada — esta habilidad es del lado del servidor | Todo |
Directorio de referencias
| Escenario | Palabras clave de activación | Referencia |
|---|---|---|
| Por qué este modelo de dinero | cargos y transferencias separados, cargo destino, application_fee_amount, transfer_group, libro mayor | architecture.md |
| Tablas, columnas, RLS | esquema, migración, payment_intents, transfers, escrow_holds, webhook_events | data-model.md |
| El contrato de acceso a datos | store, repositorio, reclamo atómico, actualización condicional, ORM | store.md |
| Montos y redondeo | numeric, bigint, unidades menores, distribuir, resto mayoritario, centavos | money.md |
| Código de cliente y adaptador | stripeClient, apiVersion, constructEvent, whsec_, PaymentIntent, refund, payout | stripe-adapter.md |
| Incorporación, estado de cuenta | Express, Accounts v2, account.updated, charges_enabled, acct_, country, region | connect-accounts.md |
| Recibir eventos de forma segura | idempotencia, duplicado, repetición, cuerpo sin procesar, firma, invalid_signature | webhooks.md |
| Dividir un pedido pagado | liquidación, distribución, source_transaction, depósito garantía, reserva, single-flight | settlement.md |
| Arreglar lo que no llegó | reintento, no financiado, backoff, adopt, tarifa de pasarela, clawback, reversión, margen | reconciliation.md |
| Tarifas recurrentes de plataforma | SetupIntent, off_session, tarjeta guardada, dunning, invoice, past_due, suspend | subscriptions.md |
| Configuración, entorno, crons, depuración | STRIPE_SECRET_KEY, endpoint webhook, stripe listen, cron, "el comerciante nunca pagó" | operations.md |
| Adaptarlo a esta aplicación | adaptar, sonda del anfitrión, renombrar, ORM, guardia de autenticación, puerto | adaptation.md |
| El registro de auditoría | procedencia, desviación, mantenido, añadido, no verificado | provenance.md |