Verifying Payment Status

    Learn how to verify the status of a PaymentIntent and monitor it with webhooks.

    PaymentIntents update in response to actions taken by the customer or payment method. Your integration can inspect the PaymentIntent to determine the status of the payment process, responding to states that require further intervention.

    Checking PaymentIntent status on the client

    When completing a payment on the client with automatic confirmation and the handleCardPayment function, you can inspect the returned PaymentIntent to determine its current status:

    stripe.handleCardPayment(clientSecret).then(function(response) {
      if (response.error) {
        // Handle error here
      } else if (response.paymentIntent && response.paymentIntent.status === 'succeeded') {
        // Handle successful payment here
      }
    });
    
    (async () => {
      let {paymentIntent, error} = await stripe.handleCardPayment(clientSecret);
      if (error) {
        // Handle error here
      } else if (paymentIntent && paymentIntent.status === 'succeeded') {
        // Handle successful payment here
      }
    })();
    

    The following are the possible outcomes of using the handleCardPayment function:

    Event What Happened Expected Integration
    Resolves with a PaymentIntent Customer completed payment on your checkout page Inform customer that their payment was successful
    Resolves with an error Customer’s payment failed on your checkout page Display error message and prompt your customer to attempt payment again

    The promise returned by handleCardPayment resolves when the payment process has either completed or failed with an error. When it completes successfully and returns a PaymentIntent, the status is always either succeeded or requires_capture. When the payment requires an additional step like authentication, the promise doesn’t resolve until that step is either complete or has timed out.

    If you would like to check the status of a PaymentIntent without using the handleCardPayment function, you can retrieve it independently by using the retrievePaymentIntent function and passing in the client secret:

    stripe.retrievePaymentIntent(clientSecret).then(function(response) {
      if (response.error) {
        // Handle error here
      } else if (response.paymentIntent && response.paymentIntent.status === 'succeeded') {
        // Handle successful payment here
      }
    });
    
    (async () => {
      let {paymentIntent, error} = await stripe.retrievePaymentIntent(clientSecret);
      if (error) {
        // Handle error here
      } else if (paymentIntent && paymentIntent.status === 'succeeded') {
        // Handle successful payment here
      }
    })();
    

    Checking PaymentIntent status on the server

    When using manual confirmation and completing the payment on the server, you can inspect the PaymentIntent’s status property to determine if the payment completed successfully.

    If the payment requires additional actions such as 3D Secure authentication, the PaymentIntent’s status will be set to requires_action. If the payment is successful, then the status is set to succeeded and the payment is complete. If the payment failed, the status is set to requires_payment_method.

    Monitoring a PaymentIntent with webhooks

    Stripe can send webhook events to your server to notify you when the status of a PaymentIntent changes. This is useful for purposes like determining when to fulfill the goods and services purchased by the customer when using automatic confirmation.

    Your integration should not attempt to handle order fulfillment on the client side because it is possible for customers to leave the page after payment is complete but before the fulfillment process initiates. Instead, we strongly recommend using webhooks to monitor the payment_intent.succeeded event and handle its completion asynchronously instead of attempting to initiate fulfillment on the client side.

    To handle a webhook event, create a route on your server and configure a corresponding webhook endpoint in the Dashboard. Stripe sends the payment_intent.succeeded event when payment is successful and the payment_intent.payment_failed event when payment isn’t successful.

    The webhook payload includes the PaymentIntent object. The following example shows how to handle both events:

    require 'sinatra'
    require 'stripe'
    
    post '/webhook' do
        payload = request.body.read
        sig_header = request.env['HTTP_STRIPE_SIGNATURE']
        event = nil
    
        begin
            event = Stripe::Webhook.construct_event(
                payload, sig_header, endpoint_secret
            )
        rescue JSON::ParserError => e
            # Invalid payload
            status 400
            return
        rescue Stripe::SignatureVerificationError => e
            # Invalid signature
            status 400
            return
        end
    
        case event['type']
        when 'payment_intent.succeeded'
            intent = event['data']['object']
            puts "Succeeded:", intent['id']
            # Fulfill the customer's purchase
        when 'payment_intent.payment_failed'
            intent = event['data']['object']
            error_message = intent['last_payment_error'] && intent['last_payment_error']['message']
            puts "Failed:", intent['id'], error_message
            # Notify the customer that payment failed
        end
    
        status 200
    end
    
    # You can find your endpoint's secret in your webhook settings
    endpoint_secret = 'whsec_...'
    
    @app.route("/webhook", methods=['POST'])
    def webhook():
      payload = request.get_data()
      sig_header = request.headers.get('STRIPE_SIGNATURE')
      event = None
    
      try:
        event = stripe.Webhook.construct_event(
          payload, sig_header, endpoint_secret
        )
      except ValueError as e:
        # invalid payload
        return "Invalid payload", 400
      except stripe.error.SignatureVerificationError as e:
        # invalid signature
        return "Invalid signature", 400
    
      event_dict = event.to_dict()
      if event_dict['type'] == "payment_intent.succeeded":
        intent = event_dict['data']['object']
        print "Succeeded: ", intent['id']
        # Fulfill the customer's purchase
      elif event_dict['type'] == "payment_intent.payment_failed":
        intent = event_dict['data']['object']
        error_message = intent['last_payment_error']['message'] if intent.get('last_payment_error') else None
        print "Failed: ", intent['id'], error_message
        # Notify the customer that payment failed
    
      return "OK", 200
    
    // You can find your endpoint's secret in your webhook settings
    $endpoint_secret = 'whsec_...';
    
    $payload = @file_get_contents('php://input');
    $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
    $event = null;
    
    try {
        $event = \Stripe\Webhook::constructEvent(
            $payload, $sig_header, $endpoint_secret
        );
    } catch(\UnexpectedValueException $e) {
        // Invalid payload
        http_response_code(400); // PHP 5.4 or greater
        exit();
    } catch(\Stripe\Error\SignatureVerification $e) {
        // Invalid signature
        http_response_code(400); // PHP 5.4 or greater
        exit();
    }
    
    if ($event->type == "payment_intent.succeeded") {
        $intent = $event->data->object;
        printf("Succeeded: %s", $intent->id);
        http_response_code(200);
        exit();
    } elseif ($event->type == "payment_intent.payment_failed") {
        $intent = $event->data->object;
        $error_message = $intent->last_payment_error ? $intent->last_payment_error->message : "";
        printf("Failed: %s, %s", $intent->id, $error_message);
        http_response_code(200);
        exit();
    }
    
    import com.stripe.model.PaymentIntent;
    import com.stripe.model.Event;
    import com.stripe.net.Webhook;
    
    import static spark.Spark.post;
    
    public class StripeJavaQuickStart {
        public static void main(String[] args) {
          String endpointSecret = "whsec_...";
    
          post("/webhook", (request, response) -> {
            String payload = request.body();
            String sigHeader = request.headers("Stripe-Signature");
            Event event = null;
    
            try {
              event = Webhook.constructEvent(
                payload, sigHeader, endpointSecret
              );
            } catch (JsonSyntaxException e) {
              // Invalid payload
              response.status(400);
              return "Invalid payload";
            } catch (SignatureVerificationException e) {
              // Invalid signature
              response.status(400);
              return "Invalid signature";
            }
    
            PaymentIntent intent = null;
            switch(event.getType()) {
              case "payment_intent.succeeded":
                intent = (PaymentIntent) event.getData().getObject();
                System.out.println("Succeeded: " + intent.getId());
                break;
                // Fulfil the customer's purchase
    
              case "payment_intent.payment_failed":
                intent = (PaymentIntent) event.getData().getObject();
                System.out.println("Failed: " + intent.getId());
                break;
                // Notify the customer that payment failed
    
              default:
                // Handle other event types
                break;
            }
    
            response.status(200);
            return "OK";
          });
        }
    }
    
    app.use(require('body-parser').text({type: '*/*'}));
    
    const endpointSecret = 'whsec_...';
    
    app.post('/webhook', function(request, response) {
      const sig = request.headers['stripe-signature'];
      const body = request.body;
    
      let event = null;
    
      try {
        event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
      }
      catch (err) {
        // invalid signature
        response.status(400).end();
        return;
      }
    
      let intent = null;
      switch (event['type']) {
        case 'payment_intent.succeeded':
          intent = event.data.object;
          console.log("Succeeded:", intent.id);
          break;
        case 'payment_intent.payment_failed':
          intent = event.data.object;
          const message = intent.last_payment_error && intent.last_payment_error.message;
          console.log('Failed:', intent.id, message);
          break;
      }
    
      response.sendStatus(200);
    });
    
    package main
    
    import (
      "encoding/json"
      "net/http"
    
      stripe "github.com/stripe/stripe-go"
      webhook "github.com/stripe/stripe-go/webhook"
    )
    
    func main() {
      http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
          fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
          w.WriteHeader(http.StatusServiceUnavailable)
          return
        }
    
        // Pass the request body & Stripe-Signature header to ConstructEvent, along with the webhook signing key
        // You can find your endpoint's secret in your webhook settings
        endpointSecret := "whsec_...";
        event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), endpointSecret)
    
        if err != nil {
          fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err)
          w.WriteHeader(http.StatusBadRequest) // Return a 400 error on a bad signature
          return
        }
    
        if event.Type == "payment_intent.succeeded" {
          var paymentIntent stripe.PaymentIntent
          err := json.Unmarshal(event.Data.Raw, &paymentIntent)
          if err != nil {
            fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
            w.WriteHeader(http.StatusBadRequest)
            return
          }
          fmt.Sprintf("Succeeded: %v\n", paymentIntent)
          // Fulfil the customer's purchase
    
        } else if event.Type == "payment_intent.payment_failed" {
          var paymentIntent stripe.PaymentIntent
            err := json.Unmarshal(event.Data.Raw, &paymentIntent)
            if err != nil {
              fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
              w.WriteHeader(http.StatusBadRequest)
              return
            }
            fmt.Sprintf("Failed: %v, %v\n", paymentIntent, paymentIntent.LastPaymentError)
            // Notify the customer that payment failed
        }
        w.WriteHeader(http.StatusOK)
      })
    
      http.ListenAndServe(":3000", nil)
    }
    using System;
    using System.IO;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using Stripe;
    
    namespace StripeExampleApi.Controllers
    {
        [Route("webhook")]
        [ApiController]
        public class WebhookController : Controller
        {
            private readonly ILogger<WebhookController> _logger;
    
            public WebhookController(ILogger<WebhookController> logger)
            {
                _logger = logger;
            }
    
            const string secret = "whsec_...";
    
            [HttpPost]
            public ActionResult Post()
            {
                try {
                    var json = new StreamReader(HttpContext.Request.Body).ReadToEnd();
                    var stripeEvent = EventUtility.ConstructEvent(json,
                        Request.Headers["Stripe-Signature"], secret);
    
                    PaymentIntent intent = null;
    
                    switch (stripeEvent.Type)
                    {
                      case "payment_intent.succeeded":
                        intent = (PaymentIntent)stripeEvent.Data.Object;
                        _logger.LogInformation("Succeeded: {ID}", intent.Id);
    
                        // Fulfil the customer's purchase
    
                        break;
                      case "payment_intent.payment_failed":
                        intent = (PaymentIntent)stripeEvent.Data.Object;
                        _logger.LogInformation("Failure: {ID}", intent.Id);
    
                        // Notify the customer that payment failed
    
                        break;
                      default:
                        // Handle other event types
    
                        break;
                    }
                    return new EmptyResult();
    
                }
                catch (StripeException e) {
                    // Invalid Signature
                    return BadRequest();
                }
            }
        }
    }
    

    When payment is unsuccessful, you can find more details by inspecting the PaymentIntent’s last_payment_error property. You can notify the customer that their payment didn’t complete and encourage them to try again with a different payment method. Reuse the same PaymentIntent to continue tracking the customer’s purchase.

    Handling specific webhook events

    The following list describes how to handle webhook events:

    Event What Happened Expected Integration
    succeeded Customer’s payment succeeded Fulfill the goods or services purchased by the customer
    amount_capturable_updated Customer’s payment is authorized and ready for capture Capture the funds that are available for payment
    payment_failed Customer’s payment was declined by card network or otherwise expired Reach out to your customer via email or push notification and prompt them to provide another payment method

    Identifying charges on a PaymentIntent

    When a PaymentIntent attempts to collect payment from a customer, it creates a charge object. You can inspect a PaymentIntent’s charges property to obtain its complete list of attempted charges:

    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
    
    intent = Stripe::PaymentIntent.retrieve('pi_Aabcxyz01aDfoo')
    charges = intent.charges.data
    
    # Set your secret key: remember to change this to your live secret key in production
    # See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
    
    intent = stripe.PaymentIntent.retrieve('pi_Aabcxyz01aDfoo')
    charges = intent['charges']['data']
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc');
    
    $intent = \Stripe\PaymentIntent::retrieve('pi_Aabcxyz01aDfoo');
    $charges = $intent->charges->data;
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc";
    
    PaymentIntent intent = PaymentIntent.retrieve("pi_Aabcxyz01aDfoo");
    Map<Charge> charges = intent.charges.getData();
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');
    
    (async () => {
      const intent = await stripe.paymentIntents.retrieve('pi_Aabcxyz01aDfoo');
      const charges = intent.charges.data;
    })();
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"
    
    intent, err := paymentintent.Get("pi_Aabcxyz01aDfoo", nil)
    charges := intent.Charges.Data
    
    // Set your secret key: remember to change this to your live secret key in production
    // See your keys here: https://dashboard.stripe.com/account/apikeys
    StripeConfiguration.SetApiKey("sk_test_4eC39HqLyjWDarjtT1zdp7dc");
    
    var service = new PaymentIntentService();
    var intent = paymentIntents.Get("pi_Aabcxyz01aDfoo");
    var charges = intent.Charges.Data;
    

    The charges are listed in reverse chronological order, so the most recent charge is first in the array. Note that the array includes any unsuccessful charges created during the payment process in addition to the final successful charge.

    Handling next actions

    Some payment methods require additional steps, such as authentication, in order to complete the payment process. The Stripe.js handleCardPayment function handles these automatically, but some advanced integrations may wish to handle these manually.

    The PaymentIntent’s next_action property exposes the next step that your integration must handle in order to complete the payment. The type of possible next actions can differ between various payment methods.

    The following is a list of next action types and the payment methods that may require them:

    Type Payment methods What it means How to handle
    redirect_to_url Cards with 3DS The customer needs to be sent to the provided URL to authenticate the payment. top.location = intent.redirect_to_url.url

    You can refer to the documentation for individual payment methods for more details about how to handle their required next actions.

    Manually handling 3D Secure authentication with redirect

    A common action of interest is authenticating the customer’s payment with 3D Secure, as required by upcoming Strong Customer Authentication rules. There are several possible ways to handle this in an integration. The recommended way is to use the handleCardPayment or handleCardAction functions in Stripe.js, which assumes responsibility for ushering customers through that process and any other actions that may be required.

    To handle 3D Secure authentication manually, you can redirect the customer. This approach is used when you manually confirm the PaymentIntent and provide a return_url destination to indicate where the customer should be sent once authentication is complete. Manual PaymentIntent confirmation can be performed on the server or on the client with Stripe.js.

    After confirmation, if the PaymentIntent has a status of requires_action, inspect the PaymentIntent’s next_action, determine if it is redirect_to_url, and redirect the customer to complete authentication. When applicable, the PaymentIntent’s next_action is populated with an object that has the following shape:

    next_action: {
        type: 'redirect_to_url',
        redirect_to_url: {
          url: 'https://hooks.stripe.com/...',
          return_url: 'https://mysite.com'
        }
    }
    

    Use the following code in the browser to redirect the customer to the address provided by the next_action property:

    var action = intent.next_action;
    if (action && action.type === 'redirect_to_url') {
      window.location = action.redirect_to_url.url;
    }
    const action = intent.next_action;
    if (action && action.type === 'redirect_to_url') {
      window.location = action.redirect_to_url.url;
    }

    When the customer finishes the authentication process, they are sent back to the destination that you specified with the return_url property when you created the PaymentIntent. The redirect also adds payment_intent and payment_intent_client_secret URL query parameters that your application can use to identify the PaymentIntent associated with the customer’s purchase.

    Next steps

    Questions?

    We're always happy to help with code or other questions you might have. Search our documentation, contact support, or connect with our sales team. You can also chat live with other developers in #stripe on freenode.

    Was this page helpful? Yes No

    Send

    Thank you for helping improve Stripe's documentation. If you need help or have any questions, please consider contacting support.

    On this page