6 min read

What Is Payment Intents in Stripe

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.

javascript
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...
Each Payment Intent gets a unique ID (starting with 'pi_') that you'll use to track the payment.

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.

javascript
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;
}
Always check the status before assuming payment went through.
Watch out: Creating a Payment Intent doesn't charge the customer—it just sets up the intent. You still need to confirm it with a payment method.

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.

javascript
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);
}
In production, use saved payment methods (pm_*) rather than raw card data.

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.

javascript
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);
Idempotency keys should be unique per logical operation, not per API call.

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.

javascript
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);
}
Always return the client_secret to the frontend if 3D Secure is needed.
Tip: Use 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.

javascript
// 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);
}
The client_secret is safe to expose to your frontend—it only allows confirming this specific Payment Intent.
Tip: Always use 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_action status—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.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free