Stripe Connect & Facturation d'Abonnements
Deux flux monétaires sur un seul compte Stripe. Une place de marché facture l'acheteur une fois et répartit le produit entre plusieurs vendeurs (Connect, paiements et transferts séparés). Un abonnement de plateforme facture ces vendeurs de façon récurrente hors session (Billing). Ils partagent un client, une paire de webhooks et un type de monnaie.
L'idée directrice qui structure tout : le paiement de l'acheteur et les reversements aux vendeurs échouent indépendamment. Au moment où le règlement s'exécute, l'argent a déjà été déplacé, donc un transfert rejeté ne doit jamais faire échouer le paiement ou le webhook, et doit pouvoir être relancé plus tard sans payer personne deux fois.
Quand l'utiliser
- Un paiement est réparti entre plusieurs vendeurs, la plateforme prélevant une commission.
- Les vendeurs intègrent Connect et sont payés avec un délai (séquestre, réserves, KYC).
- La plateforme facture ses propres vendeurs de manière récurrente sur une carte enregistrée.
- Un compte connecté semble sain mais n'a jamais reçu d'argent.
Quand NE PAS l'utiliser
| Pas cela | Utilisez plutôt |
|---|---|
| Paiement unique, pas de répartition | Stripe Checkout / Payment Element directement |
| Intégration PayPal Marketplace + paiements | Une compétence PayPal ; seules les interfaces sont partagées |
| Interface de carte, style Payment Element | Votre système de design ; cette compétence est côté serveur |
Architecture
acheteur ──paiement (compte plateforme, transfer_group = orderId)──▶ solde plateforme
│ répartition règlement (webhook OU lecture synchrone avec revendication louée)
┌────────────┴──┬────────────────────────┬────────────────┐
▼ ▼ ▼ ▼
transfert:commerçant transfert:partenaire séquestre réserve renouvelable
(source_transaction = le paiement) (release_at) (% retenu)
│ │ cron de libération du séquestre
▼ ▼
solde connecté ◀── reprise des transferts solde payable ──▶ paiement (KYC/risque)
(jambes non financées)
plateforme ──PaymentIntent off_session (client + PM enregistré)──▶ facture d'abonnement
relance : 3 tentatives, 3 jours d'écart, puis suspension
Faits essentiels
- Paiements et transferts séparés, jamais de destination unique. Un paiement prend
une seule
destination; un panier multi-vendeurs a besoin de N transferts sous un seultransfer_group, doncapplication_fee_amountest inutilisable. - Un transfert sans
source_transactionpuise dans le solde disponible et est rejeté tant que le paiement est en cours de règlement ; sur un compte plateforme récent, c'est le cas de chaque transfert. Ce seul indicateur permet de financer une part alors que le paiement est encore en cours de règlement. - Les transferts Stripe transfrontaliers ne fonctionnent qu'entre US/CA/UK/EEA/CH. Une plateforme ailleurs ne paie les comptes connectés que dans son propre pays. Stripe applique cette règle au moment du transfert, pas de l'intégration, donc les modèles appliquent la règle de région lors de l'intégration.
- Les événements Connect portent un secret de signature différent des événements de plateforme. Deux endpoints, deux secrets, une URL — vérifiez avec les deux.
- Le pays d'un compte connecté est immuable — un mauvais pays signifie une ré-intégration, pas une correction.
- L'argent n'est jamais un nombre à virgule flottante — chaînes
numeric(19,4)bout en bout, calcul enbigint, unités mineures uniquement à la limite du SDK.
Règles strictes
Ne laissez jamais un transfert échoué faire échouer le règlement. Enregistrez la jambe comme non financée et continuez. Le paiement a déjà réussi, donc le webhook doit aussi réussir ; la cron de reprise finance la jambe plus tard.
Ne traitez jamais une insertion de webhook en double comme "déjà traitée". L'insertion est une revendication ; la reprise après un crash de livraison en cours de traitement réexécute les gestionnaires, donc chaque événement s'exécute à son terme exactement une fois.
Ne renvoyez jamais un transfert sans demander à Stripe s'il existe déjà. Une jambe enregistrée comme non financée parce que la réponse a été perdue paie deux fois une fois que la fenêtre d'idempotence de 24h de Stripe est passée.
Ne dérivez jamais l'exclusivité du règlement d'un indicateur de statut écrit à la fin. Deux appelants atteignent régulièrement le règlement ; une revendication atomique louée est ce qui rend les inventaires et les transferts uniques.
Ne reversez jamais plus que la marge restante d'un transfert. Des reversements empilés (frais de passerelle, puis un remboursement) sont rejetés au-delà du montant d'origine.
Démarrage rapide
- Modélisez l'argent — money.md, data-model.md, store.md.
- Client, secrets de webhook doubles, variables d'environnement — stripe-adapter.md.
- Intégrez les comptes ; vérifiez d'abord la règle de région — connect-accounts.md.
- Recevez les événements de manière idempotente — webhooks.md.
- Réglez : revendiquer, répartir, séquestre, réserve — settlement.md.
- Convergez : reprise des jambes, rapprochement des frais — reconciliation.md.
- Facturez les locataires — subscriptions.md ; exécutez-le avec operations.md.
Adaptez-le à votre application avec adaptation.md ; l'archive de l'audit se trouve dans provenance.md.
Contrat d'Adaptation
| Point d'extension | La compétence fournit | L'hôte fournit |
|---|---|---|
| Entités métier | Tenant (org. vendeur), Order, VendorOrder, Partner | Son propre vocabulaire |
| Périmètre locataire | Un tenantId dérivé du serveur | org / espace de travail / vendeur |
| Garde d'authentification | Une signature d'adaptateur par route | Clerk, NextAuth, Supabase, personnalisé |
| Accès aux données | Un contrat PaymentsStore + SQL | Son ORM ou SDK |
| Audit + notifications | Sites d'appel, noms d'événements, formes de payload | Sa table d'audit / boîte de sortie |
| Travail en arrière-plan | Quatre crons et leur cadence | Son planificateur |
| Validation | Une forme de schéma par corps de route | zod / valibot / yup |
| UI | Rien — cette compétence est côté serveur | Tout |
Répertoire des références
| Scénario | Mots-clés déclencheurs | Référence |
|---|---|---|
| Pourquoi ce modèle monétaire | paiements et transferts séparés, destination unique, application_fee_amount, transfer_group, registre | architecture.md |
| Tables, colonnes, RLS | schéma, migration, payment_intents, transfers, escrow_holds, webhook_events | data-model.md |
| Le contrat d'accès aux données | store, repository, revendication atomique, mise à jour conditionnelle, ORM | store.md |
| Montants et arrondis | numeric, bigint, unités mineures, distribuer, plus grand reste, centimes | money.md |
| Code client et adaptateur | stripeClient, apiVersion, constructEvent, whsec_, PaymentIntent, refund, payout | stripe-adapter.md |
| Intégration, état du compte | Express, Accounts v2, account.updated, charges_enabled, acct_, country, region | connect-accounts.md |
| Réception sécurisée des événements | idempotence, duplication, rejeu, corps brut, signature, invalid_signature | webhooks.md |
| Répartition d'une commande payée | règlement, répartition, source_transaction, séquestre, réserve, single-flight | settlement.md |
| Correction de ce qui n'a pas abouti | reprise, non financé, backoff, adopter, frais de passerelle, récupération, reversement, marge | reconciliation.md |
| Frais de plateforme récurrents | SetupIntent, off_session, carte enregistrée, relance, invoice, past_due, suspend | subscriptions.md |
| Configuration, environnement, crons, débogage | STRIPE_SECRET_KEY, endpoint webhook, stripe listen, cron, "le vendeur n'a jamais payé" | operations.md |
| Adaptation à cette application | adapter, sonde hôte, renommer, ORM, garde d'authentification, port | adaptation.md |
| L'archive d'audit | provenance, déviation, conservé, ajouté, non vérifié | provenance.md |