Migrate your basic card integration Payment Intents API

Migrate to an integration that can handle bank requests for card authentication

If you followed the Card payments without bank authentication guide, your integration creates payments that decline when a bank asks the customer to authenticate the purchase.

If you start seeing many failed payments like the one in the Dashboard below or with an error code of requires_action_not_handled in the API, you should upgrade your basic integration to handle, rather than decline, these payments.

A screenshot of the dashboard showing a failed payment that says that this bank required authentication for this payment

This guide will walk you through upgrading the integration you built in the previous guide to add server and client code that will prompt the customer to authenticate the payment by displaying a modal. You can see a full sample of the final integration on GitHub.

1 Check if the payment requires authentication Server-side

Make two changes to the endpoint on your server that creates the PaymentIntent:

  1. Remove the error_on_requires_action parameter to no longer fail payments that require authentication. Instead, the PaymentIntent status changes to requires_action.
  2. Add the confirmation_method parameter to indicate that you want to explicitly (manually) confirm the payment again on the server after handling authentication requests.
curl https://api.stripe.com/v1/payment_intents \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d amount=1099 \ -d currency=usd \ -d confirm=true \ -d error_on_requires_action=true \ -d payment_method="{{PAYMENT_METHOD_ID}}" \ -d confirmation_method=manual
intent = Stripe::PaymentIntent.create( amount: 1099, currency:'usd', confirm: true, payment_method: data['payment_method_id'], error_on_requires_action: true, confirmation_method: 'manual', )
intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], error_on_requires_action: True, confirmation_method = 'manual', )
$intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'error_on_requires_action' => true, 'confirmation_method' => 'manual', ]);
let intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, error_on_requires_action: true, confirmation_method: 'manual', });
PaymentIntent intent = PaymentIntent.create(PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setPaymentMethod(paymentMethod) .setConfirm(true) .setErrorOnRequiresAction(true) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .build());
params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(2000), Currency: stripe.String(string(stripe.CurrencyUSD)), PaymentMethod: stripe.String(req.PaymentMethodID), Confirm: stripe.Bool(true), ErrorOnRequiresAction: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), }
var options = new PaymentIntentCreateOptions { Amount = 1099, Currency = "usd", PaymentMethodId = request.PaymentMethodId, Confirm = true, ErrorOnRequiresAction = true, ConfirmationMethod = "manual", };

Then update your “generate response” function to handle the requires_action state instead of erroring:

# If the request succeeds, check the # PaymentIntent's `status` and handle # its `next_action`.
# Endpoint when `/pay` is called from client post '/pay' do data = JSON.parse(request.body.read.to_s) begin # Create the PaymentIntent intent = Stripe::PaymentIntent.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: data['payment_method_id'], confirmation_method: 'manual', }) generate_response(intent) rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end end def generate_response(intent) if intent.status == 'succeeded' # The payment didn’t need any additional actions and is completed! [200, { success: true }.to_json] elsif intent.status == 'requires_action' # Tell the client to handle the action [ 200, { requiresAction: true, clientSecret: intent.client_secret }.to_json ] else # Any other status would be unexpected, so error [500, { error: 'Invalid PaymentIntent status' }.to_json] end end
# Endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def confirm_payment(): data = request.get_json() try: # Create the PaymentIntent intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], confirmation_method = 'manual', ) return generate_response(intent) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200 def generate_response(intent): if intent.status == 'succeeded': # Handle post-payment fulfillment return json.dumps({'success': True}), 200 elif: intent.status == 'requires_action' # Tell the client to handle the action return json.dumps({ 'requiresAction': True, 'clientSecret': intent.client_secret, }), 200 else: # Any other status would be unexpected, so error return json.dumps({'error': 'Invalid PaymentIntent status'}), 500
<?php # vendor using composer require_once('vendor/autoload.php'); // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); header('Content-Type: application/json'); # retrieve JSON from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); try { // Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'confirmation_method' => 'manual', ]); generateResponse($intent); } catch (\Stripe\Exception\ApiErrorException $e) { // Display error on client echo json_encode(['error' => $e->getMessage()]); } function generateResponse($intent) { if ($intent->status == 'succeeded') { // Handle post-payment fulfillment echo json_encode(['success' => true]); } elseif ($intent->status == 'requires_action') { # Tell the client to handle the action echo json_encode([ 'requiresAction' => true, 'clientSecret' => $intent->client_secret ]); } else { // Any other status would be unexpected, so error echo json_encode(['error' => 'Invalid PaymentIntent status']); } }
app.post('/pay', async (request, response) => { try { // Create the PaymentIntent let intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, confirmation_method: 'manual', }); return generateResponse(response, intent); } catch (e) { if (e.type === 'StripeCardError') { // Display error on client return response.send({ error: e.message }); } else { // Something else happened return response.status(500).send({ error: e.type }); } } }); function generateResponse(response, intent) { if (intent.status === 'succeeded') { // Handle post-payment fulfillment return response.send({ success: true }); } else if (intent.status === 'requires_action') { // Tell the client to handle the action return response.send({ requiresAction: true, clientSecret: intent.client_secret }); } else { // Any other status would be unexpected, so error return response.status(500).send({error: 'Unexpected status ' + intent.status}); } }
post("/pay", (request, response) -> { Gson gson = new Gson(); String paymentMethod = gson.fromJson(request.body(), JsonObject.class) .get("payment_method_id").getAsString(); try { PaymentIntent intent = PaymentIntent.create(PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setPaymentMethod(paymentMethod) .setConfirm(true) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .build()); return gson.toJson(buildResponse(response, intent)); } catch (CardException e) { response.status(200); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("error", e.getStripeError().getMessage()); return gson.toJson(errorResponse); } }); } private static Map<String, Object> buildResponse(Response response, PaymentIntent intent) { Map<String, Object> responseData = new HashMap<>(); if (intent.getStatus().equals("succeeded")) { response.status(200); responseData.put("success", true); } else if (intent.getStatus().equals("requires_action")) { response.status(200); responseData.put("requiresAction", true); responseData.put("clientSecret", intent.getClientSecret()); } else { response.status(500); responseData.put("error", "Invalid PaymentIntent status"); } return responseData; }
func handlePay(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentMethodID string `json:"payment_method_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(2000), Currency: stripe.String(string(stripe.CurrencyUSD)), PaymentMethod: stripe.String(req.PaymentMethodID), Confirm: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), } pi, err := paymentintent.New(params) if err != nil { if stripeErr, ok := err.(*stripe.Error); ok { // return error message to the client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ ErrorMessage: stripeErr.Msg, }) } else { w.WriteHeader(http.StatusInternalServerError) } } else { generateResponse(pi, w) } } func generateResponse(intent *stripe.PaymentIntent, w http.ResponseWriter) { if intent.Status == stripe.PaymentIntentStatusSucceeded { // Handle post-payment fulfillment w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ Success: true, }) } else if intent.Status == stripe.PaymentIntentStatusRequiresAction { // Tell the client to handle the action w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ConfirmPaymentResponse{ RequiresAction: true, PaymentIntentClientSecret: &intent.ClientSecret, }) } else { // Any other status would be unexpected, so error w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ Success: false, ErrorMessage: "Invalid Payment Intent status", }) } }
[Route("/pay")] public class CreatePaymentIntentController : Controller { public IActionResult Index([FromBody] CreatePaymentIntentRequest request) { var service = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { // Create the PaymentIntent var options = new PaymentIntentCreateOptions { Amount = 1099, Currency = "usd", PaymentMethodId = request.PaymentMethodId, Confirm = true, ConfirmationMethod = "manual", }; paymentIntent = service.Create(options); } catch (StripeException e) { return Json(new { error = e.StripeError.Message }); } return generatePaymentResponse(paymentIntent); } private IActionResult generatePaymentResponse(PaymentIntent intent) { if (intent.Status == "succeeded") { // Handle post-payment fulfillment return Json(new { success = true }); } else if (intent.Status == "requires_action") { // Tell the client to handle the action return Json(new { requiresAction = true, clientSecret = intent.ClientSecret }); } else { // Any other status would be unexpected, so error return StatusCode(500, new { error = "Invalid PaymentIntent status" }); } } }

2 Ask the customer to authenticate Client-side

Next, update your client-side code to tell Stripe to show a modal if the customer needs to authenticate.

Use stripe.handleCardAction when a PaymentIntent has a status of requires_action. If successful, the PaymentIntent will have a status of requires_confirmation and you need to confirm the PaymentIntent again on your server to finish the payment.

function handleServerResponse(responseJson) { if (responseJson.error) { // Show error from server on payment form } else if (responseJson.requiresAction) { // Use Stripe.js to handle required card action stripe.handleCardAction( responseJson.clientSecret ).then(function(result) { if (result.error) { // Show `result.error.message` in payment form } else { // The card action has been handled // The PaymentIntent can be confirmed again on the server fetch('/pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ payment_intent_id: result.paymentIntent.id }) }).then(function(confirmResult) { return confirmResult.json(); }).then(handleServerResponse); } }); } else { // Show success message } }
const handleServerResponse = async (responseJson) => { if (responseJson.error) { // Show error from server on payment form } else if (responseJson.requiresAction) { // Use Stripe.js to handle the required card action const { error: errorAction, paymentIntent } = await stripe.handleCardAction(responseJson.clientSecret); if (errorAction) { // Show error from Stripe.js in payment form } else { // The card action has been handled // The PaymentIntent can be confirmed again on the server const serverResponse = await fetch('/pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ payment_intent_id: paymentIntent.id }) }); handleServerResponse(await serverResponse.json()); } } else { // Show success message } }

3 Confirm the PaymentIntent again Server-side

Using the same endpoint you set up earlier, confirm the PaymentIntent again to finalize the payment and fulfill the order. The payment attempt fails and transitions back to requires_payment_method if it is not confirmed again within one hour.

curl https://api.stripe.com/v1/payment_intents/{{PAYMENT_INTENT_ID}}/confirm \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -X POST
# AJAX endpoint when `/pay` is called from client post '/pay' do data = JSON.parse(request.body.read.to_s) begin if data['payment_method_id'] # Create the PaymentIntent intent = Stripe::PaymentIntent.create( amount: 1099, currency: 'usd', confirm: true, payment_method: data['payment_method_id'], confirmation_method: 'manual', use_stripe_sdk: true, ) elsif data['payment_intent_id'] intent = Stripe::PaymentIntent.confirm(data['payment_intent_id']) end return generate_response(intent) rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end end
# AJAX endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def confirm_payment(): data = request.get_json() try: intent = None if 'payment_method_id' in data: # Create the PaymentIntent intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], confirmation_method = 'manual', use_stripe_sdk = True, ) elif 'payment_intent_id' in data: intent = stripe.PaymentIntent.confirm(data['payment_intent_id']) return generate_response(intent) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200
<?php # vendor using composer require_once('vendor/autoload.php'); // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); header('Content-Type: application/json'); # retrieve json from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); try { if (isset($json_obj->payment_method_id)) { # Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'confirmation_method' => 'manual', 'use_stripe_sdk' => true, ]); } if (isset($json_obj->payment_intent_id)) { $intent = \Stripe\PaymentIntent::retrieve( $json_obj->payment_intent_id ); $intent->confirm(); } generateResponse($intent); } catch (\Stripe\Exception\ApiErrorException $e) { # Display error on client echo json_encode([ 'error' => $e->getMessage() ]); }
// Using Express const express = require('express'); const app = express(); app.use(express.json()); app.post('/pay', async (request, response) => { try { let intent; if (request.body.payment_method_id) { // Create the PaymentIntent intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, confirmation_method: 'manual', use_stripe_sdk: true }); } else if (request.body.payment_intent_id) { intent = await stripe.paymentIntents.confirm( request.body.payment_intent_id ); } // Send the response to the client return generateResponse(response, intent); } catch (e) { if (e.type === 'StripeCardError') { // Display error on client return response.send({ error: e.message }); } else { // Something else happened return response.status(500).send({ error: e.type }); } } });
post("/pay", (request, response) -> { Gson gson = new Gson(); String paymentMethod = gson.fromJson(request.body(), JsonObject.class) .get("payment_method_id").getAsString(); try { if (confirmRequest.getPaymentMethodId() != null) { PaymentIntentCreateParams createParams = PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setConfirm(true) .setPaymentMethod(confirmRequest.paymentMethodId) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .setUseStripeSdk(true) .build(); intent = PaymentIntent.create(createParams); } else if (confirmRequest.getPaymentIntentId() != null) { intent = PaymentIntent.retrieve(confirmRequest.getPaymentIntentId()); intent = intent.confirm(); } return gson.toJson(buildResponse(response, intent)); } catch (CardException e) { response.status(200); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("error", e.getStripeError().getMessage()); return gson.toJson(errorResponse); } });
func handlePay(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentMethodID string `json:"payment_method_id"` } var intent *stripe.PaymentIntent var err error if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } if confirmPaymentRequest.PaymentMethodId != nil { // Create the PaymentIntent params := &stripe.PaymentIntentParams{ PaymentMethod: stripe.String(*confirmPaymentRequest.PaymentMethodId), Amount: stripe.Int64(1099), Currency: stripe.String(string(stripe.CurrencyUSD)), Confirm: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), UseStripeSdk: stripe.Bool(true), } intent, err = paymentintent.New(params) } if confirmPaymentRequest.PaymentIntentId != nil { intent, err = paymentintent.Confirm( *confirmPaymentRequest.PaymentIntentId, nil, ) } if err != nil { if stripeErr, ok := err.(*stripe.Error); ok { // return error message to the client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ ErrorMessage: stripeErr.Msg, }) } else { w.WriteHeader(http.StatusInternalServerError) } } else { generateResponse(pi, w) } }
public IActionResult Index([FromBody] CreatePaymentIntentRequest request) { var service = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { if (request.PaymentMethodId != null) { // Create the PaymentIntent var createOptions = new PaymentIntentCreateOptions { PaymentMethod = request.PaymentMethodId, Amount = 1099, Currency = "usd", Confirm = true, ConfirmationMethod = "manual", UseStripeSdk = true, }; paymentIntent = paymentIntentService.Create(createOptions); } if (request.PaymentIntentId != null) { var confirmOptions = new PaymentIntentConfirmOptions{}; paymentIntent = paymentIntentService.Confirm( request.PaymentIntentId, confirmOptions ); } } catch (StripeException e) { return Json(new { error = e.StripeError.Message }); } return generatePaymentResponse(paymentIntent); }

4 Test the integration

Use our test cards in test mode to verify that your integration was properly updated. Stripe displays a fake authentication page inside the modal in test mode that lets you simulate a successful or failed authentication attempt. In live mode the bank controls the UI of what is displayed inside the modal.

Number Description
4242424242424242 Succeeds and immediately processes the payment.
4000000000009995 Always fails with a decline code of insufficient_funds.
4000002500003155 Requires authentication which, in this integration, will pop up a modal for the user to authenticate.

If you followed the Card payments without bank authentication guide, your integration creates payments that decline when a bank asks the customer to authenticate the purchase.

If you start seeing many failed payments like the one in the Dashboard below or with an error code of requires_action_not_handled in the API, you should upgrade your basic integration to handle, rather than decline, these payments.

A screenshot of the dashboard showing a failed payment that says that this bank required authentication for this payment

This guide will walk you through upgrading the integration you built in the previous guide to add server and client code that will prompt the customer to authenticate the payment by displaying a modal. You can see a full sample of the final integration on GitHub.

1 Check if the payment requires authentication Server-side

Make three changes to the endpoint on your server that creates the PaymentIntent:

  1. Remove the error_on_requires_action parameter to no longer fail payments that require authentication. Instead, the PaymentIntent status changes to requires_action.
  2. Add the confirmation_method parameter to indicate that you want to explicitly (manually) confirm the payment again on the server after handling authentication requests.
  3. Add the use_stripe_sdk parameter to indicate that your mobile client will handle additional authentication steps.
curl https://api.stripe.com/v1/payment_intents \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d amount=1099 \ -d currency=usd \ -d confirm=true \ -d error_on_requires_action=true \ -d payment_method="{{PAYMENT_METHOD_ID}}" \ -d confirmation_method=manual \ -d use_stripe_sdk=true
intent = Stripe::PaymentIntent.create( amount: 1099, currency:'usd', confirm: true, payment_method: data['payment_method_id'], error_on_requires_action: true, confirmation_method: 'manual', use_stripe_sdk: true, )
intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], error_on_requires_action: True, confirmation_method = 'manual', use_stripe_sdk = True, )
$intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'error_on_requires_action' => true, 'confirmation_method' => 'manual', 'use_stripe_sdk' => true, ]);
let intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, error_on_requires_action: true, confirmation_method: 'manual', use_stripe_sdk: true, });
PaymentIntent intent = PaymentIntent.create(PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setPaymentMethod(paymentMethod) .setConfirm(true) .setErrorOnRequiresAction(true) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .setUseStripeSdk(true) .build());
params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(2000), Currency: stripe.String(string(stripe.CurrencyUSD)), PaymentMethod: stripe.String(req.PaymentMethodID), Confirm: stripe.Bool(true), ErrorOnRequiresAction: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), UseStripeSdk: stripe.Bool(true), }
var options = new PaymentIntentCreateOptions { Amount = 1099, Currency = "usd", PaymentMethodId = request.PaymentMethodId, Confirm = true, ErrorOnRequiresAction = true, ConfirmationMethod = "manual", UseStripeSdk = true, };

Then update your “generate response” function to handle the requires_action state instead of erroring:

# If the request succeeds, check the # PaymentIntent's `status` and handle # its `next_action`.
# Endpoint when `/pay` is called from client post '/pay' do data = JSON.parse(request.body.read.to_s) begin # Create the PaymentIntent intent = Stripe::PaymentIntent.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: data['payment_method_id'], confirmation_method: 'manual', use_stripe_sdk: true, }) generate_response(intent) rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end end def generate_response(intent) if intent.status == 'succeeded' # The payment didn’t need any additional actions and is completed! [200, { success: true }.to_json] elsif intent.status == 'requires_action' # Tell the client to handle the action [ 200, { requiresAction: true, clientSecret: intent.client_secret }.to_json ] else # Any other status would be unexpected, so error [500, { error: 'Invalid PaymentIntent status' }.to_json] end end
# Endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def confirm_payment(): data = request.get_json() try: # Create the PaymentIntent intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], confirmation_method = 'manual', use_stripe_sdk = True, ) return generate_response(intent) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200 def generate_response(intent): if intent.status == 'succeeded': # Handle post-payment fulfillment return json.dumps({'success': True}), 200 elif: intent.status == 'requires_action' # Tell the client to handle the action return json.dumps({ 'requiresAction': True, 'clientSecret': intent.client_secret, }), 200 else: # Any other status would be unexpected, so error return json.dumps({'error': 'Invalid PaymentIntent status'}), 500
<?php # vendor using composer require_once('vendor/autoload.php'); // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); header('Content-Type: application/json'); # retrieve JSON from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); try { // Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'confirmation_method' => 'manual', 'use_stripe_sdk' => true, ]); generateResponse($intent); } catch (\Stripe\Exception\ApiErrorException $e) { // Display error on client echo json_encode(['error' => $e->getMessage()]); } function generateResponse($intent) { if ($intent->status == 'succeeded') { // Handle post-payment fulfillment echo json_encode(['success' => true]); } elseif ($intent->status == 'requires_action') { # Tell the client to handle the action echo json_encode([ 'requiresAction' => true, 'clientSecret' => $intent->client_secret ]); } else { // Any other status would be unexpected, so error echo json_encode(['error' => 'Invalid PaymentIntent status']); } }
app.post('/pay', async (request, response) => { try { // Create the PaymentIntent let intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, confirmation_method: 'manual', use_stripe_sdk: true }); return generateResponse(response, intent); } catch (e) { if (e.type === 'StripeCardError') { // Display error on client return response.send({ error: e.message }); } else { // Something else happened return response.status(500).send({ error: e.type }); } } }); function generateResponse(response, intent) { if (intent.status === 'succeeded') { // Handle post-payment fulfillment return response.send({ success: true }); } else if (intent.status === 'requires_action') { // Tell the client to handle the action return response.send({ requiresAction: true, clientSecret: intent.client_secret }); } else { // Any other status would be unexpected, so error return response.status(500).send({error: 'Unexpected status ' + intent.status}); } }
post("/pay", (request, response) -> { Gson gson = new Gson(); String paymentMethod = gson.fromJson(request.body(), JsonObject.class) .get("payment_method_id").getAsString(); try { PaymentIntent intent = PaymentIntent.create(PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setPaymentMethod(paymentMethod) .setConfirm(true) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .setUseStripeSdk(true) .build()); return gson.toJson(buildResponse(response, intent)); } catch (CardException e) { response.status(200); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("error", e.getStripeError().getMessage()); return gson.toJson(errorResponse); } }); } private static Map<String, Object> buildResponse(Response response, PaymentIntent intent) { Map<String, Object> responseData = new HashMap<>(); if (intent.getStatus().equals("succeeded")) { response.status(200); responseData.put("success", true); } else if (intent.getStatus().equals("requires_action")) { response.status(200); responseData.put("requiresAction", true); responseData.put("clientSecret", intent.getClientSecret()); } else { response.status(500); responseData.put("error", "Invalid PaymentIntent status"); } return responseData; }
func handlePay(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentMethodID string `json:"payment_method_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(2000), Currency: stripe.String(string(stripe.CurrencyUSD)), PaymentMethod: stripe.String(req.PaymentMethodID), Confirm: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), UseStripeSdk: stripe.Bool(true), } pi, err := paymentintent.New(params) if err != nil { if stripeErr, ok := err.(*stripe.Error); ok { // return error message to the client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ ErrorMessage: stripeErr.Msg, }) } else { w.WriteHeader(http.StatusInternalServerError) } } else { generateResponse(pi, w) } } func generateResponse(intent *stripe.PaymentIntent, w http.ResponseWriter) { if intent.Status == stripe.PaymentIntentStatusSucceeded { // Handle post-payment fulfillment w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ Success: true, }) } else if intent.Status == stripe.PaymentIntentStatusRequiresAction { // Tell the client to handle the action w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ConfirmPaymentResponse{ RequiresAction: true, PaymentIntentClientSecret: &intent.ClientSecret, }) } else { // Any other status would be unexpected, so error w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ Success: false, ErrorMessage: "Invalid Payment Intent status", }) } }
[Route("/pay")] public class CreatePaymentIntentController : Controller { public IActionResult Index([FromBody] CreatePaymentIntentRequest request) { var service = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { // Create the PaymentIntent var options = new PaymentIntentCreateOptions { Amount = 1099, Currency = "usd", PaymentMethodId = request.PaymentMethodId, Confirm = true, ConfirmationMethod = "manual", UseStripeSdk = true, }; paymentIntent = service.Create(options); } catch (StripeException e) { return Json(new { error = e.StripeError.Message }); } return generatePaymentResponse(paymentIntent); } private IActionResult generatePaymentResponse(PaymentIntent intent) { if (intent.Status == "succeeded") { // Handle post-payment fulfillment return Json(new { success = true }); } else if (intent.Status == "requires_action") { // Tell the client to handle the action return Json(new { requiresAction = true, clientSecret = intent.ClientSecret }); } else { // Any other status would be unexpected, so error return StatusCode(500, new { error = "Invalid PaymentIntent status" }); } } }

2 Ask the customer to authenticate Client-side

Next, update your iOS code to tell Stripe to show a modal if the customer needs to authenticate.

Use handleNextActionForPayment when a PaymentIntent has a status of requires_action. If successful, the PaymentIntent will have a status of requires_confirmation and you need to confirm the PaymentIntent again on your server to finish the payment.

- (void)handleResponse:(NSDictionary *)json { if (json[@"error"] != nil) { // Display the error to the customer } else if (json[@"requiresAction"]) { // Payment requires additional actions NSString *clientSecret = json[@"clientSecret"]; STPPaymentHandler *paymentHandler = [STPPaymentHandler sharedHandler]; [paymentHandler handleNextActionForPayment:clientSecret withAuthenticationContext:self returnURL:nil completion:^(STPPaymentHandlerActionStatus status, STPPaymentIntent *paymentIntent, NSError *handleActionError) { switch (status) { case STPPaymentHandlerActionStatusFailed: { // Display handleActionError to the customer break; } case STPPaymentHandlerActionStatusCanceled: { // Canceled break; } case STPPaymentHandlerActionStatusSucceeded: { // The card action has been handled // Send the PaymentIntent ID to your server and confirm it again break; } default: break; } }]; } else { // Display success message } } # pragma mark STPAuthenticationContext - (UIViewController *)authenticationPresentingViewController { return self; }
func handleResponse(json: Dictionary<String, Any>) { // Payment failed if let payError = json["error"] as? String { // Display the error to the customer } else if json["requiresAction"] != nil { let paymentHandler = STPPaymentHandler.shared() let clientSecret = json["clientSecret"] as! String paymentHandler.handleNextAction(forPayment: clientSecret, authenticationContext: self, returnURL: nil) { status, paymentIntent, handleActionError in switch (status) { case .failed: // Display handleActionError to the customer break case .canceled: // Canceled break case .succeeded: // The card action has been handled // Send the PaymentIntent ID to your server and confirm it again break @unknown default: fatalError() break } } } else { // Display success message } } func authenticationPresentingViewController() -> UIViewController { return self }

3 Confirm the PaymentIntent again Server-side

Using the same endpoint you set up earlier, confirm the PaymentIntent again to finalize the payment and fulfill the order. The payment attempt fails and transitions back to requires_payment_method if it is not confirmed again within one hour.

curl https://api.stripe.com/v1/payment_intents/{{PAYMENT_INTENT_ID}}/confirm \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -X POST
# AJAX endpoint when `/pay` is called from client post '/pay' do data = JSON.parse(request.body.read.to_s) begin if data['payment_method_id'] # Create the PaymentIntent intent = Stripe::PaymentIntent.create( amount: 1099, currency: 'usd', confirm: true, payment_method: data['payment_method_id'], confirmation_method: 'manual', use_stripe_sdk: true, ) elsif data['payment_intent_id'] intent = Stripe::PaymentIntent.confirm(data['payment_intent_id']) end return generate_response(intent) rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end end
# AJAX endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def confirm_payment(): data = request.get_json() try: intent = None if 'payment_method_id' in data: # Create the PaymentIntent intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], confirmation_method = 'manual', use_stripe_sdk = True, ) elif 'payment_intent_id' in data: intent = stripe.PaymentIntent.confirm(data['payment_intent_id']) return generate_response(intent) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200
<?php # vendor using composer require_once('vendor/autoload.php'); // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); header('Content-Type: application/json'); # retrieve json from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); try { if (isset($json_obj->payment_method_id)) { # Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'confirmation_method' => 'manual', 'use_stripe_sdk' => true, ]); } if (isset($json_obj->payment_intent_id)) { $intent = \Stripe\PaymentIntent::retrieve( $json_obj->payment_intent_id ); $intent->confirm(); } generateResponse($intent); } catch (\Stripe\Exception\ApiErrorException $e) { # Display error on client echo json_encode([ 'error' => $e->getMessage() ]); }
// Using Express const express = require('express'); const app = express(); app.use(express.json()); app.post('/pay', async (request, response) => { try { let intent; if (request.body.payment_method_id) { // Create the PaymentIntent intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, confirmation_method: 'manual', use_stripe_sdk: true }); } else if (request.body.payment_intent_id) { intent = await stripe.paymentIntents.confirm( request.body.payment_intent_id ); } // Send the response to the client return generateResponse(response, intent); } catch (e) { if (e.type === 'StripeCardError') { // Display error on client return response.send({ error: e.message }); } else { // Something else happened return response.status(500).send({ error: e.type }); } } });
post("/pay", (request, response) -> { Gson gson = new Gson(); String paymentMethod = gson.fromJson(request.body(), JsonObject.class) .get("payment_method_id").getAsString(); try { if (confirmRequest.getPaymentMethodId() != null) { PaymentIntentCreateParams createParams = PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setConfirm(true) .setPaymentMethod(confirmRequest.paymentMethodId) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .setUseStripeSdk(true) .build(); intent = PaymentIntent.create(createParams); } else if (confirmRequest.getPaymentIntentId() != null) { intent = PaymentIntent.retrieve(confirmRequest.getPaymentIntentId()); intent = intent.confirm(); } return gson.toJson(buildResponse(response, intent)); } catch (CardException e) { response.status(200); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("error", e.getStripeError().getMessage()); return gson.toJson(errorResponse); } });
func handlePay(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentMethodID string `json:"payment_method_id"` } var intent *stripe.PaymentIntent var err error if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } if confirmPaymentRequest.PaymentMethodId != nil { // Create the PaymentIntent params := &stripe.PaymentIntentParams{ PaymentMethod: stripe.String(*confirmPaymentRequest.PaymentMethodId), Amount: stripe.Int64(1099), Currency: stripe.String(string(stripe.CurrencyUSD)), Confirm: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), UseStripeSdk: stripe.Bool(true), } intent, err = paymentintent.New(params) } if confirmPaymentRequest.PaymentIntentId != nil { intent, err = paymentintent.Confirm( *confirmPaymentRequest.PaymentIntentId, nil, ) } if err != nil { if stripeErr, ok := err.(*stripe.Error); ok { // return error message to the client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ ErrorMessage: stripeErr.Msg, }) } else { w.WriteHeader(http.StatusInternalServerError) } } else { generateResponse(pi, w) } }
public IActionResult Index([FromBody] CreatePaymentIntentRequest request) { var service = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { if (request.PaymentMethodId != null) { // Create the PaymentIntent var createOptions = new PaymentIntentCreateOptions { PaymentMethod = request.PaymentMethodId, Amount = 1099, Currency = "usd", Confirm = true, ConfirmationMethod = "manual", UseStripeSdk = true, }; paymentIntent = paymentIntentService.Create(createOptions); } if (request.PaymentIntentId != null) { var confirmOptions = new PaymentIntentConfirmOptions{}; paymentIntent = paymentIntentService.Confirm( request.PaymentIntentId, confirmOptions ); } } catch (StripeException e) { return Json(new { error = e.StripeError.Message }); } return generatePaymentResponse(paymentIntent); }

4 Test the integration

Use our test cards in test mode to verify that your integration was properly updated. Stripe displays a fake authentication page inside the modal in test mode that lets you simulate a successful or failed authentication attempt. In live mode the bank controls the UI of what is displayed inside the modal.

Number Description
4242424242424242 Succeeds and immediately processes the payment.
4000000000009995 Always fails with a decline code of insufficient_funds.
4000002500003155 Requires authentication which, in this integration, will pop up a modal for the user to authenticate.

If you followed the Card payments without bank authentication guide, your integration creates payments that decline when a bank asks the customer to authenticate the purchase.

If you start seeing many failed payments like the one in the Dashboard below or with an error code of requires_action_not_handled in the API, you should upgrade your basic integration to handle, rather than decline, these payments.

A screenshot of the dashboard showing a failed payment that says that this bank required authentication for this payment

This guide will walk you through upgrading the integration you built in the previous guide to add server and client code that will prompt the customer to authenticate the payment by displaying a modal. You can see a full sample of the final integration on GitHub.

1 Check if the payment requires authentication Server-side

Make three changes to the endpoint on your server that creates the PaymentIntent:

  1. Remove the error_on_requires_action parameter to no longer fail payments that require authentication. Instead, the PaymentIntent status changes to requires_action.
  2. Add the confirmation_method parameter to indicate that you want to explicitly (manually) confirm the payment again on the server after handling authentication requests.
  3. Add the use_stripe_sdk parameter to indicate that your mobile client will handle additional authentication steps.
curl https://api.stripe.com/v1/payment_intents \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d amount=1099 \ -d currency=usd \ -d confirm=true \ -d error_on_requires_action=true \ -d payment_method="{{PAYMENT_METHOD_ID}}" \ -d confirmation_method=manual \ -d use_stripe_sdk=true
intent = Stripe::PaymentIntent.create( amount: 1099, currency:'usd', confirm: true, payment_method: data['payment_method_id'], error_on_requires_action: true, confirmation_method: 'manual', use_stripe_sdk: true, )
intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], error_on_requires_action: True, confirmation_method = 'manual', use_stripe_sdk = True, )
$intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'error_on_requires_action' => true, 'confirmation_method' => 'manual', 'use_stripe_sdk' => true, ]);
let intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, error_on_requires_action: true, confirmation_method: 'manual', use_stripe_sdk: true, });
PaymentIntent intent = PaymentIntent.create(PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setPaymentMethod(paymentMethod) .setConfirm(true) .setErrorOnRequiresAction(true) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .setUseStripeSdk(true) .build());
params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(2000), Currency: stripe.String(string(stripe.CurrencyUSD)), PaymentMethod: stripe.String(req.PaymentMethodID), Confirm: stripe.Bool(true), ErrorOnRequiresAction: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), UseStripeSdk: stripe.Bool(true), }
var options = new PaymentIntentCreateOptions { Amount = 1099, Currency = "usd", PaymentMethodId = request.PaymentMethodId, Confirm = true, ErrorOnRequiresAction = true, ConfirmationMethod = "manual", UseStripeSdk = true, };

Then update your “generate response” function to handle the requires_action state instead of erroring:

# If the request succeeds, check the # PaymentIntent's `status` and handle # its `next_action`.
# Endpoint when `/pay` is called from client post '/pay' do data = JSON.parse(request.body.read.to_s) begin # Create the PaymentIntent intent = Stripe::PaymentIntent.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: data['payment_method_id'], confirmation_method: 'manual', use_stripe_sdk: true, }) generate_response(intent) rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end end def generate_response(intent) if intent.status == 'succeeded' # The payment didn’t need any additional actions and is completed! [200, { success: true }.to_json] elsif intent.status == 'requires_action' # Tell the client to handle the action [ 200, { requiresAction: true, clientSecret: intent.client_secret }.to_json ] else # Any other status would be unexpected, so error [500, { error: 'Invalid PaymentIntent status' }.to_json] end end
# Endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def confirm_payment(): data = request.get_json() try: # Create the PaymentIntent intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], confirmation_method = 'manual', use_stripe_sdk = True, ) return generate_response(intent) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200 def generate_response(intent): if intent.status == 'succeeded': # Handle post-payment fulfillment return json.dumps({'success': True}), 200 elif: intent.status == 'requires_action' # Tell the client to handle the action return json.dumps({ 'requiresAction': True, 'clientSecret': intent.client_secret, }), 200 else: # Any other status would be unexpected, so error return json.dumps({'error': 'Invalid PaymentIntent status'}), 500
<?php # vendor using composer require_once('vendor/autoload.php'); // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); header('Content-Type: application/json'); # retrieve JSON from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); try { // Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'confirmation_method' => 'manual', 'use_stripe_sdk' => true, ]); generateResponse($intent); } catch (\Stripe\Exception\ApiErrorException $e) { // Display error on client echo json_encode(['error' => $e->getMessage()]); } function generateResponse($intent) { if ($intent->status == 'succeeded') { // Handle post-payment fulfillment echo json_encode(['success' => true]); } elseif ($intent->status == 'requires_action') { # Tell the client to handle the action echo json_encode([ 'requiresAction' => true, 'clientSecret' => $intent->client_secret ]); } else { // Any other status would be unexpected, so error echo json_encode(['error' => 'Invalid PaymentIntent status']); } }
app.post('/pay', async (request, response) => { try { // Create the PaymentIntent let intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, confirmation_method: 'manual', use_stripe_sdk: true }); return generateResponse(response, intent); } catch (e) { if (e.type === 'StripeCardError') { // Display error on client return response.send({ error: e.message }); } else { // Something else happened return response.status(500).send({ error: e.type }); } } }); function generateResponse(response, intent) { if (intent.status === 'succeeded') { // Handle post-payment fulfillment return response.send({ success: true }); } else if (intent.status === 'requires_action') { // Tell the client to handle the action return response.send({ requiresAction: true, clientSecret: intent.client_secret }); } else { // Any other status would be unexpected, so error return response.status(500).send({error: 'Unexpected status ' + intent.status}); } }
post("/pay", (request, response) -> { Gson gson = new Gson(); String paymentMethod = gson.fromJson(request.body(), JsonObject.class) .get("payment_method_id").getAsString(); try { PaymentIntent intent = PaymentIntent.create(PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setPaymentMethod(paymentMethod) .setConfirm(true) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .setUseStripeSdk(true) .build()); return gson.toJson(buildResponse(response, intent)); } catch (CardException e) { response.status(200); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("error", e.getStripeError().getMessage()); return gson.toJson(errorResponse); } }); } private static Map<String, Object> buildResponse(Response response, PaymentIntent intent) { Map<String, Object> responseData = new HashMap<>(); if (intent.getStatus().equals("succeeded")) { response.status(200); responseData.put("success", true); } else if (intent.getStatus().equals("requires_action")) { response.status(200); responseData.put("requiresAction", true); responseData.put("clientSecret", intent.getClientSecret()); } else { response.status(500); responseData.put("error", "Invalid PaymentIntent status"); } return responseData; }
func handlePay(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentMethodID string `json:"payment_method_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } params := &stripe.PaymentIntentParams{ Amount: stripe.Int64(2000), Currency: stripe.String(string(stripe.CurrencyUSD)), PaymentMethod: stripe.String(req.PaymentMethodID), Confirm: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), UseStripeSdk: stripe.Bool(true), } pi, err := paymentintent.New(params) if err != nil { if stripeErr, ok := err.(*stripe.Error); ok { // return error message to the client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ ErrorMessage: stripeErr.Msg, }) } else { w.WriteHeader(http.StatusInternalServerError) } } else { generateResponse(pi, w) } } func generateResponse(intent *stripe.PaymentIntent, w http.ResponseWriter) { if intent.Status == stripe.PaymentIntentStatusSucceeded { // Handle post-payment fulfillment w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ Success: true, }) } else if intent.Status == stripe.PaymentIntentStatusRequiresAction { // Tell the client to handle the action w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ConfirmPaymentResponse{ RequiresAction: true, PaymentIntentClientSecret: &intent.ClientSecret, }) } else { // Any other status would be unexpected, so error w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ Success: false, ErrorMessage: "Invalid Payment Intent status", }) } }
[Route("/pay")] public class CreatePaymentIntentController : Controller { public IActionResult Index([FromBody] CreatePaymentIntentRequest request) { var service = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { // Create the PaymentIntent var options = new PaymentIntentCreateOptions { Amount = 1099, Currency = "usd", PaymentMethodId = request.PaymentMethodId, Confirm = true, ConfirmationMethod = "manual", UseStripeSdk = true, }; paymentIntent = service.Create(options); } catch (StripeException e) { return Json(new { error = e.StripeError.Message }); } return generatePaymentResponse(paymentIntent); } private IActionResult generatePaymentResponse(PaymentIntent intent) { if (intent.Status == "succeeded") { // Handle post-payment fulfillment return Json(new { success = true }); } else if (intent.Status == "requires_action") { // Tell the client to handle the action return Json(new { requiresAction = true, clientSecret = intent.ClientSecret }); } else { // Any other status would be unexpected, so error return StatusCode(500, new { error = "Invalid PaymentIntent status" }); } } }

2 Ask the customer to authenticate Client-side

Next, update your Android code to tell Stripe to show a modal if the customer needs to authenticate.

Use handleNextActionForPayment when a PaymentIntent has a status of requires_action. If successful, the PaymentIntent will have a status of requires_confirmation and you need to confirm the PaymentIntent again on your server to finish the payment.

public class CheckoutActivity extends Activity { private Stripe stripe; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); stripe = new Stripe(this, "pk_test_TYooMQauvdEDq54NiTphI7jx"); } private void handleResponse(@NonNull JSONObject json) throws JSONException { if (json.has("error")) { // Display the error to the customer final String error = json.getString("error"); } else if (json.has("requiresAction")) { final String clientSecret = json.getString("clientSecret"); stripe.handleNextActionForPayment(this, clientSecret); } else { // Display success message } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); stripe.onPaymentResult(requestCode, data, new ApiResultCallback<PaymentIntentResult>() { @Override public void onSuccess(@NonNull PaymentIntentResult result) { switch (result.getOutcome()) { case StripeIntentResult.Outcome.SUCCEEDED: { // The card action has been handled // Send the Payment Intent ID to your server and confirm it again } case StripeIntentResult.Outcome.CANCELED: { // Canceled } case StripeIntentResult.Outcome.TIMEDOUT: { // Timed-out } case StripeIntentResult.Outcome.FAILED: { // Failed } } } @Override public void onError(@NonNull Exception e) { // Error encountered } } ); } }
class CheckoutActivity : Activity() { private lateinit var stripe: Stripe override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Stripe(this, "pk_test_TYooMQauvdEDq54NiTphI7jx") } @Throws(JSONException::class) private fun handleResponse(json: JSONObject) { when { json.has("error") -> { // Display the error to the customer val error = json.getString("error") } json.has("requiresAction") -> { val clientSecret = json.getString("clientSecret") stripe.handleNextActionForPayment(this, clientSecret) } else -> { // Display success message } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) stripe.onPaymentResult(requestCode, data, object : ApiResultCallback<PaymentIntentResult> { override fun onSuccess(result: PaymentIntentResult) { when (result.outcome) { StripeIntentResult.Outcome.SUCCEEDED -> { // The card action has been handled // Send the Payment Intent ID to your server and confirm it again } StripeIntentResult.Outcome.CANCELED -> { // Canceled } StripeIntentResult.Outcome.TIMEDOUT -> { // Timed-out } StripeIntentResult.Outcome.FAILED -> { // Failed } } } override fun onError(e: Exception) { // Error encountered } } ) } }

3 Confirm the PaymentIntent again Server-side

Using the same endpoint you set up earlier, confirm the PaymentIntent again to finalize the payment and fulfill the order. The payment attempt fails and transitions back to requires_payment_method if it is not confirmed again within one hour.

curl https://api.stripe.com/v1/payment_intents/{{PAYMENT_INTENT_ID}}/confirm \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -X POST
# AJAX endpoint when `/pay` is called from client post '/pay' do data = JSON.parse(request.body.read.to_s) begin if data['payment_method_id'] # Create the PaymentIntent intent = Stripe::PaymentIntent.create( amount: 1099, currency: 'usd', confirm: true, payment_method: data['payment_method_id'], confirmation_method: 'manual', use_stripe_sdk: true, ) elsif data['payment_intent_id'] intent = Stripe::PaymentIntent.confirm(data['payment_intent_id']) end return generate_response(intent) rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end end
# AJAX endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def confirm_payment(): data = request.get_json() try: intent = None if 'payment_method_id' in data: # Create the PaymentIntent intent = stripe.PaymentIntent.create( amount = 1099, currency = 'usd', confirm = True, payment_method = data['payment_method_id'], confirmation_method = 'manual', use_stripe_sdk = True, ) elif 'payment_intent_id' in data: intent = stripe.PaymentIntent.confirm(data['payment_intent_id']) return generate_response(intent) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200
<?php # vendor using composer require_once('vendor/autoload.php'); // Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); header('Content-Type: application/json'); # retrieve json from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); try { if (isset($json_obj->payment_method_id)) { # Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'amount' => 1099, 'currency' => 'usd', 'confirm' => true, 'payment_method' => $json_obj->payment_method_id, 'confirmation_method' => 'manual', 'use_stripe_sdk' => true, ]); } if (isset($json_obj->payment_intent_id)) { $intent = \Stripe\PaymentIntent::retrieve( $json_obj->payment_intent_id ); $intent->confirm(); } generateResponse($intent); } catch (\Stripe\Exception\ApiErrorException $e) { # Display error on client echo json_encode([ 'error' => $e->getMessage() ]); }
// Using Express const express = require('express'); const app = express(); app.use(express.json()); app.post('/pay', async (request, response) => { try { let intent; if (request.body.payment_method_id) { // Create the PaymentIntent intent = await stripe.paymentIntents.create({ amount: 1099, currency: 'usd', confirm: true, payment_method: request.body.payment_method_id, confirmation_method: 'manual', use_stripe_sdk: true }); } else if (request.body.payment_intent_id) { intent = await stripe.paymentIntents.confirm( request.body.payment_intent_id ); } // Send the response to the client return generateResponse(response, intent); } catch (e) { if (e.type === 'StripeCardError') { // Display error on client return response.send({ error: e.message }); } else { // Something else happened return response.status(500).send({ error: e.type }); } } });
post("/pay", (request, response) -> { Gson gson = new Gson(); String paymentMethod = gson.fromJson(request.body(), JsonObject.class) .get("payment_method_id").getAsString(); try { if (confirmRequest.getPaymentMethodId() != null) { PaymentIntentCreateParams createParams = PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .setConfirm(true) .setPaymentMethod(confirmRequest.paymentMethodId) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .setUseStripeSdk(true) .build(); intent = PaymentIntent.create(createParams); } else if (confirmRequest.getPaymentIntentId() != null) { intent = PaymentIntent.retrieve(confirmRequest.getPaymentIntentId()); intent = intent.confirm(); } return gson.toJson(buildResponse(response, intent)); } catch (CardException e) { response.status(200); Map<String, String> errorResponse = new HashMap<>(); errorResponse.put("error", e.getStripeError().getMessage()); return gson.toJson(errorResponse); } });
func handlePay(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } var req struct { PaymentMethodID string `json:"payment_method_id"` } var intent *stripe.PaymentIntent var err error if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("json.NewDecoder.Decode: %v", err) return } if confirmPaymentRequest.PaymentMethodId != nil { // Create the PaymentIntent params := &stripe.PaymentIntentParams{ PaymentMethod: stripe.String(*confirmPaymentRequest.PaymentMethodId), Amount: stripe.Int64(1099), Currency: stripe.String(string(stripe.CurrencyUSD)), Confirm: stripe.Bool(true), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), UseStripeSdk: stripe.Bool(true), } intent, err = paymentintent.New(params) } if confirmPaymentRequest.PaymentIntentId != nil { intent, err = paymentintent.Confirm( *confirmPaymentRequest.PaymentIntentId, nil, ) } if err != nil { if stripeErr, ok := err.(*stripe.Error); ok { // return error message to the client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(CreatePaymentIntentResponse{ ErrorMessage: stripeErr.Msg, }) } else { w.WriteHeader(http.StatusInternalServerError) } } else { generateResponse(pi, w) } }
public IActionResult Index([FromBody] CreatePaymentIntentRequest request) { var service = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { if (request.PaymentMethodId != null) { // Create the PaymentIntent var createOptions = new PaymentIntentCreateOptions { PaymentMethod = request.PaymentMethodId, Amount = 1099, Currency = "usd", Confirm = true, ConfirmationMethod = "manual", UseStripeSdk = true, }; paymentIntent = paymentIntentService.Create(createOptions); } if (request.PaymentIntentId != null) { var confirmOptions = new PaymentIntentConfirmOptions{}; paymentIntent = paymentIntentService.Confirm( request.PaymentIntentId, confirmOptions ); } } catch (StripeException e) { return Json(new { error = e.StripeError.Message }); } return generatePaymentResponse(paymentIntent); }

4 Test the integration

Use our test cards in test mode to verify that your integration was properly updated. Stripe displays a fake authentication page inside the modal in test mode that lets you simulate a successful or failed authentication attempt. In live mode the bank controls the UI of what is displayed inside the modal.

Number Description
4242424242424242 Succeeds and immediately processes the payment.
4000000000009995 Always fails with a decline code of insufficient_funds.
4000002500003155 Requires authentication which, in this integration, will pop up a modal for the user to authenticate.
Was this page helpful?
Questions? Contact us.
Developer tutorials on YouTube.
You can unsubscribe at any time. Read our privacy policy.