# How to verify PaymentIntent status in Stripe API

- Tool: Stripe
- Difficulty: Beginner
- Time required: 10 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+, any backend framework
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const paymentIntent = await stripe.paymentIntents.retrieve('pi_ABC123');

console.log('Status:', paymentIntent.status);
console.log('Amount:', paymentIntent.amount / 100);
console.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.

```
function handlePaymentStatus(paymentIntent) {
  switch (paymentIntent.status) {
    case 'succeeded':
      // Payment complete — fulfill the order
      console.log('Payment succeeded! Fulfill order.');
      break;
    case 'processing':
      // Payment is processing — wait for webhook
      console.log('Payment processing. Will confirm via webhook.');
      break;
    case 'requires_action':
      // 3DS or other action needed — tell frontend
      console.log('Customer action required (3DS).');
      break;
    case 'requires_payment_method':
      // Payment failed — customer needs to retry
      console.log('Payment failed. Customer should try another card.');
      break;
    case 'canceled':
      console.log('Payment was canceled.');
      break;
    default:
      console.log('Unexpected status:', paymentIntent.status);
  }
}
```

**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.

```
app.get('/success', async (req, res) => {
  const { payment_intent } = req.query;

  if (!payment_intent) {
    return res.status(400).send('Missing payment_intent parameter');
  }

  const paymentIntent = await stripe.paymentIntents.retrieve(payment_intent);

  if (paymentIntent.status === 'succeeded') {
    res.send('Payment confirmed! Thank you for your purchase.');
  } else {
    res.send(`Payment status: ${paymentIntent.status}. Please contact support.`);
  }
});
```

**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.

```
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payment_intent.succeeded') {
    const pi = event.data.object;
    console.log('Confirmed payment:', pi.id, pi.amount / 100);
    // Fulfill the order here
  }

  res.json({ received: true });
});
```

**Expected result:** Your server receives a webhook event confirming the payment succeeded, regardless of what the customer did in their browser.

## Complete code example

File: `verify-payment.js`

```javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

// Webhook must use raw body
app.post('/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['stripe-signature'];
    let event;

    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch (err) {
      return res.status(400).send(`Webhook Error: ${err.message}`);
    }

    switch (event.type) {
      case 'payment_intent.succeeded':
        console.log('Payment confirmed:', event.data.object.id);
        // TODO: fulfill order, send confirmation email
        break;
      case 'payment_intent.payment_failed':
        console.log('Payment failed:', event.data.object.id);
        // TODO: notify customer, update order status
        break;
    }

    res.json({ received: true });
  }
);

app.use(express.json());

// Verify payment status endpoint
app.get('/api/payment-status/:id', async (req, res) => {
  try {
    const paymentIntent = await stripe.paymentIntents.retrieve(req.params.id);
    res.json({
      id: paymentIntent.id,
      status: paymentIntent.status,
      amount: paymentIntent.amount / 100,
      currency: paymentIntent.currency,
      created: new Date(paymentIntent.created * 1000).toISOString(),
    });
  } catch (err) {
    if (err.code === 'resource_missing') {
      return res.status(404).json({ error: 'PaymentIntent not found' });
    }
    res.status(500).json({ error: err.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server on port ${PORT}`));
```

## Common mistakes

- **Trusting the success URL redirect as proof of payment** — undefined Fix: Anyone can navigate to your success URL manually. Always retrieve the PaymentIntent server-side and verify status === 'succeeded'.
- **Not handling the processing status** — undefined Fix: Some payment methods (like bank debits) stay in processing for days. Do not fulfill the order until you receive the succeeded webhook.
- **Parsing the webhook body as JSON before passing to constructEvent** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-verify-payment-intent-status-in-stripe-api
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-verify-payment-intent-status-in-stripe-api
