The Payment Intent Model
A Payment Intent is Stripe's core object for tracking and managing payments throughout their entire lifecycle. When you create a Payment Intent, you're initiating a payment session that Stripe will guide through various states until it reaches a terminal state--either successfully processed or definitively canceled. This model replaces the older Charges API, offering superior handling of complex payment scenarios, automatic retry capabilities, and built-in support for regulatory requirements like SCA.
The Payment Intent encapsulates all the information needed to process a payment, including the amount, currency, payment method types, and configuration options. Stripe maintains the authoritative state, and your application interacts through API calls that trigger state transitions. Understanding these states is essential for building robust payment experiences that gracefully handle every scenario your customers might encounter. For complex e-commerce implementations, working with experienced web development teams ensures your payment infrastructure scales with your business growth.
Payment Intent States
The Payment Intent moves through well-defined states during its lifecycle:
requires_payment_method
The initial state. Stripe is waiting for a payment method to be provided through your customer's interaction with a payment form.
processing
Stripe is attempting to authorize the payment with the issuing bank after a payment method is attached and confirmed.
requires_capture
For manual capture mode, the authorization succeeded but the payment hasn't been finalized yet.
requires_action
Additional verification is needed, such as 3D Secure authentication or SCA compliance requirements.
succeeded
The payment completed successfully.
canceled
The payment cannot proceed and has ended unsuccessfully.
Your application's logic must respond appropriately to each state, typically by rendering different UI states to your customers and triggering appropriate backend actions. See the Stripe Payment Intents documentation for additional context on handling these states in your integration.
Creating a Payment Intent
The payment lifecycle begins when your server creates a Payment Intent through the Stripe API. This creation step establishes the payment record and configures how Stripe should process the transaction. Your server should create a Payment Intent in response to a customer action--perhaps clicking a checkout button, submitting an order form, or initiating a subscription signup. The Payment Intent captures the payment amount, currency, and your acceptance criteria before any customer interaction occurs.
When creating a Payment Intent, specify the amount in the smallest currency unit (so $29.99 becomes 2999 for USD), the currency as a lowercase three-letter code, and configure automatic_payment_methods to let Stripe enable all relevant payment types for your account. The capture_method parameter deserves particular attention: setting it to automatic (the default) means Stripe captures the payment immediately after authorization, while manual gives you control to verify orders before finalizing charges.
The return value includes the client_secret, which your server must securely pass to the frontend. This secret enables your frontend to interact with the Payment Intent for confirmation without exposing sensitive operations to the browser.
1import Stripe from 'stripe';2 3const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);4 5export async function createPaymentIntent(6 amount: number,7 currency: string = 'usd',8 customerId?: string,9 metadata?: Record<string, string>10) {11 const idempotencyKey = `pi_${Date.now()}_${Math.random().toString(36).slice(2)}`;12 13 const paymentIntent = await stripe.paymentIntents.create(14 {15 amount,16 currency,17 automatic_payment_methods: { enabled: true },18 customer: customerId,19 metadata: metadata || {},20 capture_method: 'automatic', // or 'manual' for delayed capture21 },22 { idempotencyKey }23 );24 25 return paymentIntent;26}Confirming the Payment Intent
Confirmation is where Stripe validates the payment method, attempts authorization, and either completes the payment or determines additional action is required. This typically occurs client-side using Stripe's JavaScript SDK, though you can also confirm from your server when the payment method is already saved.
The stripe.confirmPayment method handles the complex work of communicating with payment networks, presenting authentication challenges when needed, and returning a clear result. The key insight is that confirmation might not immediately result in a completed payment--your code must handle all possible outcomes including requires_action for additional verification.
For scenarios where you want to avoid redirects, use redirect: 'if_required'. This tells Stripe to only redirect if the payment method absolutely requires it, allowing smoother flows for cards and digital wallets that complete entirely within your page. Your success page should verify Payment Intent status through your backend rather than trusting URL parameters directly.
1const { paymentIntent, error } = await stripe.confirmPayment({2 elements,3 confirmParams: {4 return_url: `${window.location.origin}/checkout/success`,5 },6 redirect: 'if_required', // Only redirect if absolutely necessary7});8 9if (error) {10 console.error(error.message);11 handlePaymentError(error);12} else if (paymentIntent.status === 'succeeded') {13 // Payment completed successfully14 handlePaymentSuccess(paymentIntent);15} else if (paymentIntent.status === 'requires_action') {16 // Additional verification needed17 showAuthenticationRequired();18}Handling Payment Actions
The requires_action state indicates Stripe needs additional verification--commonly 3D Secure authentication for SCA compliance or fraud checks. When a Payment Intent enters this state, your frontend must handle the authentication challenge before the payment can proceed. Stripe's JavaScript SDK automatically manages most of this process, presenting the appropriate verification UI and handling communication with authentication servers.
For 3D Secure authentication, Stripe handles the challenge flow automatically when you use Payment Elements. The customer sees a verification modal from their card issuer, provides the required authentication (typically a one-time code or biometric confirmation), and Stripe receives the result. Your responsibility is to detect this state, allow the SDK to complete its work without interruption, and handle the delay gracefully--authentication can take 10-30 seconds. See our guide on Payment Methods for more on authentication flows.
1switch (paymentIntent.status) {2 case 'succeeded':3 navigateToSuccessPage();4 break;5 case 'processing':6 showProcessingMessage();7 break;8 case 'requires_action':9 // Stripe SDK handles authentication automatically10 showAuthenticationRequired();11 break;12 case 'requires_payment_method':13 showPaymentMethodRejected(paymentIntent.last_payment_error?.message);14 break;15 case 'canceled':16 showPaymentCanceled();17 break;18 default:19 showUnexpectedState(paymentIntent.status);20}Capturing Authorized Payments
For credit and debit cards, there's a distinction between authorization and capture. Automatic capture (default) completes payment immediately after authorization--ideal for immediate fulfillment scenarios. Manual capture gives you control to verify orders before finalizing charges, moving the Payment Intent to requires_capture until you explicitly call the capture API.
Implementing manual capture requires careful consideration of your business workflow. Establish clear SLAs for how long you'll hold authorizations before capturing or canceling--holding funds too long can frustrate customers and may violate card network rules. Many merchants capture within 24-48 hours, and card networks typically limit uncaptured authorizations to 7 days. Your system should track authorization timestamps and implement automated cleanup for any uncaptured payments that exceed your business rules. See Stripe Billing for subscription scenarios that often use manual capture.
1// Manual capture for delayed processing2export async function capturePayment(paymentIntentId: string) {3 const paymentIntent = await stripe.paymentIntents.capture(paymentIntentId);4 if (paymentIntent.status === 'succeeded') {5 await fulfillOrder(paymentIntent);6 }7 return paymentIntent;8}9 10// Check uncaptured authorizations11export async function getUncapturedPayments() {12 const paymentIntents = await stripe.paymentIntents.list({13 capture_method: 'manual',14 limit: 100,15 });16 return paymentIntents.data.filter(pi => pi.status === 'requires_capture');17}18 19// Cancel stale authorization20export async function cancelAuthorization(paymentIntentId: string) {21 return stripe.paymentIntents.cancel(paymentIntentId);22}Error Handling and Recovery
Robust payment integrations must handle errors gracefully at every stage. Stripe provides detailed error objects with types, codes, and messages for handling different scenarios. Common error types include card_error for problems with the submitted payment method (declined card, insufficient funds), validation_error for incorrect request parameters, and api_connection_error for network issues.
Idempotency is crucial for payment error handling. Network interruptions, timeouts, and retries can lead to duplicate API calls if your requests aren't properly idempotent. Stripe supports idempotency keys that prevent the same request from being executed twice--generate and store these keys for critical operations, ensuring retries don't create duplicate payments. For handling payment method issues, implement a graceful fallback strategy that clearly explains issues and provides straightforward paths to try alternatives.
1const errorMappings: Record<string, { userMessage: string; action: string }> = {2 'card_declined': {3 userMessage: 'Your card was declined. Please try a different payment method.',4 action: 'retry_with_different_method',5 },6 'insufficient_funds': {7 userMessage: 'Insufficient funds. Please try another payment method.',8 action: 'retry_with_different_method',9 },10 'expired_card': {11 userMessage: 'Your card has expired. Please use a different card.',12 action: 'retry_with_different_method',13 },14 'incorrect_cvc': {15 userMessage: 'The security code (CVC) is incorrect. Please check and try again.',16 action: 'retry_same_method',17 },18 'processing_error': {19 userMessage: 'A processing error occurred. Please try again.',20 action: 'retry_same_method',21 },22 'authentication_required': {23 userMessage: 'Your card requires additional verification.',24 action: 'redirect_to_authentication',25 }26};Webhooks and Asynchronous Events
While client-side confirmation provides immediate feedback, the authoritative record comes through webhooks. Stripe sends events when Payment Intents transition between states, complete successfully, or fail. Relying solely on client-side callbacks leaves your integration vulnerable to users who close their browser before confirmation completes, network issues, and race conditions.
Implementing reliable webhook handling requires several best practices. Always verify the webhook signature to ensure the event came from Stripe. Respond quickly (within a few seconds) to acknowledge receipt, then process asynchronously. Make handlers idempotent--the same event might be delivered multiple times due to retry logic. Log all webhook events for debugging and maintain a replay capability for failed events.
1export async function POST(request: Request) {2 const body = await request.text();3 const signature = request.headers.get('stripe-signature');4 5 const event = stripe.webhooks.constructEvent(6 body,7 signature,8 process.env.STRIPE_WEBHOOK_SECRET!9 );10 11 switch (event.type) {12 case 'payment_intent.succeeded':13 await handleSuccessfulPayment(event.data.object);14 break;15 case 'payment_intent.payment_failed':16 await handleFailedPayment(event.data.object);17 break;18 case 'payment_intent.processing':19 await handlePaymentProcessing(event.data.object);20 break;21 case 'payment_intent.canceled':22 await handleCanceledPayment(event.data.object);23 break;24 }25 26 return new Response(JSON.stringify({ received: true }), { status: 200 });27}Best Practices for Production
Deploying payment integrations to production requires attention to security, reliability, and compliance. Never expose your secret API key in client-side code--use separate keys for testing and production, and consider restricted keys that only have access to needed resources. Regular key rotation and monitoring for unauthorized usage are essential security practices.
Monitoring and alerting should cover all aspects of the payment lifecycle. Set up alerts for unusual patterns such as spikes in failed payments, increases in authentication requirements, or delays in webhook delivery. Track key metrics including successful payment rate, average time to payment completion, and webhook processing latency. Integrating with SEO analytics services can help you correlate payment success rates with site performance and user journey metrics.
PCI compliance is handled largely by Stripe's infrastructure when you use their client libraries, but your integration must still follow secure practices. Never log or store full card numbers, CVV codes, or authentication codes. Use Stripe's hosted payment pages or Elements to keep sensitive data out of your systems entirely.
Performance Optimization
Payment processing speed directly impacts conversion rates and customer experience. Minimize round trips by creating Payment Intents server-side and passing the client secret directly to your checkout page rather than creating them on demand during checkout. Cache customer and subscription information to avoid additional API calls during checkout.
Frontend performance matters too. Load Stripe.js asynchronously to prevent it from blocking page render. Preload the Stripe key and initialize Stripe early in the page lifecycle. For returning customers with saved payment methods, use setup_future_usage to prepare payment methods during checkout without charging them, making subsequent checkouts nearly instant. Leveraging AI-powered automation can help optimize checkout flows and reduce cart abandonment through intelligent payment routing and failure recovery.
Batch operations for managing multiple payments reduce API overhead and improve throughput. The Stripe API supports retrieving and operating on multiple Payment Intents in single calls, which can significantly reduce latency when processing large volumes of payments.
Verify your Payment Intent implementation covers all essential components for production
Server-side creation
Payment Intents created with validated amounts, complete metadata, and idempotency keys
Client-side handling
All payment confirmation outcomes handled, including requires_action states and redirects
Webhook verification
Signature verification, idempotent updates, and proper event handling for all states
Error recovery
User-friendly error messages with actionable recovery options for each error type
Security practices
Secret keys protected server-side, PCI compliance maintained, idempotency implemented
Monitoring
Alerts configured, success rates tracked, dashboards operational for all lifecycle events
Frequently Asked Questions
Sources
- Stripe: How PaymentIntents and SetupIntents work - Official documentation on Payment Intent lifecycle states and transitions
- Stripe: The Payment Intents API - API reference and implementation best practices
- Digital Thrive Knowledge Base: Stripe - Our internal positioning and recommended approach for Stripe integration