/stripe-guides

How to verify payment intent status in Stripe API?

Learn how to verify payment intent status in Stripe API with step-by-step guides, code examples, and best practices for Node.js, Python, PHP, and Ruby.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to verify payment intent status in Stripe API?

How to Verify Payment Intent Status in Stripe API

 

Step 1: Set up your Stripe account and obtain API keys

 

Before you can verify payment intent status, you need to have a Stripe account and API keys. Here's how to get them:

  • Sign up for a Stripe account at https://stripe.com
  • Navigate to the Developers section in your Stripe Dashboard
  • Locate your API keys (you'll have both test and live keys)
  • Make sure to use test keys for development and live keys for production

Your API keys will look something like this:

  • Publishable key: pk_test_51Hd...
  • Secret key: sk_test_51Hd...

 

Step 2: Install the Stripe library

 

Depending on your programming language, install the appropriate Stripe library:

For Node.js:

npm install stripe

For Python:

pip install stripe

For PHP:

composer require stripe/stripe-php

For Ruby:

gem install stripe

 

Step 3: Initialize the Stripe client

 

Now, initialize the Stripe client with your secret API key:

For Node.js:

const stripe = require('stripe')('sk_test_your_secret_key');

For Python:

import stripe
stripe.api_key = 'sk_test_your_secret\_key'

For PHP:

\Stripe\Stripe::setApiKey('sk_test_your_secret_key');

For Ruby:

require 'stripe'
Stripe.api_key = 'sk_test_your_secret\_key'

 

Step 4: Retrieve a payment intent by ID

 

To verify a payment intent status, you first need to retrieve it using its ID:

For Node.js:

async function getPaymentIntent(paymentIntentId) {
  try {
    const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
    return paymentIntent;
  } catch (error) {
    console.error('Error retrieving payment intent:', error);
    throw error;
  }
}

For Python:

def get_payment_intent(payment_intent_id):
    try:
        payment_intent = stripe.PaymentIntent.retrieve(payment_intent\_id)
        return payment\_intent
    except Exception as e:
        print(f"Error retrieving payment intent: {e}")
        raise

For PHP:

function getPaymentIntent($paymentIntentId) {
    try {
        $paymentIntent = \Stripe\PaymentIntent::retrieve($paymentIntentId);
        return $paymentIntent;
    } catch (Exception $e) {
        echo 'Error retrieving payment intent: ' . $e->getMessage();
        throw $e;
    }
}

For Ruby:

def get_payment_intent(payment_intent_id)
  begin
    payment_intent = Stripe::PaymentIntent.retrieve(payment_intent\_id)
    return payment\_intent
  rescue Stripe::StripeError => e
    puts "Error retrieving payment intent: #{e.message}"
    raise
  end
end

 

Step 5: Check the payment intent status

 

After retrieving the payment intent, you can check its status. Here are the possible status values:

  • requires_payment_method: Initial status, waiting for payment details
  • requires\_confirmation: Payment details collected, needs confirmation
  • requires\_action: Additional actions needed (like 3D Secure)
  • processing: Payment is being processed
  • requires\_capture: Payment authorized, needs capture
  • canceled: Payment canceled
  • succeeded: Payment completed successfully

For Node.js:

async function checkPaymentStatus(paymentIntentId) {
  try {
    const paymentIntent = await getPaymentIntent(paymentIntentId);
    
    switch (paymentIntent.status) {
      case 'succeeded':
        console.log('Payment successful!');
        // Handle successful payment
        break;
      case 'processing':
        console.log('Payment is processing.');
        // Payment is processing, no action needed
        break;
      case 'requires_payment_method':
        console.log('Payment failed. Please try another payment method.');
        // Handle failed payment
        break;
      case 'requires\_action':
        console.log('Additional authentication required.');
        // Redirect customer to complete authentication
        break;
      case 'requires\_capture':
        console.log('Payment authorized, needs capture.');
        // Proceed with capturing the payment
        break;
      case 'canceled':
        console.log('Payment canceled.');
        // Handle canceled payment
        break;
      default:
        console.log(`Unexpected status: ${paymentIntent.status}`);
    }
    
    return paymentIntent.status;
  } catch (error) {
    console.error('Error checking payment status:', error);
    throw error;
  }
}

For Python:

def check_payment_status(payment_intent_id):
    try:
        payment_intent = get_payment_intent(payment_intent\_id)
        
        if payment\_intent.status == 'succeeded':
            print('Payment successful!')
            # Handle successful payment
        elif payment\_intent.status == 'processing':
            print('Payment is processing.')
            # Payment is processing, no action needed
        elif payment_intent.status == 'requires_payment\_method':
            print('Payment failed. Please try another payment method.')
            # Handle failed payment
        elif payment_intent.status == 'requires_action':
            print('Additional authentication required.')
            # Redirect customer to complete authentication
        elif payment_intent.status == 'requires_capture':
            print('Payment authorized, needs capture.')
            # Proceed with capturing the payment
        elif payment\_intent.status == 'canceled':
            print('Payment canceled.')
            # Handle canceled payment
        else:
            print(f'Unexpected status: {payment\_intent.status}')
        
        return payment\_intent.status
    except Exception as e:
        print(f"Error checking payment status: {e}")
        raise

For PHP:

function checkPaymentStatus($paymentIntentId) {
    try {
        $paymentIntent = getPaymentIntent($paymentIntentId);
        
        switch ($paymentIntent->status) {
            case 'succeeded':
                echo 'Payment successful!';
                // Handle successful payment
                break;
            case 'processing':
                echo 'Payment is processing.';
                // Payment is processing, no action needed
                break;
            case 'requires_payment_method':
                echo 'Payment failed. Please try another payment method.';
                // Handle failed payment
                break;
            case 'requires\_action':
                echo 'Additional authentication required.';
                // Redirect customer to complete authentication
                break;
            case 'requires\_capture':
                echo 'Payment authorized, needs capture.';
                // Proceed with capturing the payment
                break;
            case 'canceled':
                echo 'Payment canceled.';
                // Handle canceled payment
                break;
            default:
                echo 'Unexpected status: ' . $paymentIntent->status;
        }
        
        return $paymentIntent->status;
    } catch (Exception $e) {
        echo 'Error checking payment status: ' . $e->getMessage();
        throw $e;
    }
}

For Ruby:

def check_payment_status(payment_intent_id)
  begin
    payment_intent = get_payment_intent(payment_intent\_id)
    
    case payment\_intent.status
    when 'succeeded'
      puts 'Payment successful!'
      # Handle successful payment
    when 'processing'
      puts 'Payment is processing.'
      # Payment is processing, no action needed
    when 'requires_payment_method'
      puts 'Payment failed. Please try another payment method.'
      # Handle failed payment
    when 'requires\_action'
      puts 'Additional authentication required.'
      # Redirect customer to complete authentication
    when 'requires\_capture'
      puts 'Payment authorized, needs capture.'
      # Proceed with capturing the payment
    when 'canceled'
      puts 'Payment canceled.'
      # Handle canceled payment
    else
      puts "Unexpected status: #{payment\_intent.status}"
    end
    
    return payment\_intent.status
  rescue Stripe::StripeError => e
    puts "Error checking payment status: #{e.message}"
    raise
  end
end

 

Step 6: Implement a webhook to receive updates

 

For real-time status updates, set up a webhook to receive event notifications from Stripe.

First, set up the webhook endpoint in your Stripe Dashboard:

  • Go to the Developers section in your Stripe Dashboard
  • Click on Webhooks
  • Add an endpoint with the URL where you'll receive webhook events
  • Select events to listen to (at minimum, select "payment_intent.succeeded", "payment_intent.payment\_failed", etc.)

Then, implement a webhook handler in your application:

For Node.js (using Express):

const express = require('express');
const app = express();

// Use JSON parser for webhook events
app.use('/stripe-webhook', express.raw({type: 'application/json'}));

app.post('/stripe-webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

  try {
    // Create the event from payload and signature using your webhook secret
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      'whsec_your_webhook\_secret'
    );
  } catch (err) {
    console.log(`Webhook Error: ${err.message}`);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle the event
  switch (event.type) {
    case 'payment\_intent.succeeded':
      const paymentIntent = event.data.object;
      console.log(`PaymentIntent ${paymentIntent.id} was successful!`);
      // Update your database, fulfill the order, etc.
      break;
    case 'payment_intent.payment_failed':
      const failedPaymentIntent = event.data.object;
      console.log(`Payment failed for PaymentIntent ${failedPaymentIntent.id}`);
      // Notify the customer, update your database, etc.
      break;
    default:
      console.log(`Unhandled event type ${event.type}`);
  }

  // Return a 200 response to acknowledge receipt of the event
  res.status(200).json({received: true});
});

app.listen(3000, () => console.log('Running on port 3000'));

For Python (using Flask):

from flask import Flask, request, jsonify
import stripe

app = Flask(**name**)
stripe.api_key = 'sk_test_your_secret\_key'
webhook_secret = 'whsec_your_webhook_secret'

@app.route('/stripe-webhook', methods=['POST'])
def stripe\_webhook():
    payload = request.data
    sig\_header = request.headers.get('Stripe-Signature')

    try:
        event = stripe.Webhook.construct\_event(
            payload, sig_header, webhook_secret
        )
    except ValueError as e:
        # Invalid payload
        return jsonify({'error': str(e)}), 400
    except stripe.error.SignatureVerificationError as e:
        # Invalid signature
        return jsonify({'error': str(e)}), 400

    # Handle the event
    if event['type'] == 'payment\_intent.succeeded':
        payment\_intent = event\['data']\['object']
        print(f"PaymentIntent {payment\_intent['id']} was successful!")
        # Update your database, fulfill the order, etc.
    elif event['type'] == 'payment_intent.payment_failed':
        payment\_intent = event\['data']\['object']
        print(f"Payment failed for PaymentIntent {payment\_intent['id']}")
        # Notify the customer, update your database, etc.
    else:
        print(f"Unhandled event type {event['type']}")

    return jsonify({'status': 'success'}), 200

if **name** == '**main**':
    app.run(port=3000)

For PHP:

type) {
    case 'payment\_intent.succeeded':
        $paymentIntent = $event->data->object;
        echo "PaymentIntent " . $paymentIntent->id . " was successful!";
        // Update your database, fulfill the order, etc.
        break;
    case 'payment_intent.payment_failed':
        $paymentIntent = $event->data->object;
        echo "Payment failed for PaymentIntent " . $paymentIntent->id;
        // Notify the customer, update your database, etc.
        break;
    default:
        echo "Unhandled event type " . $event->type;
}

http_response_code(200);
?>

For Ruby (using Sinatra):

require 'sinatra'
require 'stripe'
require 'json'

Stripe.api_key = 'sk_test_your_secret\_key'
webhook_secret = 'whsec_your_webhook_secret'

post '/stripe-webhook' do
  payload = request.body.read
  sig_header = request.env['HTTP_STRIPE\_SIGNATURE']

  begin
    event = Stripe::Webhook.construct\_event(
      payload, sig_header, webhook_secret
    )
  rescue JSON::ParserError => e
    # Invalid payload
    halt 400, { error: e.message }.to\_json
  rescue Stripe::SignatureVerificationError => e
    # Invalid signature
    halt 400, { error: e.message }.to\_json
  end

  # Handle the event
  case event.type
  when 'payment\_intent.succeeded'
    payment\_intent = event.data.object
    puts "PaymentIntent #{payment\_intent.id} was successful!"
    # Update your database, fulfill the order, etc.
  when 'payment_intent.payment_failed'
    payment\_intent = event.data.object
    puts "Payment failed for PaymentIntent #{payment\_intent.id}"
    # Notify the customer, update your database, etc.
  else
    puts "Unhandled event type #{event.type}"
  end

  status 200
  { received: true }.to\_json
end

 

Step 7: Capturing a payment (for uncaptured payments)

 

If the payment intent status is "requires_capture" (which happens with manual capture), you'll need to capture the payment:

For Node.js:

async function capturePayment(paymentIntentId) {
  try {
    const paymentIntent = await stripe.paymentIntents.capture(paymentIntentId);
    console.log('Payment captured successfully!');
    return paymentIntent;
  } catch (error) {
    console.error('Error capturing payment:', error);
    throw error;
  }
}

For Python:

def capture_payment(payment_intent\_id):
    try:
        payment_intent = stripe.PaymentIntent.capture(payment_intent\_id)
        print('Payment captured successfully!')
        return payment\_intent
    except Exception as e:
        print(f"Error capturing payment: {e}")
        raise

For PHP:

function capturePayment($paymentIntentId) {
    try {
        $paymentIntent = \Stripe\PaymentIntent::capture($paymentIntentId);
        echo 'Payment captured successfully!';
        return $paymentIntent;
    } catch (Exception $e) {
        echo 'Error capturing payment: ' . $e->getMessage();
        throw $e;
    }
}

For Ruby:

def capture_payment(payment_intent\_id)
  begin
    payment_intent = Stripe::PaymentIntent.capture(payment_intent\_id)
    puts 'Payment captured successfully!'
    return payment\_intent
  rescue Stripe::StripeError => e
    puts "Error capturing payment: #{e.message}"
    raise
  end
end

 

Step 8: Implementing a complete verification workflow

 

Here's a complete example of how to implement payment verification in a real application:

For Node.js:

async function verifyAndProcessPayment(paymentIntentId) {
  try {
    const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
    
    // Log the current status
    console.log(`Payment status: ${paymentIntent.status}`);
    
    // Process based on status
    switch (paymentIntent.status) {
      case 'succeeded':
        // Payment was successful, fulfill the order
        await fulfillOrder(paymentIntent.metadata.orderId);
        return { success: true, message: 'Payment successful. Order fulfilled.' };
        
      case 'requires\_capture':
        // Payment is authorized but needs capture
        const capturedPayment = await stripe.paymentIntents.capture(paymentIntentId);
        await fulfillOrder(capturedPayment.metadata.orderId);
        return { success: true, message: 'Payment captured. Order fulfilled.' };
        
      case 'processing':
        // Payment is still processing
        await updateOrderStatus(paymentIntent.metadata.orderId, 'processing');
        return { success: true, message: 'Payment is processing. Order status updated.' };
        
      case 'requires_payment_method':
        // The initial payment attempt failed
        await updateOrderStatus(paymentIntent.metadata.orderId, 'payment\_failed');
        return { success: false, message: 'Payment failed. Customer needs to try again.' };
        
      case 'requires\_action':
        // Customer needs to complete additional authentication
        return { 
          success: false, 
          message: 'Additional authentication required.',
          requires\_action: true,
          payment_intent_client_secret: paymentIntent.client_secret
        };
        
      case 'canceled':
        // Payment was canceled
        await updateOrderStatus(paymentIntent.metadata.orderId, 'payment\_canceled');
        return { success: false, message: 'Payment was canceled.' };
        
      default:
        // Unknown status
        await updateOrderStatus(paymentIntent.metadata.orderId, 'unknown_payment_status');
        return { success: false, message: `Unexpected payment status: ${paymentIntent.status}` };
    }
  } catch (error) {
    console.error('Error processing payment:', error);
    // Log error and update order status
    await updateOrderStatus(error.metadata?.orderId, 'payment\_error');
    return { success: false, message: 'An error occurred while processing the payment.' };
  }
}

// Example helper functions
async function fulfillOrder(orderId) {
  // Logic to fulfill the order (e.g., ship products, send email, etc.)
  console.log(`Fulfilling order ${orderId}`);
}

async function updateOrderStatus(orderId, status) {
  // Logic to update the order status in your database
  console.log(`Updating order ${orderId} to status: ${status}`);
}

// Example usage in an Express route
app.get('/verify-payment/:paymentIntentId', async (req, res) => {
  try {
    const result = await verifyAndProcessPayment(req.params.paymentIntentId);
    res.json(result);
  } catch (error) {
    res.status(500).json({ success: false, message: error.message });
  }
});

 

Step 9: Handling errors and edge cases

 

When verifying payment intents, be sure to handle these common errors and edge cases:

  • Invalid payment intent ID: The payment intent might not exist or might be inaccessible
  • Expired payment intents: Payment intents expire after 24 hours if not confirmed
  • Network failures: Temporary connectivity issues with the Stripe API
  • Rate limiting: Stripe has rate limits that might affect your requests
  • Webhooks arriving out of order: Events might not always arrive in chronological order

Here's an example of handling these cases:

async function verifyPaymentWithErrorHandling(paymentIntentId) {
  try {
    const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
    return { success: true, status: paymentIntent.status, paymentIntent };
  } catch (error) {
    // Handle specific Stripe errors
    if (error.type === 'StripeInvalidRequestError') {
      if (error.code === 'resource\_missing') {
        return { success: false, error: 'Payment not found. It may have been deleted or the ID is incorrect.' };
      }
    }
    
    if (error.type === 'StripePermissionError') {
      return { success: false, error: 'No permission to access this payment.' };
    }
    
    if (error.type === 'StripeRateLimitError') {
      // Implement exponential backoff and retry
      console.log('Rate limited by Stripe. Retrying...');
      await new Promise(resolve => setTimeout(resolve, 1000));
      return verifyPaymentWithErrorHandling(paymentIntentId);
    }
    
    if (error.type === 'StripeAPIError' || error.type === 'StripeConnectionError') {
      // Network or API error - retry with backoff
      console.log('Stripe API error. Retrying...');
      await new Promise(resolve => setTimeout(resolve, 2000));
      return verifyPaymentWithErrorHandling(paymentIntentId);
    }
    
    // General error
    console.error('Error verifying payment:', error);
    return { success: false, error: 'An unexpected error occurred while verifying the payment.' };
  }
}

 

Step 10: Implementing idempotency for reliability

 

When making API calls to Stripe, especially when capturing payments, use idempotency keys to prevent duplicate operations:

async function capturePaymentWithIdempotency(paymentIntentId, idempotencyKey) {
  try {
    const paymentIntent = await stripe.paymentIntents.capture(
      paymentIntentId,
      { idempotency\_key: idempotencyKey }
    );
    console.log('Payment captured successfully!');
    return paymentIntent;
  } catch (error) {
    console.error('Error capturing payment:', error);
    throw error;
  }
}

// Example usage:
const idempotencyKey = `capture_${paymentIntentId}_${Date.now()}`;
await capturePaymentWithIdempotency(paymentIntentId, idempotencyKey);

 

Conclusion

 

By following these steps, you should now have a robust system for verifying payment intent status in the Stripe API. This approach ensures that you can track payments throughout their lifecycle, handle various status changes appropriately, and reliably process both successful and failed payments.

Remember to always use test API keys during development and to thoroughly test your integration before going live. Stripe provides a test mode that allows you to simulate various payment scenarios without processing real transactions.

For more information, refer to the official Stripe documentation at https://stripe.com/docs/payments/payment-intents.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022