Payment Intents

A complete guide to Stripe's payment infrastructure for building robust, secure payment flows that handle complex payment scenarios.

Payment Intents: A Complete Guide to Stripe's Payment Infrastructure

Payment Intents represent the core payment object in Stripe's infrastructure, guiding you through the entire process of collecting payments from your customers. Whether you're building an e-commerce platform, a subscription service, or a marketplace, understanding Payment Intents is essential for implementing robust payment flows that handle complex scenarios like 3D Secure authentication, payment method selection, and post-authorization updates.

At its most fundamental level, a Payment Intent encapsulates all the information needed to process a payment: the amount, currency, payment methods to accept, and the current status of the payment attempt. Stripe's Payment Intents API replaces the older Charges API as the recommended approach for handling payments, offering better support for Strong Customer Authentication (SCA) requirements, dynamic payment method selection, and complex payment flows that may require multiple steps to complete.

The Payment Intent model is designed to be flexible enough to handle simple one-step payments while also supporting sophisticated multi-step flows where customers may need to verify their identity, select different payment methods, or authorize payments that aren't captured immediately. This flexibility makes Payment Intents the foundation for building payment experiences that work across different regions, payment methods, and regulatory requirements.

Why Payment Intents?

Core capabilities that make Payment Intents the foundation of modern payment integrations

Status-Driven Lifecycle

Clear state machine tracks every payment from creation through final success or cancellation, making it easy to build correct payment flows.

Multi-Step Payment Flows

Support for complex scenarios including 3D Secure authentication, payment method selection, and authorization without immediate capture.

Global Payment Methods

Accept cards, digital wallets, bank transfers, and localized payment methods with a single integration using the Payment Element.

SCA Compliance

Built-in support for Strong Customer Authentication requirements in Europe and other regulated markets.

What Is a Payment Intent?

A Payment Intent is a Stripe object that represents your intent to collect payment from a customer. It tracks the lifecycle of a payment attempt from creation through final success or failure, providing you with real-time status updates and handling the complexity of different payment method behaviors. When you create a Payment Intent, you're essentially telling Stripe that you want to collect a specific amount from a customer, and Stripe provides you with the tools and workflows to complete that collection.

Key Components

The Payment Intent object contains several key pieces of information that define your payment request:

  • Amount: The amount to charge, expressed in the smallest currency unit (cents for USD)
  • Currency: The three-letter ISO currency code for the payment
  • Payment Methods: Configuration for which payment methods to accept
  • Status: The current state of the payment lifecycle
  • Metadata: Custom data for associating payments with your business records

Payment Method Configuration

Stripe provides two approaches for configuring which payment methods to accept: automatic_payment_methods and explicit payment_method_types. The modern integration pattern uses automatic_payment_methods, which lets Stripe automatically determine which payment methods to display based on your account settings and the customer's location. This approach simplifies your integration because you don't need to explicitly list every payment method you accept--Stripe handles the logic of determining which methods are valid for each transaction based on your business profile and regional regulations.

For more control over the payment methods available to your customers, you can specify payment_method_types as an array to explicitly define which payment methods to enable. This approach is useful when you want to restrict payments to specific methods or when you need fine-grained control over which payment options appear in your checkout flow.

ConfigurationUse CaseBenefits
automatic_payment_methodsMost integrations, new projectsSimplest setup, automatically adapts to new payment methods
payment_method_types: ['card']Card-only acceptanceFocused UI, simpler compliance
payment_method_types: ['card', 'us_bank_account']Marketplaces requiring bank paymentsExplicit control over enabled methods
payment_method_types: ['klarna', 'afterpay']Buy-now-pay-later focusTailored checkout experience

The metadata field in a Payment Intent accepts a JSON object containing up to 50 keys with string values, totaling no more than 500 characters. You might use metadata to store an order ID, customer reference, or any other information that helps you reconcile Stripe payments with your internal systems. The metadata is never used by Stripe for payment processing but is included in all webhook events related to the Payment Intent, making it invaluable for maintaining the connection between Stripe's payment processing and your business logic.

The Payment Intent Lifecycle

The lifecycle of a Payment Intent follows a well-defined state machine that guides a payment from initial creation through either successful completion or cancellation. Understanding this lifecycle is essential for building payment flows that handle all possible scenarios correctly, including cases where additional authentication is required, payment methods fail, or customers abandon the checkout process midway through.

Status States

StateDescription
requires_payment_methodInitial state, waiting for payment method
requires_confirmationReady for server-side confirmation
requires_actionCustomer must complete additional verification
processingPayment is being finalized
requires_capturePayment authorized but not yet captured
succeededPayment completed successfully
canceledPayment was abandoned or expired

State Transitions

When you first create a Payment Intent, it begins in the requires_payment_method state. This is the initial state where the Payment Intent is waiting for a payment method to be provided. At this point, your customer hasn't yet entered any payment information, or the previous payment method attempt failed. This state typically corresponds to displaying a payment form to your customer and waiting for them to provide card details or select a payment method.

After your customer provides payment details and you call the confirm method, the Payment Intent moves to either requires_confirmation or requires_action, depending on whether the payment requires additional verification. If the payment method is valid and doesn't require authentication, the Payment Intent enters requires_confirmation, meaning you need to explicitly confirm the payment on your server to finalize the charge. If the payment method requires additional verification--such as 3D Secure for European cards or other regional authentication requirements--the Payment Intent enters requires_action, indicating that your customer needs to complete an additional step before the payment can proceed.

For payments that enter requires_action, the flow typically involves redirecting your customer to an authentication page or displaying an embedded verification component. Once your customer completes the required action, the Payment Intent either moves to requires_confirmation (if confirmation is still needed) or begins processing the payment. During the processing phase, the Payment Intent is in the processing state, which indicates that Stripe is working to finalize the payment with the payment method provider.

The final states in the lifecycle are succeeded, canceled, and requires_capture. A Payment Intent reaches succeeded when the payment has been successfully processed and the funds have been secured. For payment methods that support authorization and capture separation (like cards), the Payment Intent might enter requires_capture instead, indicating that the payment has been authorized but not yet captured. You must explicitly capture authorized payments within the authorization window, or the authorization will expire and the payment will be canceled. The canceled state indicates that the payment attempt was abandoned, expired, or explicitly canceled, and no payment will be collected.

Creating a Payment Intent

Creating a Payment Intent is the first step in any Stripe payment flow. The API provides extensive options for customizing how you accept payments. The most common approach is to create the Payment Intent on your server when your customer begins the checkout process, then pass the client secret to your frontend where it can be used to initialize payment UI components like Stripe Elements or the Payment Element.

Core Parameters

The fundamental parameters for creating a Payment Intent include the amount you want to charge, the currency for the charge, and configuration for which payment methods to accept. The amount must always be expressed in the smallest currency unit--so $10.00 USD would be specified as 1000 cents. This approach eliminates floating-point precision issues and ensures consistent handling across different currencies. Similarly, the currency should be specified as a three-letter ISO code in lowercase, such as usd, eur, or gbp.

Stripe's modern integration pattern uses automatic_payment_methods to let Stripe automatically determine which payment methods to display based on your account settings and the customer's location. This approach simplifies your integration because you don't need to explicitly list every payment method you accept--Stripe handles the logic of which methods are valid for each transaction. For more control, you can still specify payment_method_types as an array to explicitly define which payment methods to enable.

Client Secret Usage

Your server should create the Payment Intent and then return the client_secret to your frontend. This client secret is used to initialize Stripe's client-side libraries and must never be exposed in public code or stored in databases. The frontend uses the client secret to communicate with Stripe about payment method selection and confirmation without ever touching your secret key, which remains securely on your server. When creating a Payment Intent, you should also consider whether you want to enable manual capture for cards. By default, Stripe uses automatic capture, meaning payments are captured immediately after authorization. However, for certain use cases like orders that might be fulfilled later, you might want to authorize a payment without capturing it immediately.

Creating a Payment Intent (Server-Side)
1import Stripe from 'stripe';2import { NextRequest, NextResponse } from 'next/server';3 4const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {5 apiVersion: '2025-04-30.preview',6});7 8export async function POST(request: NextRequest) {9 try {10 const { amount, currency = 'usd', customerId, metadata } = await request.json();11 12 const paymentIntent = await stripe.paymentIntents.create({13 amount: Math.round(amount * 100),14 currency,15 automatic_payment_methods: {16 enabled: true,17 },18 customer: customerId,19 metadata: metadata || {},20 });21 22 return NextResponse.json({23 clientSecret: paymentIntent.client_secret,24 paymentIntentId: paymentIntent.id,25 });26 } catch (error) {27 return NextResponse.json(28 { error: 'Failed to create payment intent' },29 { status: 500 }30 );31 }32}

Updating a Payment Intent

After creating a Payment Intent, you may need to update its details before the payment is finalized. Stripe allows modification of several aspects including amount, currency, and metadata. However, not all updates are allowed at every stage of the lifecycle, and some changes have specific restrictions to prevent fraud and ensure payment integrity.

Common Update Operations

  • Amount Updates: Adjusting the charge amount for price changes or discounts
  • Currency Changes: Modifying the payment currency
  • Metadata Updates: Adding or updating custom business data
  • Payment Method Configuration: Changing accepted payment methods

Update Restrictions

The most common update operation is modifying the amount, which you might need to do when prices change, discounts are applied, or shipping costs are calculated after the initial order was placed. When updating the amount, Stripe will reject the update if the payment has already been partially captured or if the new amount would exceed certain limits. For manual capture payments, you can update the amount even after authorization, which is useful for situations where final costs aren't known at the time of authorization.

When updating a Payment Intent that has already been confirmed but is in a state that allows modifications, you need to consider the impact on the customer experience. If the payment has already been authorized, changing the amount might require re-authorization depending on the payment method and the magnitude of the change. Stripe handles most of these complexities automatically, but it's important to communicate clearly with your customers when payment details change.

Updating the payment method configuration allows you to change which payment methods are accepted for a specific Payment Intent. This might be useful if you want to offer different payment options based on customer preferences or if you need to restrict accepted methods due to regional regulations. The update endpoint is the primary method for modifying Payment Intent details after creation. Your server makes this call using your secret key, and Stripe validates that the requested update is allowed based on the current state of the Payment Intent. For example, you cannot update the currency of a Payment Intent that has already moved beyond the initial states, and you cannot reduce the amount below what has already been captured.

Updating a Payment Intent
1import Stripe from 'stripe';2 3const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {4 apiVersion: '2025-04-30.preview',5});6 7export async function updatePaymentIntent(8 paymentIntentId: string,9 updates: { amount?: number; currency?: string; metadata?: Record<string, string> }10) {11 const paymentIntent = await stripe.paymentIntents.update(paymentIntentId, {12 amount: updates.amount ? Math.round(updates.amount * 100) : undefined,13 currency: updates.currency,14 metadata: updates.metadata,15 });16 17 return paymentIntent;18}

Handling Payment Intent Statuses

The Payment Intent status is the single most important piece of information for determining what to do next in your payment flow. Your application should always check the current status before taking action, and you should design your user interface to reflect the current state appropriately.

Frontend Status Handling

  • requires_payment_method: Display payment form and wait for customer input. This is the entry point for the payment flow, and it's where most customers will spend the most time--entering card details or selecting payment methods.
  • requires_action: Handle the authentication flow. For 3D Secure, this typically involves displaying an iframe or redirecting the customer to their bank's verification page. Stripe's client libraries handle most of this complexity automatically.
  • processing: Show a clear processing indicator. During this state, you should avoid making additional API calls that might interfere with the payment in progress. For some methods like bank transfers, the processing period might be longer.
  • succeeded: Fulfill the order or grant access to the service the customer paid for. This is your trigger to complete the business transaction, confident that the payment has been successfully processed.
  • canceled: Handle gracefully. This might happen because the customer abandoned the flow, the payment method failed too many times, or the authorization expired.

Status-Driven UX Design

Designing your UI to reflect the current Payment Intent status is crucial for providing a smooth customer experience. When the Payment Intent is in requires_payment_method, your frontend should display a payment form and wait for customer input. Stripe Elements or the Payment Element handle this state by providing a secure, pre-built UI that collects payment information.

For payments in processing, your application should communicate clearly with customers during this state, letting them know that their payment is being processed. You should still verify the Payment Intent status through your server (not just rely on client-side events) before fulfilling orders, as client-side events can sometimes be spoofed. Error messages should be clear and actionable--payment failures can occur for many reasons, and customers need guidance on what to do next.

Integration with Stripe Elements

Stripe Elements provides a set of pre-built, customizable UI components for collecting payment information securely. When used with Payment Intents, Elements handles the complexity of creating and confirming payment methods, presenting 3D Secure challenges, and managing the overall payment flow. This integration significantly reduces the development effort required to build PCI-compliant payment forms while providing a professional, consistent user experience.

Payment Element vs Card Element

The Payment Element is Stripe's newest and most capable payment UI component, designed to handle multiple payment method types with a single integration. Unlike the older Card Element, which only accepted cards, the Payment Element dynamically displays the payment methods you've enabled for your account, including cards, Apple Pay, Google Pay, bank transfers, and other regional payment methods. This dynamic behavior means that your payment form automatically adapts to support new payment methods as Stripe adds them, without requiring code changes.

Integration Flow

Integrating the Payment Element with a Payment Intent involves creating the Payment Intent on your server, passing the client secret to your frontend, and then initializing the Payment Element with that client secret. The Payment Element automatically handles the flow of collecting payment details, confirming the payment with Stripe, handling any required authentication, and reporting the status back to your application. For AI-powered automation workflows, you can extend this integration to trigger automated processes based on payment success events.

The Card Element remains available for integrations that only need to accept cards. While the Payment Element is recommended for most new integrations, the Card Element provides a more focused experience when cards are your only payment method and offers slightly more customization options for the card input field itself.

Payment Element Integration (React)
1'use client';2 3import { loadStripe } from '@stripe/stripe-js';4import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';5 6const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!);7 8function CheckoutForm() {9 const stripe = useStripe();10 const elements = useElements();11 const [message, setMessage] = useState<string | null>(null);12 const [isLoading, setIsLoading] = useState(false);13 14 const handleSubmit = async (e: React.FormEvent) => {15 e.preventDefault();16 if (!stripe || !elements) return;17 18 setIsLoading(true);19 const { error } = await stripe.confirmPayment({20 elements,21 confirmParams: { return_url: `${window.location.origin}/success` },22 });23 24 if (error) setMessage(error.message);25 setIsLoading(false);26 };27 28 return (29 <form onSubmit={handleSubmit}>30 <PaymentElement />31 <button disabled={isLoading || !stripe || !elements}>32 {isLoading ? 'Processing...' : 'Pay now'}33 </button>34 {message && <div>{message}</div>}35 </form>36 );37}38 39export function PaymentCheckout({ clientSecret }: { clientSecret: string }) {40 return (41 <Elements stripe={stripePromise} options={{ clientSecret }}>42 <CheckoutForm />43 </Elements>44 );45}

Webhook Handling for Payment Events

Webhooks provide a reliable way to receive notifications about Payment Intent status changes, even when customers leave your site before completing payment or when events occur asynchronously (like refunds or disputes). Instead of relying solely on client-side callbacks, webhook handlers ensure your application maintains accurate payment records and can trigger business logic reliably.

Essential Webhook Events

EventAction
payment_intent.succeededFulfill order, send confirmation
payment_intent.payment_failedNotify customer, offer retry
payment_intent.processingUpdate status, prevent duplicate orders
payment_intent.canceledHandle abandonment, free inventory

Security and Best Practices

Stripe sends webhook events whenever a Payment Intent changes status, and your server should handle these events to update your database, send order confirmations, and trigger any downstream processes. The most important event for Payment Intents is payment_intent.succeeded, which indicates that a payment has been completed successfully. Your webhook handler should verify the event's authenticity using the webhook signature, then process the payment accordingly. Proper webhook security is essential for maintaining the integrity of your payment infrastructure.

Always verify webhook signatures before processing any webhook event. Stripe signs each webhook with a secret that's unique to your endpoint, and you should use the Stripe libraries to verify this signature rather than implementing the verification logic yourself. This verification ensures that webhook events actually came from Stripe and not from a malicious actor trying to manipulate your payment records. Complementing secure webhook handling with professional SEO services helps ensure your payment-related pages are properly indexed and visible to search engines.

When implementing webhook handlers, it's crucial to handle idempotency--your handler might receive the same event multiple times due to retry logic or network issues. Your implementation should track which events have already been processed and avoid duplicating side effects. Stripe provides an id on each event that you can use for deduplication.

Webhook Handler for Payment Events
1import Stripe from 'stripe';2 3const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {4 apiVersion: '2025-04-30.preview',5});6const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;7 8export async function POST(request: NextRequest) {9 const body = await request.text();10 const signature = request.headers.get('stripe-signature')!;11 let event;12 13 try {14 event = stripe.webhooks.constructEvent(body, signature, webhookSecret);15 } catch (err) {16 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });17 }18 19 switch (event.type) {20 case 'payment_intent.succeeded':21 const pi = event.data.object as Stripe.PaymentIntent;22 await fulfillOrder(pi.metadata.orderId);23 break;24 case 'payment_intent.payment_failed':25 // Notify customer26 break;27 case 'payment_intent.canceled':28 // Handle cancellation29 break;30 }31 32 return NextResponse.json({ received: true });33}

Best Practices and Security Considerations

Security Essentials

Building robust payment flows requires attention to security at every layer of your application. Never expose your Stripe secret key in client-side code or in any repository that might be shared publicly. Use separate keys for test and live modes, and rotate your API keys immediately if you suspect any exposure. Stripe's documentation recommends using restricted keys for specific purposes to limit the impact of any potential key compromise.

  • Never expose secret keys in client-side code
  • Verify webhook signatures before processing events
  • Calculate amounts server-side and never trust client-submitted amounts
  • Use Stripe Elements to minimize PCI compliance burden
  • Implement comprehensive error handling for all payment scenarios

PCI Compliance

Using Stripe Elements or the Payment Element significantly reduces your PCI compliance burden because sensitive payment data never touches your servers. If you must handle payment data directly (which Stripe strongly discourages), you must implement appropriate security controls and may need to complete a PCI compliance assessment.

Error Handling

Error handling should be comprehensive and user-friendly. Payment failures can occur for many reasons--insufficient funds, expired cards, fraud checks, or authentication requirements. Your application should provide clear, actionable error messages to customers while logging detailed error information for debugging. Stripe error objects include a type field that categorizes errors and a code field that provides specific failure reasons, allowing you to implement appropriate responses for each failure scenario.

Error Handling Example
1try {2 const paymentIntent = await stripe.paymentIntents.create({3 amount: Math.round(amount * 100),4 currency,5 automatic_payment_methods: { enabled: true },6 });7} catch (error) {8 if (error instanceof Stripe.errors.StripeError) {9 switch (error.type) {10 case 'StripeCardError':11 return { success: false, message: 'Card declined. Try another payment method.' };12 case 'StripeRateLimitError':13 return { success: false, message: 'Too many requests. Please wait.' };14 case 'StripeInvalidRequestError':15 return { success: false, message: 'Payment error. Please try again.' };16 }17 }18}

Advanced Payment Intent Features

Payment Splits for Marketplaces

For marketplaces and platforms, you can use application fees and transfers to split payments between your platform and connected accounts. This enables you to take a commission while routing the remainder to sellers or service providers automatically.

Saving Payment Methods

The setup_future_usage parameter allows you to indicate that a payment method should be saved for future use. When this parameter is set, Stripe saves the payment method to the customer object after successful payment, enabling one-click payments on return visits. This feature is particularly valuable for subscription businesses or any service where customers make repeated purchases.

Multi-Currency Support

Accept payments in any of Stripe's supported currencies with automatic handling of currency conversion. You can create Payment Intents in any supported currency, and Stripe handles the conversion when the payment method is in a different currency. For businesses operating internationally, this flexibility allows you to display prices in local currencies while accepting payments from customers around the world.

Manual Capture

For orders fulfilled later, authorize payments without immediate capture using capture_method: 'manual'. This is useful when you need to verify inventory, confirm availability, or wait for a fulfillment window before finalizing the charge. The Payment Intent will enter requires_capture after successful authorization, and you must explicitly capture within the authorization window.

Integration with Billing

Combine Payment Intents with Stripe Billing for subscription initial payments while leveraging Billing's recurring billing capabilities for subsequent charges. This pattern ensures a smooth setup experience for new subscribers while automating ongoing collection. Building comprehensive payment solutions that integrate seamlessly with your existing systems requires expertise in both web development and payment infrastructure.

For businesses that need to collect payments in installments or require customer authentication before charging, Payment Intents can be combined with other Stripe features like Setup Intents (for saving payment methods without immediate charge) and manual capture (for authorizing payments before finalizing them). These combinations enable a wide range of payment scenarios while maintaining a consistent integration pattern.

Frequently Asked Questions

Ready to Build Secure Payment Flows?

Our team specializes in implementing robust payment infrastructure with Stripe. Let us help you build payment experiences that drive conversions while maintaining security and compliance.