Skip to main content

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

🎉 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.