Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to verify PaymentIntent status in Stripe API

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.

What you'll learn

  • The full PaymentIntent status lifecycle and what each status means
  • How to retrieve and check PaymentIntent status via the API
  • How to use webhooks as the source of truth for payment confirmation
  • How to handle each status in your application logic
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner5 min read10 minutesStripe API v2024-12+, Node.js 18+, any backend frameworkMarch 2026RapidDev Engineering Team
TL;DR

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

1

Retrieve a PaymentIntent by ID

Use stripe.paymentIntents.retrieve() with the pi_ ID to get the current state of a payment.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3const paymentIntent = await stripe.paymentIntents.retrieve('pi_ABC123');
4
5console.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.

2

Handle each status in your application

Write a switch statement to handle each possible PaymentIntent status and take the appropriate action in your application.

typescript
1function handlePaymentStatus(paymentIntent) {
2 switch (paymentIntent.status) {
3 case 'succeeded':
4 // Payment complete — fulfill the order
5 console.log('Payment succeeded! Fulfill order.');
6 break;
7 case 'processing':
8 // Payment is processing — wait for webhook
9 console.log('Payment processing. Will confirm via webhook.');
10 break;
11 case 'requires_action':
12 // 3DS or other action needed — tell frontend
13 console.log('Customer action required (3DS).');
14 break;
15 case 'requires_payment_method':
16 // Payment failed — customer needs to retry
17 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.

3

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.

typescript
1app.get('/success', async (req, res) => {
2 const { payment_intent } = req.query;
3
4 if (!payment_intent) {
5 return res.status(400).send('Missing payment_intent parameter');
6 }
7
8 const paymentIntent = await stripe.paymentIntents.retrieve(payment_intent);
9
10 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.

4

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.

typescript
1app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
2 const sig = req.headers['stripe-signature'];
3 let event;
4
5 try {
6 event = stripe.webhooks.constructEvent(
7 req.body,
8 sig,
9 process.env.STRIPE_WEBHOOK_SECRET
10 );
11 } catch (err) {
12 return res.status(400).send(`Webhook Error: ${err.message}`);
13 }
14
15 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 here
19 }
20
21 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

verify-payment.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5
6// Webhook must use raw body
7app.post('/webhook',
8 express.raw({ type: 'application/json' }),
9 (req, res) => {
10 const sig = req.headers['stripe-signature'];
11 let event;
12
13 try {
14 event = stripe.webhooks.constructEvent(
15 req.body,
16 sig,
17 process.env.STRIPE_WEBHOOK_SECRET
18 );
19 } catch (err) {
20 return res.status(400).send(`Webhook Error: ${err.message}`);
21 }
22
23 switch (event.type) {
24 case 'payment_intent.succeeded':
25 console.log('Payment confirmed:', event.data.object.id);
26 // TODO: fulfill order, send confirmation email
27 break;
28 case 'payment_intent.payment_failed':
29 console.log('Payment failed:', event.data.object.id);
30 // TODO: notify customer, update order status
31 break;
32 }
33
34 res.json({ received: true });
35 }
36);
37
38app.use(express.json());
39
40// Verify payment status endpoint
41app.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});
58
59const 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.

ChatGPT Prompt

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.

Stripe Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.