Introduction
Par défaut, Stripe capture immédiatement l’argent lors d’un paiement. Mais dans certains cas, vous voulez d’abord autoriser le paiement (vérifier que les fonds sont disponibles) puis capturer plus tard. C’est le flux « authorize-then-capture », essentiel pour les réservations, pré-commandes et locations.
Cas d’usage
| Secteur | Scénario | |---------|----------| | **Voyages** | Autoriser à la réservation, capturer au départ | | **E-commerce** | Autoriser à la commande, capturer à l'expédition | | **Location** | Autoriser une caution, capturer les dégâts éventuels | | **Hôtellerie** | Autoriser à la réservation, capturer au check-out |
Fonctionnement
┌─────────────────────────────────────────────────────────────┐
│ ÉTAPE 1 : AUTORISATION │
│ │
│ Client → Carte validée → Fonds "bloqués" (pas débités) │
│ Statut PaymentIntent: requires_capture │
│ Durée: 7 jours max (selon carte) │
└──────────────────────────┬──────────────────────────────────┘
│
│ Jusqu'à 7 jours plus tard
│
┌──────────────────────────▼──────────────────────────────────┐
│ ÉTAPE 2 : CAPTURE │
│ │
│ Option A: Capturer → Fonds débités │
│ Option B: Annuler → Fonds libérés │
└─────────────────────────────────────────────────────────────┘
Créer une autorisation
Côté serveur (Node.js)
"`typescript"`
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
});
async function createAuthorization(
amount: number,
customerId: string,
metadata: Record<string, string>
) {
const paymentIntent = await stripe.paymentIntents.create({
amount, // En centimes
currency: 'eur',
customer: customerId,
capture_method: 'manual', // ← Clé pour la capture différée
automatic_payment_methods: { enabled: true },
metadata,
});
return paymentIntent;
}
Côté React
"`tsx"`
function ReservationPayment({ reservationId }: { reservationId: string }) {
const stripe = useStripe();
const elements = useElements();
const handleAuthorize = async () => {
if (!stripe || !elements) return;
// 1. Créer l'autorisation côté serveur
const response = await fetch('/api/authorize-payment', {
method: 'POST',
body: JSON.stringify({ reservationId }),
});
const { clientSecret } = await response.json();
// 2. Confirmer avec la carte du client
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
clientSecret,
confirmParams: {
return_url: `${window.location.origin}/reservation/${reservationId}/confirmed`,
},
redirect: 'if_required',
});
if (error) {
console.error('Autorisation échouée:', error.message);
} else if (paymentIntent.status === 'requires_capture') {
console.log('Autorisation réussie, capture en attente');
}
};
return (
<form onSubmit={handleAuthorize}>
<PaymentElement />
<button type="submit">Réserver (autorisation)</button>
<p className="info">Votre carte sera débitée uniquement après confirmation</p>
</form>
);
}
Capturer le paiement
Capture totale
"`typescript"`
async function capturePayment(paymentIntentId: string) {
const paymentIntent = await stripe.paymentIntents.capture(paymentIntentId);
// Statut: succeeded
return paymentIntent;
}
Capture partielle
Vous pouvez capturer moins que le montant autorisé :
"`typescript"`
async function capturePartial(paymentIntentId: string, amountToCapture: number) {
const paymentIntent = await stripe.paymentIntents.capture(paymentIntentId, {
amount_to_capture: amountToCapture, // En centimes
});
// Le reste est automatiquement libéré
return paymentIntent;
}
// Exemple: Autorisation de 100€, capture de 80€
// → 80€ débités, 20€ libérés automatiquement
Annuler une autorisation
Si vous ne voulez plus capturer :
"`typescript"`
async function cancelAuthorization(paymentIntentId: string) {
const paymentIntent = await stripe.paymentIntents.cancel(paymentIntentId, {
cancellation_reason: 'requested_by_customer',
});
// Fonds libérés immédiatement
return paymentIntent;
}
Workflow complet pour les réservations
"`typescript"`
// 1. À LA RÉSERVATION - Autoriser
async function handleReservation(reservationId: string, amount: number, customerId: string) {
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'eur',
customer: customerId,
capture_method: 'manual',
metadata: { reservationId },
});
// Sauvegarder en DB
await db.payment.create({
data: {
reservationId,
stripePaymentIntentId: paymentIntent.id,
amount,
status: 'AUTHORIZED', // Nouveau statut
},
});
return paymentIntent.client_secret;
}
// 2. WEBHOOK - Confirmer l'autorisation
async function handleAuthorizationSucceeded(paymentIntent: Stripe.PaymentIntent) {
if (paymentIntent.status !== 'requires_capture') return;
await db.payment.update({
where: { stripePaymentIntentId: paymentIntent.id },
data: { status: 'AUTHORIZED' },
});
await db.reservation.update({
where: { id: paymentIntent.metadata.reservationId },
data: { status: 'CONFIRMED' },
});
}
// 3. AU DÉPART - Capturer
async function handleDeparture(reservationId: string) {
const payment = await db.payment.findFirst({
where: { reservationId, status: 'AUTHORIZED' },
});
if (!payment) throw new Error('Aucune autorisation trouvée');
const captured = await stripe.paymentIntents.capture(payment.stripePaymentIntentId);
await db.payment.update({
where: { id: payment.id },
data: { status: 'CAPTURED', paidAmount: captured.amount_received },
});
}
// 4. EN CAS D'ANNULATION - Libérer
async function handleCancellation(reservationId: string) {
const payment = await db.payment.findFirst({
where: { reservationId, status: 'AUTHORIZED' },
});
if (!payment) return; // Pas d'autorisation à annuler
await stripe.paymentIntents.cancel(payment.stripePaymentIntentId);
await db.payment.update({
where: { id: payment.id },
data: { status: 'CANCELED' },
});
}
Gérer les webhooks
"`typescript"`
async function handlePaymentIntentWebhook(event: Stripe.Event) {
const paymentIntent = event.data.object as Stripe.PaymentIntent;
switch (event.type) {
// Autorisation réussie
case 'payment_intent.amount_capturable_updated':
if (paymentIntent.status === 'requires_capture') {
await handleAuthorizationSucceeded(paymentIntent);
}
break;
// Capture réussie
case 'payment_intent.succeeded':
await handleCaptureSucceeded(paymentIntent);
break;
// Échec
case 'payment_intent.payment_failed':
await handlePaymentFailed(paymentIntent);
break;
// Annulation
case 'payment_intent.canceled':
await handlePaymentCanceled(paymentIntent);
break;
}
}
Délais d’autorisation
Important : Les autorisations ont une durée de vie limitée !
| Type de carte | Durée max | |---------------|-----------| | Visa / Mastercard | 7 jours | | Amex | 7 jours | | Cartes corporate | Parfois moins |
Que faire si l’autorisation expire ?
"`typescript"`
async function refreshAuthorization(reservationId: string) {
const payment = await db.payment.findFirst({
where: { reservationId, status: 'AUTHORIZED' },
});
if (!payment) return;
// Vérifier si l'autorisation est encore valide
const paymentIntent = await stripe.paymentIntents.retrieve(
payment.stripePaymentIntentId
);
if (paymentIntent.status === 'canceled') {
// L'autorisation a expiré, en créer une nouvelle
console.log('Autorisation expirée, nouvelle autorisation requise');
// Notifier le client
await sendReauthorizationEmail(reservationId);
}
}
// Cron job quotidien pour vérifier les autorisations
async function checkExpiringAuthorizations() {
const payments = await db.payment.findMany({
where: {
status: 'AUTHORIZED',
createdAt: {
lt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // > 5 jours
},
},
});
for (const payment of payments) {
await refreshAuthorization(payment.reservationId);
}
}
Capture automatique vs manuelle
Comparaison
| Aspect | Capture automatique | Capture manuelle | |--------|--------------------|------------------| | `capture_method` | `automatic` (défaut) | `manual` | | Débit | Immédiat | À votre initiative | | Cas d'usage | E-commerce classique | Réservations, locations | | Complexité | Simple | Plus de code | | Webhook clé | `payment_intent.succeeded` | `payment_intent.amount_capturable_updated` |
Statuts PaymentIntent
"`typescript"` type PaymentIntentStatus = | 'requires_payment_method' // En attente de carte | 'requires_confirmation' // En attente de confirmation | 'requires_action' // 3D Secure requis | 'processing' // En cours | 'requires_capture' // ← Autorisé, en attente de capture | 'succeeded' // Capturé | 'canceled'; // Annulé
Bonnes pratiques
1. Informez vos clients
"`tsx"`
<div className="payment-info">
<PaymentElement />
<div className="authorization-notice">
⚠️ Votre carte sera pré-autorisée mais pas débitée immédiatement.
Le débit sera effectué au moment du départ.
</div>
</div>
2. Capturez rapidement
Ne laissez pas les autorisations « traîner ». Capturez dès que possible pour éviter les expirations.
3. Gérez les échecs de capture
"`typescript"`
async function safeCapturePayment(paymentIntentId: string) {
try {
return await stripe.paymentIntents.capture(paymentIntentId);
} catch (error) {
if (error.code === 'payment_intent_unexpected_state') {
// L'autorisation a expiré ou a été annulée
console.error('Impossible de capturer, autorisation expirée');
// Demander une nouvelle autorisation au client
}
throw error;
}
}
4. Logs et audit
"`typescript"`
async function captureWithAudit(
paymentIntentId: string,
reason: string,
capturedBy: string
) {
const result = await stripe.paymentIntents.capture(paymentIntentId);
// Log pour audit
await db.paymentAudit.create({
data: {
paymentIntentId,
action: 'CAPTURE',
amount: result.amount_received,
reason,
performedBy: capturedBy,
timestamp: new Date(),
},
});
return result;
}
Conclusion
La capture différée est un outil puissant pour les modèles économiques basés sur les réservations. Points clés :
- Utilisez `capture_method: ‘manual’` lors de la création
- Capturez dans les 7 jours
- Gérez les expirations avec des alertes
- Informez clairement vos clients
Cette technique vous permet de réserver des fonds sans les débiter, offrant une meilleure expérience client et une flexibilité opérationnelle.
📚 Série complète : Maîtriser Stripe avec Node.js et React
- Chapitre 1 : Configuration initiale Intégrer Stripe dans React/Next.js Configuration initiale et premier formulaire de paiement.
- Chapitre 2 : Automatisation Gérer les webhooks Stripe dans Node.js Recevoir et traiter les événements de paiement.
- Chapitre 3 : Stratégie de Paiement Stripe Checkout vs Elements : Quel choix pour votre projet ? Comparatif pour choisir la bonne solution.
- Chapitre 4 : Gestion après-vente (Précédent) Implémenter les remboursements Stripe Gérer les remboursements et litiges.
- Chapitre 5 : Concepts Avancés (Vous êtes ici) PaymentIntent et capture différée Autoriser puis capturer les paiements.
🎉 Félicitations !
Vous avez terminé cette série sur l’intégration Stripe avec Node.js et React. Vous maîtrisez maintenant :
✅ L’intégration de Stripe Elements dans React
✅ La gestion des webhooks
✅ Le choix entre Checkout et Elements
✅ Les remboursements et litiges
✅ La capture différée
Pour aller plus loin : Explorez Stripe Connect pour les marketplaces et Stripe Billing pour les abonnements.
