Verify a PaymentIntent's status by calling stripe.paymentIntents.retrieve(paymentIntentId). The status field tells you exactly where the payment is in its lifecycle: requires_payment_method, requires_confirmation, requires_action (3DS), processing, succeeded, or canceled. Always check status server-side rather than relying on frontend redirects.
Understanding PaymentIntent Status in Stripe
A PaymentIntent tracks the lifecycle of a payment from creation to completion. Its status field transitions through several states: requires_payment_method (no card yet), requires_confirmation (card attached, awaiting confirm), requires_action (3DS needed), processing (charge in progress), succeeded (done), or canceled. Checking this status server-side is the reliable way to know whether a customer has paid — never trust a frontend redirect alone, because customers can navigate to your success URL without paying.
Prerequisites
- A Stripe account with test API keys
- Node.js 18 or newer installed
- The stripe npm package installed
- At least one PaymentIntent created (from a previous payment flow)
Step-by-step guide
Retrieve a PaymentIntent by ID
Retrieve a PaymentIntent by ID
Use stripe.paymentIntents.retrieve() with the pi_ ID to get the current state of a payment.
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);23const paymentIntent = await stripe.paymentIntents.retrieve('pi_ABC123');45console.log('Status:', paymentIntent.status);6console.log('Amount:', paymentIntent.amount / 100);7console.log('Currency:', paymentIntent.currency);Expected result: The PaymentIntent object is returned with the current status, amount, currency, and all associated data.
Handle each status in your application
Handle each status in your application
Write a switch statement to handle each possible PaymentIntent status and take the appropriate action in your application.
1function handlePaymentStatus(paymentIntent) {2 switch (paymentIntent.status) {3 case 'succeeded':4 // Payment complete — fulfill the order5 console.log('Payment succeeded! Fulfill order.');6 break;7 case 'processing':8 // Payment is processing — wait for webhook9 console.log('Payment processing. Will confirm via webhook.');10 break;11 case 'requires_action':12 // 3DS or other action needed — tell frontend13 console.log('Customer action required (3DS).');14 break;15 case 'requires_payment_method':16 // Payment failed — customer needs to retry17 console.log('Payment failed. Customer should try another card.');18 break;19 case 'canceled':20 console.log('Payment was canceled.');21 break;22 default:23 console.log('Unexpected status:', paymentIntent.status);24 }25}Expected result: Your application responds appropriately to each payment state.
Verify payment on your success page
Verify payment on your success page
When a customer lands on your success URL, retrieve the PaymentIntent server-side to confirm the payment actually succeeded. The success URL should include the PaymentIntent ID or Checkout Session ID.
1app.get('/success', async (req, res) => {2 const { payment_intent } = req.query;34 if (!payment_intent) {5 return res.status(400).send('Missing payment_intent parameter');6 }78 const paymentIntent = await stripe.paymentIntents.retrieve(payment_intent);910 if (paymentIntent.status === 'succeeded') {11 res.send('Payment confirmed! Thank you for your purchase.');12 } else {13 res.send(`Payment status: ${paymentIntent.status}. Please contact support.`);14 }15});Expected result: The success page confirms the payment status before showing a confirmation message.
Set up a webhook for reliable confirmation
Set up a webhook for reliable confirmation
Webhooks are the most reliable way to confirm payment status. The payment_intent.succeeded event fires even if the customer closes their browser during 3DS.
1app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {2 const sig = req.headers['stripe-signature'];3 let event;45 try {6 event = stripe.webhooks.constructEvent(7 req.body,8 sig,9 process.env.STRIPE_WEBHOOK_SECRET10 );11 } catch (err) {12 return res.status(400).send(`Webhook Error: ${err.message}`);13 }1415 if (event.type === 'payment_intent.succeeded') {16 const pi = event.data.object;17 console.log('Confirmed payment:', pi.id, pi.amount / 100);18 // Fulfill the order here19 }2021 res.json({ received: true });22});Expected result: Your server receives a webhook event confirming the payment succeeded, regardless of what the customer did in their browser.
Complete working example
1const express = require('express');2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);34const app = express();56// Webhook must use raw body7app.post('/webhook',8 express.raw({ type: 'application/json' }),9 (req, res) => {10 const sig = req.headers['stripe-signature'];11 let event;1213 try {14 event = stripe.webhooks.constructEvent(15 req.body,16 sig,17 process.env.STRIPE_WEBHOOK_SECRET18 );19 } catch (err) {20 return res.status(400).send(`Webhook Error: ${err.message}`);21 }2223 switch (event.type) {24 case 'payment_intent.succeeded':25 console.log('Payment confirmed:', event.data.object.id);26 // TODO: fulfill order, send confirmation email27 break;28 case 'payment_intent.payment_failed':29 console.log('Payment failed:', event.data.object.id);30 // TODO: notify customer, update order status31 break;32 }3334 res.json({ received: true });35 }36);3738app.use(express.json());3940// Verify payment status endpoint41app.get('/api/payment-status/:id', async (req, res) => {42 try {43 const paymentIntent = await stripe.paymentIntents.retrieve(req.params.id);44 res.json({45 id: paymentIntent.id,46 status: paymentIntent.status,47 amount: paymentIntent.amount / 100,48 currency: paymentIntent.currency,49 created: new Date(paymentIntent.created * 1000).toISOString(),50 });51 } catch (err) {52 if (err.code === 'resource_missing') {53 return res.status(404).json({ error: 'PaymentIntent not found' });54 }55 res.status(500).json({ error: err.message });56 }57});5859const PORT = process.env.PORT || 3000;60app.listen(PORT, () => console.log(`Server on port ${PORT}`));Common mistakes when verifying PaymentIntent status in Stripe API
Why it's a problem: Trusting the success URL redirect as proof of payment
How to avoid: Anyone can navigate to your success URL manually. Always retrieve the PaymentIntent server-side and verify status === 'succeeded'.
Why it's a problem: Not handling the processing status
How to avoid: Some payment methods (like bank debits) stay in processing for days. Do not fulfill the order until you receive the succeeded webhook.
Why it's a problem: Parsing the webhook body as JSON before passing to constructEvent
How to avoid: Use express.raw({ type: 'application/json' }) for the webhook route. The constructEvent function needs the raw body bytes for signature verification.
Best practices
- Always verify payment status server-side — never trust frontend redirects alone
- Use webhooks (payment_intent.succeeded) as the primary confirmation mechanism
- Use express.raw() for your webhook endpoint to preserve the raw body for signature verification
- Handle all possible PaymentIntent statuses in your application logic
- Log PaymentIntent status changes for debugging and audit purposes
- Set up idempotent order fulfillment to handle duplicate webhook deliveries
- Test with 4242424242424242 for success and decline cards for failures to verify all status paths
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Write a Node.js Express server with two endpoints: GET /payment-status/:id that retrieves a Stripe PaymentIntent and returns its status, and POST /webhook that verifies Stripe webhook signatures and handles payment_intent.succeeded events. Use stripe.webhooks.constructEvent with raw body.
Add payment verification to my app. After a Stripe payment redirect, verify the PaymentIntent status server-side before showing a success message. Also set up a webhook endpoint for payment_intent.succeeded as the reliable confirmation source.
Frequently asked questions
What are all possible PaymentIntent statuses?
The statuses are: requires_payment_method, requires_confirmation, requires_action, processing, requires_capture (for manual capture), succeeded, and canceled.
How long can a PaymentIntent stay in processing?
Card payments process in seconds. Bank debits (ACH, SEPA) can take 2-5 business days. Boleto and OXXO payments can take up to 7 days.
Can a succeeded PaymentIntent change status?
No. Once a PaymentIntent reaches succeeded, it stays there. Refunds are tracked separately on the associated Charge object, not by changing the PaymentIntent status.
How do I check status for a Checkout Session instead of a PaymentIntent?
Retrieve the Checkout Session with stripe.checkout.sessions.retrieve(sessionId) and check payment_status. It will be 'paid', 'unpaid', or 'no_payment_required'.
What if I need a reliable payment verification system for a high-volume application?
For applications processing thousands of payments where reliable fulfillment is critical, the RapidDev team can help build a webhook-driven system with idempotent processing, retry logic, and comprehensive monitoring.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation