If you've worked with Stripe, you've probably wondered why you can't just send card data directly to charge a customer. Payment Intents is Stripe's answer—it's an API object that manages the entire payment workflow, from initial creation through confirmation and status tracking. Understanding Payment Intents is crucial because it handles authentication requirements, retries, and idempotency automatically.
What Payment Intents Actually Does
A Payment Intent is a Stripe API object that represents your intent to collect payment from a customer. Unlike the older Charge API, Payment Intents are designed to handle modern payment complexity.
Create a Payment Intent to Start a Payment Flow
Every payment with Stripe starts with creating a Payment Intent. This tells Stripe: 'I want to collect $X from a customer.' You specify the amount in the smallest currency unit (cents for USD), the currency, and optionally a payment method.
const stripe = require('stripe')('sk_test_YOUR_KEY');
const paymentIntent = await stripe.paymentIntents.create({
amount: 2000, // $20.00 in cents
currency: 'usd',
payment_method_types: ['card'],
});
console.log(paymentIntent.id); // pi_1234...Understand the Payment Intent Statuses
After you create a Payment Intent, it moves through different statuses. requires_payment_method means you need to attach a card. requires_confirmation means you've attached a payment method but need to confirm it. requires_action means the customer needs to authenticate (like 3D Secure). succeeded means you got the money.
const intent = await stripe.paymentIntents.retrieve('pi_1234');
switch (intent.status) {
case 'requires_payment_method':
console.log('Need to collect card details');
break;
case 'requires_confirmation':
console.log('Ready to confirm the payment');
break;
case 'requires_action':
console.log('Customer needs to authenticate via 3D Secure');
break;
case 'succeeded':
console.log('Payment successful');
break;
}Creating and Confirming Payment Intents
Most payments flow through two steps: create the Payment Intent server-side, then confirm it with a payment method (either client-side or server-side depending on your setup).
Confirm a Payment Intent Server-Side
If you're handling payment methods server-side, you can confirm directly. Pass the Payment Intent ID and a payment method, and Stripe will attempt the charge. This returns the updated Payment Intent with its new status.
const confirmed = await stripe.paymentIntents.confirm('pi_1234', {
payment_method: 'pm_card_visa', // Pre-existing payment method ID
});
if (confirmed.status === 'succeeded') {
console.log('Charge successful, amount:', confirmed.amount_received);
}Use Idempotency Keys to Handle Retries Safely
Network failures happen. If you retry a Payment Intent confirm and get a timeout, you don't want to charge twice. That's where idempotency keys come in—pass the same key and Stripe returns the same result without double-charging.
const idempotencyKey = 'order_12345_attempt_1'; // Unique per logical request
const confirmed = await stripe.paymentIntents.confirm('pi_1234',
{ payment_method: 'pm_card_visa' },
{ idempotencyKey } // Stripe returns same result for same key
);
console.log(confirmed.status);Handle Failed Confirmations and Retry Logic
Not every confirmation succeeds. Cards decline, funds are insufficient, or 3D Secure is needed. Check the response status and last_payment_error to decide what to do next.
try {
const confirmed = await stripe.paymentIntents.confirm('pi_1234', {
payment_method: 'pm_card_visa',
});
if (confirmed.status === 'requires_action') {
// Return client secret to frontend for 3D Secure
return { clientSecret: confirmed.client_secret };
} else if (confirmed.status === 'succeeded') {
return { success: true };
}
} catch (error) {
console.error('Payment failed:', error.message);
}stripe.paymentIntents.update() to add metadata or change the description after creation but before confirmation.Why Payment Intents Matter for Modern Payments
The old Charge API didn't handle authentication well. Payment Intents is built for the modern payments world where regulations require customer authentication.
Payment Intents Handles SCA/3D Secure Automatically
Strong Customer Authentication (SCA) and 3D Secure are regulatory requirements in Europe and elsewhere. With Payment Intents, if a card requires authentication, the status becomes requires_action. You send the client_secret to your frontend, the customer completes authentication, and the payment automatically succeeds.
// Server creates Payment Intent
const intent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'usd',
payment_method: 'pm_card_visa',
confirm: true, // Confirm immediately
});
if (intent.status === 'requires_action') {
// Send to frontend for authentication
console.log('Send to frontend:', intent.client_secret);
}confirm: true when creating a Payment Intent if you have the payment method ready—it's more efficient than creating then confirming separately.Common Pitfalls
- Creating a Payment Intent without confirming it and forgetting to confirm later—the payment never goes through and customer sees no clear error.
- Not handling the
requires_actionstatus—3D Secure authentication fails silently because you tried to process a payment that needed authentication. - Reusing the same idempotency key across different charges—Stripe returns the original result, potentially charging the wrong amount or customer.
- Storing payment method IDs without understanding they're tied to a Stripe account���they only work for the same Stripe account that created them.
Wrapping Up
Payment Intents is how Stripe handles the full payment lifecycle—from initial intent through confirmation, authentication, and completion. It replaces the older Charge API because modern payments require handling authentication, retries, and complex payment flows. If you want to track your Stripe payment patterns and conversion metrics across all your tools, Product Analyst can help.