Finalize payments on the server Payment Intents API

Learn how to confirm a payment on your server and handle card authentication requests.

For a wider range of support and future proofing, we strongly recommend our standard integration for asynchronous payments.

This guide lets you wait for the returned response from the client and finalize a payment on the server, without using webhooks or processing offline events. While it may seem simpler, this integration is difficult to scale as your business grows.

Limitations
  • Only supports cards: You’ll have to write more code to support ACH and popular regional payment methods separately.
  • Double-charge risk: By synchronously creating a new PaymentIntent each time your customer attempts to pay, you risk accidentally double-charging your customer. Be sure to follow best practices.
  • Extra trip to client: If your customer has a card with 3D Secure or is subject to regulations like Strong Customer Authentication, this integration requires an extra trip to the client to authenticate.

If you don’t mind these limitations, feel free to integrate. Otherwise, use the standard integration.

What you're building

1 Set up Stripe

First, you need a Stripe account. Register now.

Use our official libraries for access to the Stripe API from your application:

# Available as a gem sudo gem install stripe
# If you use bundler, you can add this line to your Gemfile gem 'stripe'
# Install through pip pip install --upgrade stripe
# Or find the Stripe package on http://pypi.python.org/pypi/stripe/
# Find the version you want to pin: # https://github.com/stripe/stripe-python/blob/master/CHANGELOG.md # Specify that version in your requirements.txt file stripe>=2.48.0,<3.0
# Install the PHP library via Composer composer require stripe/stripe-php
# Or download the source directly: https://github.com/stripe/stripe-php/releases
/* For Gradle, add the following dependency to your build.gradle and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest */ implementation "com.stripe:stripe-java:{VERSION}"
<!-- For Maven, add the following dependency to your POM and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest --> <dependency> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>{VERSION}</version> </dependency>
# For other environments, manually install the following JARs: # - The Stripe JAR from https://github.com/stripe/stripe-java/releases/latest # - Google Gson from https://github.com/google/gson
# Install via npm npm install --save stripe
# Install stripe-go go get -u github.com/stripe/stripe-go
// Then import the package import ( "github.com/stripe/stripe-go/v71" )
# Install via dotnet dotnet add package Stripe.net dotnet restore
# Or install via NuGet PM> Install-Package Stripe.net

2 Collect card details Client-side

Collect card information on the client with Stripe.js and Stripe Elements. Elements is a set of prebuilt UI components for collecting and validating card number, ZIP code, and expiration date.

A Stripe Element contains an iframe that securely sends the payment information to Stripe over a HTTPS connection. The checkout page address must also start with https:// rather than http:// for your integration to work.

You can test your integration without using HTTPS. Enable it when you’re ready to accept live payments.

Include the Stripe.js script in the head of every page on your site. Elements is automatically available as a feature of Stripe.js.

<script src="https://js.stripe.com/v3/"></script>

Including the script on every page of your site lets you take advantage of Stripe’s advanced fraud functionality and ability to detect anomalous browsing behavior.

Build the payment form

To securely collect card details from your customers, Elements creates UI components for you that are hosted by Stripe. They are then placed into your payment form as an iframe. To determine where to insert these components, create empty DOM elements (containers) with unique IDs within your payment form.

<form id='payment-form'> <label> Card details <!-- placeholder for Elements --> <div id="card-element"></div> </label> <button type="submit">Submit Payment</button> </form>
/** * Shows how you can use CSS to style your Element's container. * These classes are added to your Stripe Element by default. * You can override these classNames by using the options passed * to the `card` elemenent. * https://stripe.com/docs/js/elements_object/create_element?type=card#elements_create-options-classes */ .StripeElement { height: 40px; padding: 10px 12px; width: 100%; color: #32325d; background-color: white; border: 1px solid transparent; border-radius: 4px; box-shadow: 0 1px 3px 0 #e6ebf1; -webkit-transition: box-shadow 150ms ease; transition: box-shadow 150ms ease; } .StripeElement--focus { box-shadow: 0 1px 3px 0 #cfd7df; } .StripeElement--invalid { border-color: #fa755a; } .StripeElement--webkit-autofill { background-color: #fefde5 !important; }

Next, create an instance of the Stripe object, providing your publishable API key as the first parameter. Afterwards, create an instance of the Elements object and use it to mount a Card element in the relevant placeholder in the page.

var stripe = Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx'); var elements = stripe.elements(); // Set up Stripe.js and Elements to use in checkout form var style = { base: { color: "#32325d", fontFamily: '"Helvetica Neue", Helvetica, sans-serif', fontSmoothing: "antialiased", fontSize: "16px", "::placeholder": { color: "#aab7c4" } }, invalid: { color: "#fa755a", iconColor: "#fa755a" }, }; var cardElement = elements.create('card', {style: style}); cardElement.mount('#card-element');
const stripe = Stripe('pk_test_TYooMQauvdEDq54NiTphI7jx'); const elements = stripe.elements(); // Set up Stripe.js and Elements to use in checkout form const style = { base: { color: "#32325d", fontFamily: '"Helvetica Neue", Helvetica, sans-serif', fontSmoothing: "antialiased", fontSize: "16px", "::placeholder": { color: "#aab7c4" } }, invalid: { color: "#fa755a", iconColor: "#fa755a" }, }; const cardElement = elements.create('card', {style}); cardElement.mount('#card-element');

The card Element simplifies the form and minimizes the number of fields required by inserting a single, flexible input field that securely collects all necessary card details.

Otherwise, combine cardNumber, cardExpiry, and cardCvc Elements for a flexible, multi-input card form.

Create a PaymentMethod

Finally, use stripe.createPaymentMethod on your client to collect the card details and create a PaymentMethod when the user clicks the submit button.

var form = document.getElementById('payment-form'); form.addEventListener('submit', function(event) { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); stripe.createPaymentMethod({ type: 'card', card: cardElement, billing_details: { // Include any additional collected billing details. name: 'Jenny Rosen', }, }).then(stripePaymentMethodHandler); });
const form = document.getElementById('payment-form'); form.addEventListener('submit', async (event) => { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); const result = await stripe.createPaymentMethod({ type: 'card', card: cardElement, billing_details: { // Include any additional collected billing details. name: 'Jenny Rosen', }, }) stripePaymentMethodHandler(result); });

Install React Stripe.js and the Stripe.js loader from the npm public registry.

npm install --save @stripe/react-stripe-js @stripe/stripe-js

We also provide a UMD build for sites that are not using npm or modules.

Include the Stripe.js script which exports a global Stripe function and the UMD build of React Stripe.js which exports a global ReactStripe object. Always load the Stripe.js script directly from js.stripe.com to remain PCI compliant. Do not include the script in a bundle or host a copy of it yourself.

<!-- Stripe.js --> <script src="https://js.stripe.com/v3/"></script> <!-- React Stripe.js development build --> <script src="https://unpkg.com/@stripe/react-stripe-js@latest/dist/react-stripe.umd.js"></script> <!-- When you are ready to deploy your site to production, remove the above development script, and include the following production build. --> <script src="https://unpkg.com/@stripe/react-stripe-js@latest/dist/react-stripe.umd.min.js"></script>

Add Stripe.js and Elements to your page

To use Element components, wrap the root of your React app in an Elements provider. Call loadStripe with your publishable key and pass the returned Promise to the Elements provider.

import React from 'react'; import ReactDOM from 'react-dom'; import {Elements} from '@stripe/react-stripe-js'; import {loadStripe} from '@stripe/stripe-js'; import CheckoutForm from './CheckoutForm'; // Make sure to call `loadStripe` outside of a component’s render to avoid // recreating the `Stripe` object on every render. const stripePromise = loadStripe("pk_test_TYooMQauvdEDq54NiTphI7jx"); function App() { return ( <Elements stripe={stripePromise}> <CheckoutForm /> </Elements> ); }; ReactDOM.render(<App />, document.getElementById('root'));

Add and configure a CardElement component

Use individual Element components, such as CardElement, to build your form.

/** * Use the CSS tab above to style your Element's container. */ import React from 'react'; import {CardElement} from '@stripe/react-stripe-js'; import './CardSectionStyles.css' const CARD_ELEMENT_OPTIONS = { style: { base: { color: "#32325d", fontFamily: '"Helvetica Neue", Helvetica, sans-serif', fontSmoothing: "antialiased", fontSize: "16px", "::placeholder": { color: "#aab7c4", }, }, invalid: { color: "#fa755a", iconColor: "#fa755a", }, }, }; function CardSection() { return ( <label> Card details <CardElement options={CARD_ELEMENT_OPTIONS} /> </label> ); }; export default CardSection;
/** * Shows how you can use CSS to style your Element's container. * These classes are added to your Stripe Element by default. * You can override these classNames by using the options passed * to the CardElement component. * https://stripe.com/docs/js/elements_object/create_element?type=card#elements_create-options-classes */ .StripeElement { height: 40px; padding: 10px 12px; width: 100%; color: #32325d; background-color: white; border: 1px solid transparent; border-radius: 4px; box-shadow: 0 1px 3px 0 #e6ebf1; -webkit-transition: box-shadow 150ms ease; transition: box-shadow 150ms ease; } .StripeElement--focus { box-shadow: 0 1px 3px 0 #cfd7df; } .StripeElement--invalid { border-color: #fa755a; } .StripeElement--webkit-autofill { background-color: #fefde5 !important; }

Elements are completely customizable. You can style Elements to match the look and feel of your site, providing a seamless checkout experience for your customers. It’s also possible to style various input states, for example when the Element has focus.

The CardElement simplifies the form and minimizes the number of required fields by inserting a single, flexible input field that securely collects all necessary card and billing details. Otherwise, combine CardNumberElement, CardExpiryElement, and CardCvcElement elements for a flexible, multi-input card form.

Create a PaymentMethod

In your payment form’s submit handler, use stripe.createPaymentMethod to collect the card details and create a PaymentMethod.

To call stripe.createPaymentMethod from your payment form component, use the useStripe and useElements hooks.

If you prefer traditional class components over hooks, you can instead use an ElementsConsumer.

import React from 'react'; import {useStripe, useElements, CardElement} from '@stripe/react-stripe-js'; import CardSection from './CardSection'; export default function CheckoutForm() { const stripe = useStripe(); const elements = useElements(); const handleSubmit = async (event) => { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); if (!stripe || !elements) { // Stripe.js has not yet loaded. // Make sure to disable form submission until Stripe.js has loaded. return; } const result = await stripe.createPaymentMethod({ type: 'card', card: elements.getElement(CardElement), billing_details: { // Include any additional collected billing details. name: 'Jenny Rosen', }, }); stripePaymentMethodHandler(result); }; return ( <form onSubmit={this.handleSubmit}> <CardSection /> <button type="submit" disabled={!stripe}> Submit Payment </button> </form> ); }
import React from 'react'; import {ElementsConsumer, CardElement} from '@stripe/react-stripe-js'; import CardSection from './CardSection'; class CheckoutForm extends React.Component { handleSubmit = async (event) => { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); const {stripe, elements} = this.props if (!stripe || !elements) { // Stripe.js has not yet loaded. // Make sure to disable form submission until Stripe.js has loaded. return; } const result = await stripe.createPaymentMethod({ type: 'card', card: elements.getElement(CardElement), billing_details: { // Include any additional collected billing details. name: 'Jenny Rosen', }, }); stripePaymentMethodHandler(result); }; render() { const {stripe} = this.props; return ( <form onSubmit={this.handleSubmit}> <CardSection /> <button type="submit" disabled={!stripe}> Submit Payment </button> </form> ); } } export default function InjectedCheckoutForm() { return ( <ElementsConsumer> {({stripe, elements}) => ( <CheckoutForm stripe={stripe} elements={elements} /> )} </ElementsConsumer> ); }

3 Submit the PaymentMethod to your server Client-side

If the PaymentMethod was created successfully, send its ID to your server.

function stripePaymentMethodHandler(result) { if (result.error) { // Show error in payment form } else { // Otherwise send paymentMethod.id to your server (see Step 4) fetch('/pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ payment_method_id: result.paymentMethod.id, }) }).then(function(result) { // Handle server response (see Step 4) result.json().then(function(json) { handleServerResponse(json); }) }); } }
const stripePaymentMethodHandler = async (result) => { if (result.error) { // Show error in payment form } else { // Otherwise send paymentMethod.id to your server (see Step 4) const res = await fetch('/pay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ payment_method_id: result.paymentMethod.id, }), }) const paymentResponse = await res.json(); // Handle server response (see Step 4) handleServerResponse(paymentResponse); } }

4 Create a PaymentIntent Server-side

Set up an endpoint on your server to receive the request. This endpoint will also be used in Step 6 to handle cards that require an extra step of authentication.

Create a new PaymentIntent with the ID of the PaymentMethod created on your client. You can confirm the PaymentIntent by setting the confirm property to true when the PaymentIntent is created or by calling confirm after creation. Separate authorization and capture is also supported for card payments.

If the payment requires additional actions such as 3D Secure authentication, the PaymentIntent’s status will be set to requires_action. If the payment failed, the status is set back to requires_payment_method and you should show an error to your user. If the payment doesn’t require any additional authentication then a charge is created and the PaymentIntent status is set to succeeded.

curl https://api.stripe.com/v1/payment_intents \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d payment_method="{{PAYMENT_METHOD_ID}}" \ -d amount=1099 \ -d currency=usd \ -d confirmation_method=manual \ -d confirm=true
# 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( payment_method: data['payment_method_id'], amount: 1099, currency: 'usd', confirmation_method: 'manual', confirm: true, ) elsif data['payment_intent_id'] intent = Stripe::PaymentIntent.confirm(data['payment_intent_id']) end rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end return generate_response(intent) end def generate_response(intent) # Note that if your API version is before 2019-02-11, 'requires_action' # appears as 'requires_source_action'. if intent.status == 'requires_action' && intent.next_action.type == 'use_stripe_sdk' # Tell the client to handle the action [ 200, { requires_action: true, payment_intent_client_secret: intent.client_secret }.to_json ] elsif intent.status == 'succeeded' # The payment didn’t need any additional actions and is completed! # Handle post-payment fulfillment [200, { success: true }.to_json] else # Invalid status return [500, { error: 'Invalid PaymentIntent status' }.to_json] end end
# AJAX endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def pay(): data = request.get_json() intent = None try: if 'payment_method_id' in data: # Create the PaymentIntent intent = stripe.PaymentIntent.create( payment_method = data['payment_method_id'], amount = 1099, currency = 'usd', confirmation_method = 'manual', confirm = True, ) elif 'payment_intent_id' in data: intent = stripe.PaymentIntent.confirm(data['payment_intent_id']) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200 return generate_response(intent) def generate_response(intent): # Note that if your API version is before 2019-02-11, 'requires_action' # appears as 'requires_source_action'. if intent.status == 'requires_action' and intent.next_action.type == 'use_stripe_sdk': # Tell the client to handle the action return json.dumps({ 'requires_action': True, 'payment_intent_client_secret': intent.client_secret, }), 200 elif intent.status == 'succeeded': # The payment didn’t need any additional actions and completed! # Handle post-payment fulfillment return json.dumps({'success': True}), 200 else: # Invalid status return json.dumps({'error': 'Invalid PaymentIntent status'}), 500
<?php # vendor using composer require_once('vendor/autoload.php'); \Stripe\Stripe::setApiKey(getenv('STRIPE_SECRET_KEY')); header('Content-Type: application/json'); # retrieve json from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); $intent = null; try { if (isset($json_obj->payment_method_id)) { # Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'payment_method' => $json_obj->payment_method_id, 'amount' => 1099, 'currency' => 'usd', 'confirmation_method' => 'manual', 'confirm' => 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() ]); } function generateResponse($intent) { # Note that if your API version is before 2019-02-11, 'requires_action' # appears as 'requires_source_action'. if ($intent->status == 'requires_action' && $intent->next_action->type == 'use_stripe_sdk') { # Tell the client to handle the action echo json_encode([ 'requires_action' => true, 'payment_intent_client_secret' => $intent->client_secret ]); } else if ($intent->status == 'succeeded') { # The payment didn’t need any additional actions and completed! # Handle post-payment fulfillment echo json_encode([ "success" => true ]); } else { # Invalid status http_response_code(500); echo json_encode(['error' => 'Invalid PaymentIntent status']); } } ?>
package com.stripe.generator; import static spark.Spark.post; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import com.stripe.Stripe; import com.stripe.model.PaymentIntent; import com.stripe.param.PaymentIntentCreateParams; import java.util.HashMap; import java.util.Map; import spark.Request; import spark.Response; import spark.Route; public class MyApp { static class ConfirmPaymentRequest { @SerializedName("payment_method_id") String paymentMethodId; @SerializedName("payment_intent_id") String paymentIntentId; public String getPaymentMethodId() { return paymentMethodId; } public String getPaymentIntentId() { return paymentIntentId; } } /** * Your application. */ public static void main(String[] args) { Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); post(new Route("/pay") { @Override public Object handle(Request request, Response response) { Gson gson = new Gson(); ConfirmPaymentRequest confirmRequest = gson.fromJson(request.body(), ConfirmPaymentRequest.class); PaymentIntent intent; try { if (confirmRequest.getPaymentMethodId() != null) { PaymentIntentCreateParams createParams = PaymentIntentCreateParams.builder() .setAmount(1099) .setCurrency("usd") .setConfirm(true) .setPaymentMethod(confirmRequest.paymentMethodId) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .build(); intent = PaymentIntent.create(createParams); } else if (confirmRequest.getPaymentIntentId() != null) { intent = PaymentIntent.retrieve(confirmRequest.getPaymentIntentId()); intent = intent.confirm(); } Map<String, Object> responseData = generateResponse(response, intent); return gson.toJson(responseData); } catch (Exception e) { response.status(500); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("message", e.getMessage()); return gson.toJson(errorResponse); } } private Map<String, Object> generateResponse(Response response, PaymentIntent intent) { response.type("application/json"); Map<String, Object> responseData = new HashMap<>(); // Note that if your API version is before 2019-02-11, 'requires_action' // appears as 'requires_source_action'. if (intent.getStatus().equals("requires_action") && intent.getNextAction().getType().equals("use_stripe_sdk")) { responseData.put("requires_action", true); responseData.put("payment_intent_client_secret", intent.getClientSecret()); } else if (intent.getStatus().equals("succeeded")) { responseData.put("success", true); } else { // invalid status responseData.put("Error", "Invalid status"); response.status(500); return responseData; } response.status(200); return responseData; } }); } }
// 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({ payment_method: request.body.payment_method_id, amount: 1099, currency: 'usd', confirmation_method: 'manual', confirm: true }); } else if (request.body.payment_intent_id) { intent = await stripe.paymentIntents.confirm( request.body.payment_intent_id ); } // Send the response to the client response.send(generateResponse(intent)); } catch (e) { // Display error on client return response.send({ error: e.message }); } }); const generateResponse = (intent) => { // Note that if your API version is before 2019-02-11, 'requires_action' // appears as 'requires_source_action'. if ( intent.status === 'requires_action' && intent.next_action.type === 'use_stripe_sdk' ) { // Tell the client to handle the action return { requires_action: true, payment_intent_client_secret: intent.client_secret }; } else if (intent.status === 'succeeded') { // The payment didn’t need any additional actions and completed! // Handle post-payment fulfillment return { success: true }; } else { // Invalid status return { error: 'Invalid PaymentIntent status' } } };
package main import ( "encoding/json" "net/http" "os" "fmt" stripe "github.com/stripe/stripe-go/v71" paymentintent "github.com/stripe/stripe-go/v71/paymentintent" ) // Structs to handle request and response JSON serializations type ConfirmPaymentRequest struct { PaymentMethodId *string `json:"payment_method_id"` PaymentIntentId *string `json:"payment_intent_id"` } type ErrorResponse struct { Message string `json:"error"` } type ConfirmPaymentResponse struct { RequiresAction bool `json:"requires_action"` PaymentIntentClientSecret *string `json:"payment_intent_client_secret"` } func generatePaymentResponse(intent *stripe.PaymentIntent, w http.ResponseWriter) { // Note that if your API version is before 2019-02-11, PaymentIntentStatusRequiresAction // appears as PaymentIntentStatusRequiresSourceAction. if intent.Status == stripe.PaymentIntentStatusRequiresAction && intent.NextAction.Type == "use_stripe_sdk" { // Tell the client to handle the action w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ConfirmPaymentResponse{ RequiresAction: true, PaymentIntentClientSecret: &intent.ClientSecret, }) } else if intent.Status == stripe.PaymentIntentStatusSucceeded { // The payment didn’t need any additional actions and completed! // Handle post-payment fulfillment w.WriteHeader(http.StatusOK) fmt.Fprint(w, "Success") } else { // Invalid status w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Invalid Payment Intent status: %s", intent.Status) } } func main() { // Initialize stripe-go with Stripe secret key from environment stripe.Key = os.Getenv("STRIPE_SECRET_KEY") // AJAX endpoint when `/pay` is called from client http.HandleFunc("/pay", func(w http.ResponseWriter, req *http.Request) { var confirmPaymentRequest ConfirmPaymentRequest json.NewDecoder(req.Body).Decode(&confirmPaymentRequest) var intent *stripe.PaymentIntent var err error 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)), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), Confirm: 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 { // Display error on client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ErrorResponse{Message: stripeErr.Msg}) } else { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Other error occurred, %v\n", err.Error()) } } else { generatePaymentResponse(intent, w) } }) http.ListenAndServe(":"+os.Getenv("PORT"), nil) }
using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Stripe; using Newtonsoft.Json; namespace Controllers { public class ConfirmPaymentRequest { [JsonProperty("payment_method_id")] public string PaymentMethodId { get; set; } [JsonProperty("payment_intent_id")] public string PaymentIntentId { get; set; } } // AJAX endpoint when `/pay` is called from client [Route("/pay")] public class ConfirmPaymentController : Controller { public IActionResult Index([FromBody] ConfirmPaymentRequest request) { var paymentIntentService = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { if (request.PaymentMethodId != null) { // Create the PaymentIntent var createOptions = new PaymentIntentCreateOptions { PaymentMethod = request.PaymentMethodId, Amount = 1099, Currency = "usd", ConfirmationMethod = "manual", Confirm = 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); } private IActionResult generatePaymentResponse(PaymentIntent intent) { // Note that if your API version is before 2019-02-11, 'requires_action' // appears as 'requires_source_action'. if (intent.Status == "requires_action" && intent.NextAction.Type == "use_stripe_sdk") { // Tell the client to handle the action return Json(new { requires_action = true, payment_intent_client_secret = intent.ClientSecret }); } else if (intent.Status == "succeeded") { // The payment didn’t need any additional actions and completed! // Handle post-payment fulfillment return Json(new { success = true }); } else { // Invalid status return StatusCode(500, new { error = "Invalid PaymentIntent status" }); } } } }

If you want to save the card to reuse later, create a Customer to store the PaymentMethod and pass the following additional parameters when creating the PaymentIntent:

  • customer. Set to the ID of the Customer.
  • setup_future_usage. Set to off_session to tell Stripe that you plan to reuse this PaymentMethod for off-session payments when your customer is not present. Setting this property saves the PaymentMethod to the Customer after the PaymentIntent is confirmed and any required actions from the user are complete.

See the code sample on saving cards after a payment for more details.

5 Handle any next actions Client-side

Write code to handle situations that require your customer to intervene. A payment normally succeeds after you confirm it on the server in step 4. However, when the PaymentIntent requires additional action from the customer, such as authenticating with 3D Secure, this code comes into play.

Use stripe.handleCardAction to trigger the UI for handling customer action. If authentication succeeds, the PaymentIntent has a status of requires_confirmation. You then need to confirm the PaymentIntent again on your server to finish the payment.

While testing, use a test card number that requires authentication (e.g., 4000002760003184) to force this flow. Using a card that doesn’t require authentication (e.g., 4242424242424242) skips this part of the flow and completes at step 4.

function handleServerResponse(response) { if (response.error) { // Show error from server on payment form } else if (response.requires_action) { // Use Stripe.js to handle required card action stripe.handleCardAction( response.payment_intent_client_secret ).then(handleStripeJsResult); } else { // Show success message } } function handleStripeJsResult(result) { if (result.error) { // Show error 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); } }
const handleServerResponse = async (response) => { if (response.error) { // Show error from server on payment form } else if (response.requires_action) { // Use Stripe.js to handle the required card action const { error: errorAction, paymentIntent } = await stripe.handleCardAction(response.payment_intent_client_secret); 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 } }

6 Confirm the PaymentIntent again Server-side

This code is only executed when a payment requires additional authentication—just like the handling in the previous step. The code itself is not optional because any payment could require this extra step.

Using the same endpoint you set up in step 4, confirm the PaymentIntent again to finalize the payment and fulfill the order. Make sure this confirmation happens within one hour of the payment attempt. Otherwise, the payment fails and transitions back to requires_payment_method.

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( payment_method: data['payment_method_id'], amount: 1099, currency: 'usd', confirmation_method: 'manual', confirm: true, ) elsif data['payment_intent_id'] intent = Stripe::PaymentIntent.confirm(data['payment_intent_id']) end rescue Stripe::CardError => e # Display error on client return [200, { error: e.message }.to_json] end return generate_response(intent) end def generate_response(intent) # Note that if your API version is before 2019-02-11, 'requires_action' # appears as 'requires_source_action'. if intent.status == 'requires_action' && intent.next_action.type == 'use_stripe_sdk' # Tell the client to handle the action [ 200, { requires_action: true, payment_intent_client_secret: intent.client_secret }.to_json ] elsif intent.status == 'succeeded' # The payment didn’t need any additional actions and is completed! # Handle post-payment fulfillment [200, { success: true }.to_json] else # Invalid status return [500, { error: 'Invalid PaymentIntent status' }.to_json] end end
# AJAX endpoint when `/pay` is called from client @app.route('/pay', methods=['POST']) def pay(): data = request.get_json() intent = None try: if 'payment_method_id' in data: # Create the PaymentIntent intent = stripe.PaymentIntent.create( payment_method = data['payment_method_id'], amount = 1099, currency = 'usd', confirmation_method = 'manual', confirm = True, ) elif 'payment_intent_id' in data: intent = stripe.PaymentIntent.confirm(data['payment_intent_id']) except stripe.error.CardError as e: # Display error on client return json.dumps({'error': e.user_message}), 200 return generate_response(intent) def generate_response(intent): # Note that if your API version is before 2019-02-11, 'requires_action' # appears as 'requires_source_action'. if intent.status == 'requires_action' and intent.next_action.type == 'use_stripe_sdk': # Tell the client to handle the action return json.dumps({ 'requires_action': True, 'payment_intent_client_secret': intent.client_secret, }), 200 elif intent.status == 'succeeded': # The payment didn’t need any additional actions and completed! # Handle post-payment fulfillment return json.dumps({'success': True}), 200 else: # Invalid status return json.dumps({'error': 'Invalid PaymentIntent status'}), 500
<?php # vendor using composer require_once('vendor/autoload.php'); \Stripe\Stripe::setApiKey(getenv('STRIPE_SECRET_KEY')); header('Content-Type: application/json'); # retrieve json from POST body $json_str = file_get_contents('php://input'); $json_obj = json_decode($json_str); $intent = null; try { if (isset($json_obj->payment_method_id)) { # Create the PaymentIntent $intent = \Stripe\PaymentIntent::create([ 'payment_method' => $json_obj->payment_method_id, 'amount' => 1099, 'currency' => 'usd', 'confirmation_method' => 'manual', 'confirm' => 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() ]); } function generateResponse($intent) { # Note that if your API version is before 2019-02-11, 'requires_action' # appears as 'requires_source_action'. if ($intent->status == 'requires_action' && $intent->next_action->type == 'use_stripe_sdk') { # Tell the client to handle the action echo json_encode([ 'requires_action' => true, 'payment_intent_client_secret' => $intent->client_secret ]); } else if ($intent->status == 'succeeded') { # The payment didn’t need any additional actions and completed! # Handle post-payment fulfillment echo json_encode([ "success" => true ]); } else { # Invalid status http_response_code(500); echo json_encode(['error' => 'Invalid PaymentIntent status']); } } ?>
package com.stripe.generator; import static spark.Spark.post; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import com.stripe.Stripe; import com.stripe.model.PaymentIntent; import com.stripe.param.PaymentIntentCreateParams; import java.util.HashMap; import java.util.Map; import spark.Request; import spark.Response; import spark.Route; public class MyApp { static class ConfirmPaymentRequest { @SerializedName("payment_method_id") String paymentMethodId; @SerializedName("payment_intent_id") String paymentIntentId; public String getPaymentMethodId() { return paymentMethodId; } public String getPaymentIntentId() { return paymentIntentId; } } /** * Your application. */ public static void main(String[] args) { Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); post(new Route("/pay") { @Override public Object handle(Request request, Response response) { Gson gson = new Gson(); ConfirmPaymentRequest confirmRequest = gson.fromJson(request.body(), ConfirmPaymentRequest.class); PaymentIntent intent; try { if (confirmRequest.getPaymentMethodId() != null) { PaymentIntentCreateParams createParams = PaymentIntentCreateParams.builder() .setAmount(1099) .setCurrency("usd") .setConfirm(true) .setPaymentMethod(confirmRequest.paymentMethodId) .setConfirmationMethod(PaymentIntentCreateParams.ConfirmationMethod.MANUAL) .build(); intent = PaymentIntent.create(createParams); } else if (confirmRequest.getPaymentIntentId() != null) { intent = PaymentIntent.retrieve(confirmRequest.getPaymentIntentId()); intent = intent.confirm(); } Map<String, Object> responseData = generateResponse(response, intent); return gson.toJson(responseData); } catch (Exception e) { response.status(500); Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("message", e.getMessage()); return gson.toJson(errorResponse); } } private Map<String, Object> generateResponse(Response response, PaymentIntent intent) { response.type("application/json"); Map<String, Object> responseData = new HashMap<>(); // Note that if your API version is before 2019-02-11, 'requires_action' // appears as 'requires_source_action'. if (intent.getStatus().equals("requires_action") && intent.getNextAction().getType().equals("use_stripe_sdk")) { responseData.put("requires_action", true); responseData.put("payment_intent_client_secret", intent.getClientSecret()); } else if (intent.getStatus().equals("succeeded")) { responseData.put("success", true); } else { // invalid status responseData.put("Error", "Invalid status"); response.status(500); return responseData; } response.status(200); return responseData; } }); } }
// 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({ payment_method: request.body.payment_method_id, amount: 1099, currency: 'usd', confirmation_method: 'manual', confirm: true }); } else if (request.body.payment_intent_id) { intent = await stripe.paymentIntents.confirm( request.body.payment_intent_id ); } // Send the response to the client response.send(generateResponse(intent)); } catch (e) { // Display error on client return response.send({ error: e.message }); } }); const generateResponse = (intent) => { // Note that if your API version is before 2019-02-11, 'requires_action' // appears as 'requires_source_action'. if ( intent.status === 'requires_action' && intent.next_action.type === 'use_stripe_sdk' ) { // Tell the client to handle the action return { requires_action: true, payment_intent_client_secret: intent.client_secret }; } else if (intent.status === 'succeeded') { // The payment didn’t need any additional actions and completed! // Handle post-payment fulfillment return { success: true }; } else { // Invalid status return { error: 'Invalid PaymentIntent status' } } };
package main import ( "encoding/json" "net/http" "os" "fmt" stripe "github.com/stripe/stripe-go/v71" paymentintent "github.com/stripe/stripe-go/v71/paymentintent" ) // Structs to handle request and response JSON serializations type ConfirmPaymentRequest struct { PaymentMethodId *string `json:"payment_method_id"` PaymentIntentId *string `json:"payment_intent_id"` } type ErrorResponse struct { Message string `json:"error"` } type ConfirmPaymentResponse struct { RequiresAction bool `json:"requires_action"` PaymentIntentClientSecret *string `json:"payment_intent_client_secret"` } func generatePaymentResponse(intent *stripe.PaymentIntent, w http.ResponseWriter) { // Note that if your API version is before 2019-02-11, PaymentIntentStatusRequiresAction // appears as PaymentIntentStatusRequiresSourceAction. if intent.Status == stripe.PaymentIntentStatusRequiresAction && intent.NextAction.Type == "use_stripe_sdk" { // Tell the client to handle the action w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ConfirmPaymentResponse{ RequiresAction: true, PaymentIntentClientSecret: &intent.ClientSecret, }) } else if intent.Status == stripe.PaymentIntentStatusSucceeded { // The payment didn’t need any additional actions and completed! // Handle post-payment fulfillment w.WriteHeader(http.StatusOK) fmt.Fprint(w, "Success") } else { // Invalid status w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Invalid Payment Intent status: %s", intent.Status) } } func main() { // Initialize stripe-go with Stripe secret key from environment stripe.Key = os.Getenv("STRIPE_SECRET_KEY") // AJAX endpoint when `/pay` is called from client http.HandleFunc("/pay", func(w http.ResponseWriter, req *http.Request) { var confirmPaymentRequest ConfirmPaymentRequest json.NewDecoder(req.Body).Decode(&confirmPaymentRequest) var intent *stripe.PaymentIntent var err error 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)), ConfirmationMethod: stripe.String(string( stripe.PaymentIntentConfirmationMethodManual, )), Confirm: 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 { // Display error on client w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ErrorResponse{Message: stripeErr.Msg}) } else { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "Other error occurred, %v\n", err.Error()) } } else { generatePaymentResponse(intent, w) } }) http.ListenAndServe(":"+os.Getenv("PORT"), nil) }
using System; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Stripe; using Newtonsoft.Json; namespace Controllers { public class ConfirmPaymentRequest { [JsonProperty("payment_method_id")] public string PaymentMethodId { get; set; } [JsonProperty("payment_intent_id")] public string PaymentIntentId { get; set; } } // AJAX endpoint when `/pay` is called from client [Route("/pay")] public class ConfirmPaymentController : Controller { public IActionResult Index([FromBody] ConfirmPaymentRequest request) { var paymentIntentService = new PaymentIntentService(); PaymentIntent paymentIntent = null; try { if (request.PaymentMethodId != null) { // Create the PaymentIntent var createOptions = new PaymentIntentCreateOptions { PaymentMethod = request.PaymentMethodId, Amount = 1099, Currency = "usd", ConfirmationMethod = "manual", Confirm = 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); } private IActionResult generatePaymentResponse(PaymentIntent intent) { // Note that if your API version is before 2019-02-11, 'requires_action' // appears as 'requires_source_action'. if (intent.Status == "requires_action" && intent.NextAction.Type == "use_stripe_sdk") { // Tell the client to handle the action return Json(new { requires_action = true, payment_intent_client_secret = intent.ClientSecret }); } else if (intent.Status == "succeeded") { // The payment didn’t need any additional actions and completed! // Handle post-payment fulfillment return Json(new { success = true }); } else { // Invalid status return StatusCode(500, new { error = "Invalid PaymentIntent status" }); } } } }

7 Test the integration

By this point you should have a basic card integration that collects card details and makes a payment.

There are several test cards you can use in test mode to make sure this integration is ready. Use them with any CVC, postal code, and future expiration date.

Number Description
4242424242424242 Succeeds and immediately processes the payment.
4000002500003155 Requires authentication. Stripe will trigger a modal asking for the customer to authenticate.
4000000000009995 Always fails with a decline code of insufficient_funds.

For the full list of test cards see our guide on testing.

For a wider range of support and future proofing, we strongly recommend our standard integration for asynchronous payments.

This guide lets you use a single client-to-server flow to take payments, without using webhooks or processing offline events. While it may seem simpler, this integration will be difficult to scale as your business grows. Synchronous payment flows have several limitations:

  • Only supports cards: You’ll have to write more code to support ACH and popular regional payment methods separately.
  • Double-charge risk: By synchronously creating a new PaymentIntent each time your customer attempts to pay, you risk accidentally double-charging your customer. Be sure to follow best practices.
  • Manual authentication handling: If your customer has a card with 3D Secure or is subject to regulations like Strong Customer Authentication, you’ll have to make extra trips to the client.

If you don’t mind these limitations, feel free to integrate. Otherwise, use the standard integration.

1 Set up Stripe Client-side Server-side

First, you need a Stripe account. Register now.

Server-side

This integration requires endpoints on your server that talk to the Stripe API. Use our official libraries for access to the Stripe API from your server:

# Available as a gem sudo gem install stripe
# If you use bundler, you can add this line to your Gemfile gem 'stripe'
# Install through pip pip install --upgrade stripe
# Or find the Stripe package on http://pypi.python.org/pypi/stripe/
# Find the version you want to pin: # https://github.com/stripe/stripe-python/blob/master/CHANGELOG.md # Specify that version in your requirements.txt file stripe>=2.48.0,<3.0
# Install the PHP library via Composer composer require stripe/stripe-php
# Or download the source directly: https://github.com/stripe/stripe-php/releases
/* For Gradle, add the following dependency to your build.gradle and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest */ implementation "com.stripe:stripe-java:{VERSION}"
<!-- For Maven, add the following dependency to your POM and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest --> <dependency> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>{VERSION}</version> </dependency>
# For other environments, manually install the following JARs: # - The Stripe JAR from https://github.com/stripe/stripe-java/releases/latest # - Google Gson from https://github.com/google/gson
# Install via npm npm install --save stripe
# Install stripe-go go get -u github.com/stripe/stripe-go
// Then import the package import ( "github.com/stripe/stripe-go/v71" )
# Install via dotnet dotnet add package Stripe.net dotnet restore
# Or install via NuGet PM> Install-Package Stripe.net

Client-side

The iOS SDK is open source, fully documented, and compatible with apps supporting iOS 10 or above.

  1. If you haven't already, install the latest version of CocoaPods.
  2. If you don't have an existing Podfile, run the following command to create one:
    pod init
  3. Add this line to your Podfile:
    pod 'Stripe'
  4. Run the following command:
    pod install
  5. Don't forget to use the .xcworkspace file to open your project in Xcode, instead of the .xcodeproj file, from here on out.
  6. In the future, to update to the latest version of the SDK, just run:
    pod update Stripe
  1. If you haven't already, install the latest version of Carthage.
  2. Add this line to your Cartfile:
    github "stripe/stripe-ios"
  3. Follow the Carthage installation instructions.
  4. In the future, to update to the latest version of the SDK, run the following command:
    carthage update stripe-ios --platform ios
  1. Head to our GitHub releases page and download and unzip Stripe.framework.zip.
  2. Drag Stripe.framework to the "Embedded Binaries" section of your Xcode project's "General" settings. Make sure to select "Copy items if needed".
  3. Head to the "Build Phases" section of your Xcode project settings, and create a new "Run Script Build Phase". Paste the following snippet into the text field:
    bash "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Stripe.framework/integrate-dynamic-framework.sh"
  4. In the future, to update to the latest version of our SDK, just repeat steps 1 and 2.

When your app starts, configure the SDK with your Stripe publishable key so that it can make requests to the Stripe API.

import UIKit import Stripe @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { Stripe.setDefaultPublishableKey("pk_test_TYooMQauvdEDq54NiTphI7jx") // do any other necessary launch configuration return true } }
#import "AppDelegate.h" #import <Stripe/Stripe.h> @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [Stripe setDefaultPublishableKey:@"pk_test_TYooMQauvdEDq54NiTphI7jx"]; // do any other necessary launch configuration return YES; } @end

2 Create your checkout screen Client-side

Securely collect card information on the client with STPPaymentCardTextField, a drop-in UI component provided by the SDK.

STPPaymentCardTextField performs on-the-fly validation and formatting.

Create an instance of the card component and a “Pay” button with the following code:

import UIKit import Stripe class CheckoutViewController: UIViewController { lazy var cardTextField: STPPaymentCardTextField = { let cardTextField = STPPaymentCardTextField() return cardTextField }() lazy var payButton: UIButton = { let button = UIButton(type: .custom) button.layer.cornerRadius = 5 button.backgroundColor = .systemBlue button.titleLabel?.font = UIFont.systemFont(ofSize: 22) button.setTitle("Pay", for: .normal) button.addTarget(self, action: #selector(pay), for: .touchUpInside) return button }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white let stackView = UIStackView(arrangedSubviews: [cardTextField, payButton]) stackView.axis = .vertical stackView.spacing = 20 stackView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stackView) NSLayoutConstraint.activate([ stackView.leftAnchor.constraint(equalToSystemSpacingAfter: view.leftAnchor, multiplier: 2), view.rightAnchor.constraint(equalToSystemSpacingAfter: stackView.rightAnchor, multiplier: 2), stackView.topAnchor.constraint(equalToSystemSpacingBelow: view.topAnchor, multiplier: 2), ]) } @objc func pay() { // ... } }
#import "CheckoutViewController.h" #import <Stripe/Stripe.h> @interface CheckoutViewController () @property (nonatomic, weak) STPPaymentCardTextField *cardTextField; @property (nonatomic, weak) UIButton *payButton; @end @implementation CheckoutViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; STPPaymentCardTextField *cardTextField = [[STPPaymentCardTextField alloc] init]; self.cardTextField = cardTextField; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.layer.cornerRadius = 5; button.backgroundColor = [UIColor systemBlueColor]; button.titleLabel.font = [UIFont systemFontOfSize:22]; [button setTitle:@"Pay" forState:UIControlStateNormal]; [button addTarget:self action:@selector(pay) forControlEvents:UIControlEventTouchUpInside]; self.payButton = button; UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[cardTextField, button]]; stackView.axis = UILayoutConstraintAxisVertical; stackView.translatesAutoresizingMaskIntoConstraints = NO; stackView.spacing = 20; [self.view addSubview:stackView]; [NSLayoutConstraint activateConstraints:@[ [stackView.leftAnchor constraintEqualToSystemSpacingAfterAnchor:self.view.leftAnchor multiplier:2], [self.view.rightAnchor constraintEqualToSystemSpacingAfterAnchor:stackView.rightAnchor multiplier:2], [stackView.topAnchor constraintEqualToSystemSpacingBelowAnchor:self.view.topAnchor multiplier:2], ]]; } - (void)pay { // ... } @end

Run your app, and make sure your checkout screen shows the card component and pay button.

3 Collect card details Client-side

When your customer is ready to check out, create a PaymentMethod with the details collected by the card element.

class CheckoutViewController: UIViewController { // ... @objc func pay() { // Collect card details on the client let cardParams = cardTextField.cardParams let paymentMethodParams = STPPaymentMethodParams(card: cardParams, billingDetails: nil, metadata: nil) STPAPIClient.shared().createPaymentMethod(with: paymentMethodParams) { [weak self] paymentMethod, error in // Create PaymentMethod failed if let createError = error { self?.displayAlert(title: "Payment failed", message: createError.localizedDescription) } if let paymentMethodId = paymentMethod?.stripeId { self?.pay(withPaymentMethod: paymentMethodId) } } } func pay(withPaymentMethod paymentMethodId: String? = nil, withPaymentIntent paymentIntentId: String? = nil) { // ...continued in the next step } // ... }
@implementation CheckoutViewController // ... - (void)pay { // Collect card details on the client STPPaymentMethodCardParams *cardParams = self.cardTextField.cardParams; STPPaymentMethodParams *paymentMethodParams = [STPPaymentMethodParams paramsWithCard:cardParams billingDetails:nil metadata:nil]; [[STPAPIClient sharedClient] createPaymentMethodWithParams:paymentMethodParams completion:^(STPPaymentMethod *paymentMethod, NSError *createError) { // Create PaymentMethod failed if (createError != nil) { [self displayAlertWithTitle:@"Payment failed" message:createError.localizedDescription ?: @"" restartDemo:NO]; } else if (paymentMethod != nil) { // Create a PaymentIntent on the server with a PaymentMethod NSLog(@"Created PaymentMethod"); [self payWithPaymentMethod:paymentMethod.stripeId orPaymentIntent:nil]; } }]; } // ... @end

4 Create a PaymentIntent Client-side Server-side

Stripe uses a PaymentIntent object to represent your intent to collect payment from a customer, tracking your charge attempts and payment state changes throughout the process.

On the server

Add an endpoint that creates the PaymentIntent with the following parameters:

If you want to save the card to reuse later, create a Customer to store the PaymentMethod and pass the following additional parameters when creating the PaymentIntent:

  • customer. Set to the ID of the Customer.
  • setup_future_usage. Set to off_session to tell Stripe that you plan to reuse this PaymentMethod for off-session payments when your customer is not present. Setting this property saves the PaymentMethod to the Customer after the PaymentIntent is confirmed and any required actions from the user are complete.

Once the PaymentIntent is created, return the following to the client:

  • the PaymentIntent’s client secret
  • requiresAction: true if the PaymentIntent status is requires_action

On the client

Request a PaymentIntent from your server. This example passes the server a list of items to determine the price. Always decide how much to charge on the server side, a trusted environment, as opposed to the client. This prevents malicious customers from being able to choose their own prices.

let BackendUrl = "http://127.0.0.1:4242/" class CheckoutViewController: UIViewController { // ... func displayAlert(title: String, message: String, restartDemo: Bool = false) { // ...omitted for brevity } func pay(withPaymentMethod paymentMethodId: String? = nil, withPaymentIntent paymentIntentId: String? = nil) { // Create a PaymentIntent on the server let url = URL(string: BackendUrl + "pay")! var json: [String: Any] = [:] if let paymentMethodId = paymentMethodId {
See all 61 lines json = [ "useStripeSdk": true, "paymentMethodId": paymentMethodId, "currency": "usd", "items": [ "id": "photo_subscription" ] ] } else if let paymentIntentId = paymentIntentId { json = [ "paymentIntentId": paymentIntentId, ] } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try? JSONSerialization.data(withJSONObject: json) let task = URLSession.shared.dataTask(with: request, completionHandler: { [weak self] (data, response, requestError) in guard let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else { self?.displayAlert(title: "Payment failed", message: requestError?.localizedDescription ?? "") return } let payError = json["error"] as? String let clientSecret = json["clientSecret"] as? String let requiresAction = json["requiresAction"] as? Bool // Payment failed if let payError = payError { self?.displayAlert(title: "Payment failed", message: payError) } // Payment succeeded, no additional action required else if clientSecret != nil && (requiresAction == nil || requiresAction == false) { self?.displayAlert(title: "Payment succeeded", message: clientSecret ?? "", restartDemo: true) } // Payment requires additional action else if clientSecret != nil && requiresAction == true && self != nil { // ...continued in the next step } }) task.resume() } }
@implementation CheckoutViewController // ... - (void)displayAlertWithTitle:(NSString *)title message:(NSString *)message restartDemo:(BOOL)restartDemo { // ...omitted for brevity } - (void)payWithPaymentMethod:(NSString *)paymentMethodId orPaymentIntent:(NSString *)paymentIntentId { NSDictionary *json = @{}; if (paymentMethodId != nil) { json = @{ @"useStripeSdk": @YES, @"paymentMethodId": paymentMethodId, @"currency": @"usd", @"items": @[ @{@"id": @"photo_subscription"} ] }; } else { json = @{ @"paymentIntentId": paymentIntentId, }; } NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@pay", BackendUrl]]; NSData *body = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil]; NSMutableURLRequest *request = [[NSURLRequest requestWithURL:url] mutableCopy]; [request setHTTPMethod:@"POST"]; [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; [request setHTTPBody:body]; NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *requestError) { NSError *error = requestError; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; // Request failed if (error != nil || httpResponse.statusCode != 200) { [self displayAlertWithTitle:@"Payment failed" message:error.localizedDescription ?: @"" restartDemo:NO]; } else { NSNumber *requiresAction = json[@"requiresAction"]; NSString *clientSecret = json[@"clientSecret"]; NSString *payError = json[@"error"]; // Payment failed if (payError != nil) { [self displayAlertWithTitle:@"Payment failed" message:payError restartDemo:NO]; } // Payment succeeded else if (clientSecret != nil && (requiresAction == nil || [requiresAction isEqualToNumber:@NO])) { [self displayAlertWithTitle:@"Payment succeeded" message:clientSecret restartDemo:YES]; } // Payment requires additional actions else if (clientSecret != nil && [requiresAction isEqualToNumber:@YES]) { // ...continued in the next step } } }]; [task resume]; } // ... @end

5 Handle any next actions Client-side

Write code to handle situations that require your customer to intervene. A normal payment succeeds after you confirm it on the server in step 4. However, when the PaymentIntent requires additional action from the customer, such as authenticating with 3D Secure, this code comes into play.

In these cases, the PaymentIntent’s status is set to requires_action. On the client, pass the PaymentIntent ID to STPPaymentHandler handleNextActionForPayment. STPPaymentHandler presents view controllers, using the STPAuthenticationContext passed in, and walks the customer through authentication. For more details, read about 3D Secure authentication on iOS.

After handling required actions on the client, the status of the PaymentIntent changes to requires_confirmation. This step enables your integration to synchronously fulfill the order on your backend and return the fulfillment result to your client.

Send the PaymentIntent ID to your backend and confirm it again within one hour to finalize the payment. Otherwise, the payment attempt fails and transitions back to requires_payment_method.

class CheckoutViewController: UIViewController { // ... // Create or confirm a PaymentIntent on the server func pay(withPaymentMethod paymentMethodId: String? = nil, withPaymentIntent paymentIntentId: String? = nil) { // ... // Payment requires additional action else if clientSecret != nil && requiresAction == true && self != nil { let paymentHandler = STPPaymentHandler.shared() paymentHandler.handleNextAction(forPayment: clientSecret!, authenticationContext: self!, returnURL: nil) { status, paymentIntent, handleActionError in switch (status) { case .failed: self?.displayAlert(title: "Payment failed", message: handleActionError?.localizedDescription ?? "") break
See all 42 lines case .canceled: self?.displayAlert(title: "Payment canceled", message: handleActionError?.localizedDescription ?? "") break case .succeeded: if let paymentIntent = paymentIntent, paymentIntent.status == STPPaymentIntentStatus.requiresConfirmation { print("Re-confirming PaymentIntent after handling action") self?.pay(withPaymentIntent: paymentIntent.stripeId) } else { self?.displayAlert(title: "Payment succeeded", message: paymentIntent?.description ?? "", restartDemo: true) } break @unknown default: fatalError() break } } } // ... } } extension CheckoutViewController: STPAuthenticationContext { func authenticationPresentingViewController() -> UIViewController { return self } }
@implementation CheckoutViewController <STPAuthenticationContext> // ... - (void)payWithPaymentMethod:(NSString *)paymentMethodId orPaymentIntent:(NSString *)paymentIntentId { // ... // Payment requires additional actions else if (clientSecret != nil && [requiresAction isEqualToNumber:@YES]) { STPPaymentHandler *paymentHandler = [STPPaymentHandler sharedHandler]; [paymentHandler handleNextActionForPayment:clientSecret withAuthenticationContext:self returnURL:nil completion:^(STPPaymentHandlerActionStatus status, STPPaymentIntent *paymentIntent, NSError *handleActionError) { switch (status) { case STPPaymentHandlerActionStatusFailed: { [self displayAlertWithTitle:@"Payment failed" message:handleActionError.localizedDescription ?: @"" restartDemo:NO]; break; } case STPPaymentHandlerActionStatusCanceled: { [self displayAlertWithTitle:@"Payment canceled" message:handleActionError.localizedDescription ?: @"" restartDemo:NO]; break; } case STPPaymentHandlerActionStatusSucceeded: { // After handling a required action on the client, the status of the PaymentIntent is // requires_confirmation. You must send the PaymentIntent ID to your backend // and confirm it to finalize the payment. This step enables your integration to // synchronously fulfill the order on your backend and return the fulfillment result // to your client. if (paymentIntent.status == STPPaymentIntentStatusRequiresConfirmation) { NSLog(@"Re-confirming PaymentIntent after handling action"); [self payWithPaymentMethod:nil orPaymentIntent:paymentIntent.stripeId]; } else { [self displayAlertWithTitle:@"Payment succeeded" message:paymentIntent.description restartDemo:YES]; } break; } default: break; } }]; } // ... } # pragma mark STPAuthenticationContext - (UIViewController *)authenticationPresentingViewController { return self; } @end

6 Test the integration

By this point you should have a basic card integration that collects card details and makes a payment.

There are several test cards you can use in test mode to make sure this integration is ready. Use them with any CVC, postal code, and future expiration date.

Number Description
4242424242424242 Succeeds and immediately processes the payment.
4000002500003155 Requires authentication. Stripe will trigger a modal asking for the customer to authenticate.
4000000000009995 Always fails with a decline code of insufficient_funds.

For the full list of test cards see our guide on testing.

Next steps

Now that you have a working integration, test your payment flows using the test card numbers and PaymentMethods:

For a wider range of support and future proofing, we strongly recommend our standard integration for asynchronous payments.

This guide lets you use a single client-to-server flow to take payments, without using webhooks or processing offline events. While it may seem simpler, this integration will be difficult to scale as your business grows. Synchronous payment flows have several limitations:

  • Only supports cards: You’ll have to write more code to support ACH and popular regional payment methods separately.
  • Double-charge risk: By synchronously creating a new PaymentIntent each time your customer attempts to pay, you risk accidentally double-charging your customer. Be sure to follow best practices.
  • Manual authentication handling: If your customer has a card with 3D Secure or is subject to regulations like Strong Customer Authentication, you’ll have to make extra trips to the client.

If you don’t mind these limitations, feel free to integrate. Otherwise, use the standard integration.

1 Set up Stripe Client-side Server-side

First, you need a Stripe account. Register now.

Server-side

This integration requires endpoints on your server that talk to the Stripe API. Use our official libraries for access to the Stripe API from your server:

# Available as a gem sudo gem install stripe
# If you use bundler, you can add this line to your Gemfile gem 'stripe'
# Install through pip pip install --upgrade stripe
# Or find the Stripe package on http://pypi.python.org/pypi/stripe/
# Find the version you want to pin: # https://github.com/stripe/stripe-python/blob/master/CHANGELOG.md # Specify that version in your requirements.txt file stripe>=2.48.0,<3.0
# Install the PHP library via Composer composer require stripe/stripe-php
# Or download the source directly: https://github.com/stripe/stripe-php/releases
/* For Gradle, add the following dependency to your build.gradle and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest */ implementation "com.stripe:stripe-java:{VERSION}"
<!-- For Maven, add the following dependency to your POM and replace {VERSION} with the version number you want to use from - https://mvnrepository.com/artifact/com.stripe/stripe-java or - https://github.com/stripe/stripe-java/releases/latest --> <dependency> <groupId>com.stripe</groupId> <artifactId>stripe-java</artifactId> <version>{VERSION}</version> </dependency>
# For other environments, manually install the following JARs: # - The Stripe JAR from https://github.com/stripe/stripe-java/releases/latest # - Google Gson from https://github.com/google/gson
# Install via npm npm install --save stripe
# Install stripe-go go get -u github.com/stripe/stripe-go
// Then import the package import ( "github.com/stripe/stripe-go/v71" )
# Install via dotnet dotnet add package Stripe.net dotnet restore
# Or install via NuGet PM> Install-Package Stripe.net

Client-side

The Android SDK is open source and fully documented.

To install the SDK, add stripe-android to the dependencies block of your app/build.gradle file:

apply plugin: 'com.android.application' android { ... } dependencies { // ... // Stripe Android SDK implementation 'com.stripe:stripe-android:14.5.0' }

Configure the SDK with your Stripe publishable key so that it can make requests to the Stripe API, such as in your Application subclass:

import com.stripe.android.PaymentConfiguration class MyApp : Application() { override fun onCreate() { super.onCreate() PaymentConfiguration.init( applicationContext, "pk_test_TYooMQauvdEDq54NiTphI7jx" ) } }
import com.stripe.android.PaymentConfiguration; public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); PaymentConfiguration.init( getApplicationContext(), "pk_test_TYooMQauvdEDq54NiTphI7jx" ); } }

Our code samples also use OkHttp and GSON to make HTTP requests to a server.

2 Create your checkout screen Client-side

Securely collect card information on the client with CardInputWidget, a drop-in UI component provided by the SDK.

CardInputWidget performs on-the-fly validation and formatting.

Create an instance of the card component and a “Pay” button by adding the following to your checkout page’s layout:

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:showIn="@layout/activity_checkout" tools:context=".CheckoutActivity"> <!-- ... --> <com.stripe.android.view.CardInputWidget android:id="@+id/cardInputWidget" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp"/> <Button android:text="Pay" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/payButton" android:layout_marginTop="20dp" app:layout_constraintTop_toBottomOf="@+id/cardInputWidget" app:layout_constraintStart_toStartOf="@+id/cardInputWidget" app:layout_constraintEnd_toEndOf="@+id/cardInputWidget"/> <!-- ... --> </androidx.constraintlayout.widget.ConstraintLayout>

Run your app, and make sure your checkout screen shows the card component and pay button.

3 Collect card details Client-side

When your customer is ready to check out, create a PaymentMethod with the details collected by the card element.

class CheckoutActivity : AppCompatActivity() { private lateinit var stripe: Stripe // ... private fun pay() { val weakActivity = WeakReference<Activity>(this) // Collect card details on the client val cardInputWidget = findViewById<CardInputWidget>(R.id.cardInputWidget) val params = cardInputWidget.paymentMethodCreateParams if (params == null) { return } // Configure the SDK with your Stripe publishable key so that it can make requests to the Stripe API stripe = Stripe(applicationContext, PaymentConfiguration.getInstance(applicationContext).publishableKey) stripe.createPaymentMethod(params, object : ApiResultCallback<PaymentMethod> { // Create PaymentMethod failed override fun onError(e: Exception) { displayAlert(weakActivity.get(), "Payment failed", "Error: $e") } override fun onSuccess(result: PaymentMethod) { // Create a PaymentIntent on the server with a PaymentMethod print("Created PaymentMethod") pay(result.id, null) } }) } private fun pay(paymentMethod: String?, paymentIntent: String?) { // ... } }
public class CheckoutActivity extends AppCompatActivity { private Stripe stripe; // ... private void pay() { CardInputWidget cardInputWidget = findViewById(R.id.cardInputWidget); PaymentMethodCreateParams params = cardInputWidget.getPaymentMethodCreateParams(); if (params == null) { return; } // Configure the SDK with your Stripe publishable key so that it can make requests to the Stripe API stripe = Stripe(applicationContext, PaymentConfiguration.getInstance(applicationContext).getPublishableKey()); stripe.createPaymentMethod(params, new ApiResultCallback<PaymentMethod>() { @Override public void onSuccess(@NonNull PaymentMethod result) { // Create and confirm the PaymentIntent by calling the sample server's /pay endpoint. pay(result.id, null); } @Override public void onError(@NonNull Exception e) { } }); } private void pay(@Nullable String paymentMethodId, @Nullable String paymentIntentId) { // ...continued in the next step }

4 Create a PaymentIntent Client-side Server-side

Stripe uses a PaymentIntent object to represent your intent to collect payment from a customer, tracking your charge attempts and payment state changes throughout the process.

On the server

Add an endpoint that creates the PaymentIntent with the following parameters:

  • payment_method_id - the id of the PaymentMethod from the previous step
  • use_stripe_sdk
    • For apps that integrate with Stripe Android SDK v10.0.0 and above, the value of this parameter must be set to true
    • For apps that integrate with older SDK versions, do not pass this parameter
  • confirm - Set this to true to confirm the PaymentIntent

If you want to save the card to reuse later, create a Customer to store the PaymentMethod and pass the following additional parameters when creating the PaymentIntent:

  • customer. Set to the ID of the Customer.
  • setup_future_usage. Set to off_session to tell Stripe that you plan to reuse this PaymentMethod for off-session payments when your customer is not present. Setting this property saves the PaymentMethod to the Customer after the PaymentIntent is confirmed and any required actions from the user are complete.

Once the PaymentIntent is created, return the following to the client:

  • the PaymentIntent’s client secret
  • requiresAction: true if the PaymentIntent status is requires_action

On the client

Request a PaymentIntent from your server. This example passes the server a list of items to determine the price. Always decide how much to charge on the server side, a trusted environment, as opposed to the client. This prevents malicious customers from being able to choose their own prices.

class CheckoutActivity : AppCompatActivity() { private val backendUrl = "http://10.0.2.2:4242/" private val httpClient = OkHttpClient() private fun displayAlert(activity: Activity?, title: String, message: String, restartDemo: Boolean = false) { // omitted for brevity } private fun pay(paymentMethod: String?, paymentIntent: String?) { val weakActivity = WeakReference<Activity>(this) var json = "" if (!paymentMethod.isNullOrEmpty()) { json = """ { "useStripeSdk":true, "paymentMethodId":"$paymentMethod", "currency":"usd", "items": [ {"id":"photo_subscription"} ] } """ } else if (!paymentIntent.isNullOrEmpty()) { json = """ { "paymentIntentId":"$paymentIntent" } """ } // Create a PaymentIntent on the server val mediaType = "application/json; charset=utf-8".toMediaType() val body = json.toRequestBody(mediaType) val request = Request.Builder() .url(backendUrl + "pay") .post(body) .build() httpClient.newCall(request) .enqueue(object: Callback { override fun onFailure(call: Call, e: IOException) { displayAlert(weakActivity.get(), "Payment failed", "Error: $e") } override fun onResponse(call: Call, response: Response) { // Request failed if (!response.isSuccessful) { displayAlert(weakActivity.get(), "Payment failed", "Error: $response") } else { val responseData = response.body?.string() var responseJson = JSONObject(responseData) val payError: String? = responseJson.optString("error") val clientSecret: String? = responseJson.optString("clientSecret") val requiresAction: Boolean? = responseJson.optBoolean("requiresAction") // Payment failed if (payError != null && payError.isNotEmpty()) { displayAlert(weakActivity.get(), "Payment failed", "Error: $payError") } // Payment succeeded else if ((clientSecret != null && clientSecret.isNotEmpty()) && (requiresAction == null || requiresAction == false)) { displayAlert(weakActivity.get(), "Payment succeeded", "$clientSecret", restartDemo = true) } // Payment requires additional actions else if ((clientSecret != null && clientSecret.isNotEmpty()) && requiresAction == true) { runOnUiThread { if (weakActivity.get() != null) { // ...continued in the next step } } } } } }) } }
public class CheckoutActivity extends AppCompatActivity { private static final String BACKEND_URL = "http://10.0.2.2:4242/"; private OkHttpClient httpClient = new OkHttpClient(); private void pay(@Nullable String paymentMethodId, @Nullable String paymentIntentId) { final MediaType mediaType = MediaType.get("application/json; charset=utf-8"); final String json; if (paymentMethodId != null) { json = "{" + "\"useStripeSdk\":true," + "\"paymentMethodId\":" + "\"" + paymentMethodId + "\"," + "\"currency\":\"usd\"," + "\"items\":[" + "{\"id\":\"photo_subscription\"}" + "]" + "}"; } else { json = "{" + "\"paymentIntentId\":" + "\"" + paymentIntentId + "\"" + "}"; } RequestBody body = RequestBody.create(json, mediaType); Request request = new Request.Builder() .url(BACKEND_URL + "pay") .post(body) .build(); httpClient .newCall(request) .enqueue(new PayCallback(this, stripe)); } private void displayAlert(@NonNull String title, @NonNull String message, boolean restartDemo) { // ...omitted for brevity } private static final class PayCallback implements Callback { @NonNull private final WeakReference<CheckoutActivity> activityRef; @NonNull private final Stripe stripe; private PayCallback(@NonNull CheckoutActivity activity, @NonNull Stripe stripe) { this.activityRef = new WeakReference<>(activity); this.stripe = stripe; } @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { final CheckoutActivity activity = activityRef.get(); if (activity == null) { return; } activity.runOnUiThread(() -> { Toast.makeText(activity, "Error: " + e.toString(), Toast.LENGTH_LONG).show(); }); } @Override public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException { final CheckoutActivity activity = activityRef.get(); if (activity == null) { return; } if (!response.isSuccessful()) { activity.runOnUiThread(() -> { Toast.makeText(activity, "Error: " + response.toString(), Toast.LENGTH_LONG).show(); }); } else { Gson gson = new Gson(); Type type = new TypeToken<Map<String, String>>(){}.getType(); Map<String, String> responseMap = gson.fromJson(response.body().string(), type); String error = responseMap.get("error"); String paymentIntentClientSecret = responseMap.get("clientSecret"); String requiresAction = responseMap.get("requiresAction"); if (error != null) { activity.displayAlert("Error", error, false); } else if (paymentIntentClientSecret != null) { if ("true".equals(requiresAction)) { // ...continued in the next step } else { activity.displayAlert("Payment succeeded", paymentIntentClientSecret, true); } } } } } }

5 Handle any next actions Client-side

If the payment requires additional actions like 3D Secure authentication, the PaymentIntent’s status is set to requires_action. On the client, pass the PaymentIntent ID to authenticatePayment. This SDK presents additional activities and walks the customer through authentication. See Supporting 3D Secure Authentication on Android to learn more.

After handling a required action on the client, the status of the PaymentIntent is requires_confirmation. This step enables your integration to synchronously fulfill the order on your backend and return the fulfillment result to your client.

Send the PaymentIntent ID to your backend and confirm it again within one hour to finalize the payment. Otherwise, the payment attempt fails and transitions back to requires_payment_method.

class CheckoutActivity : AppCompatActivity() { // ... private fun pay(paymentMethod: String?, paymentIntent: String?) { // ... val activity = weakActivity.get()!! stripe.authenticatePayment(activity, clientSecret) // ... } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val weakActivity = WeakReference<Activity>(this) // Handle the result of stripe.authenticatePayment stripe.onPaymentResult(requestCode, data, object : ApiResultCallback<PaymentIntentResult> { override fun onSuccess(result: PaymentIntentResult) { val paymentIntent = result.intent val status = paymentIntent.status if (status == StripeIntent.Status.Succeeded) { val gson = GsonBuilder().setPrettyPrinting().create() displayAlert(weakActivity.get(), "Payment succeeded", gson.toJson(paymentIntent), restartDemo = true) } else if (status == StripeIntent.Status.RequiresPaymentMethod) { // Payment failed – allow retrying using a different payment method displayAlert(weakActivity.get(), "Payment failed", paymentIntent.lastPaymentError!!.message ?: "") } else if (status == StripeIntent.Status.RequiresConfirmation) { print("Re-confirming PaymentIntent after handling a required action") pay(null, paymentIntent.id) } else { displayAlert(weakActivity.get(), "Payment status unknown", "unhandled status: $status", restartDemo = true) } } override fun onError(e: Exception) { // Payment request failed – allow retrying using the same payment method displayAlert(weakActivity.get(), "Payment failed", e.toString()) } }) } }
public class CheckoutActivity extends AppCompatActivity { // ... private static final class PayCallback implements Callback { // ... activity.runOnUiThread(() -> stripe.authenticatePayment(activity, paymentIntentClientSecret)); // ... } private static final class PaymentResultCallback implements ApiResultCallback<PaymentIntentResult> { private final WeakReference<CheckoutActivity> activityRef; PaymentResultCallback(@NonNull CheckoutActivity activity) { activityRef = new WeakReference<>(activity); } @Override public void onSuccess(@NonNull PaymentIntentResult result) { final CheckoutActivity activity = activityRef.get(); if (activity == null) { return; } PaymentIntent paymentIntent = result.getIntent(); PaymentIntent.Status status = paymentIntent.getStatus(); if (status == PaymentIntent.Status.Succeeded) { // Payment completed successfully activity.runOnUiThread(() -> { Gson gson = new GsonBuilder().setPrettyPrinting().create(); activity.displayAlert("Payment completed", gson.toJson(paymentIntent), true); }); } else if (status == PaymentIntent.Status.RequiresPaymentMethod) { // Payment failed – allow retrying using a different payment method activity.runOnUiThread(() -> activity.displayAlert("Payment failed", paymentIntent.getLastPaymentError().message, false)); } else if (status == PaymentIntent.Status.RequiresConfirmation) { // After handling a required action on the client, the status of the PaymentIntent is // requires_confirmation. You must send the PaymentIntent ID to your backend // and confirm it to finalize the payment. This step enables your integration to // synchronously fulfill the order on your backend and return the fulfillment result // to your client. activity.pay(null, paymentIntent.getId()); } } @Override public void onError(@NonNull Exception e) { final CheckoutActivity activity = activityRef.get(); if (activity == null) { return; } // Payment request failed – allow retrying using the same payment method activity.runOnUiThread(() -> { activity.displayAlert("Error", e.toString(), false); }); } } }

The result of payment authentication returns to your calling Activity via Activity#onActivityResult(). Handle the result by calling Stripe#onPaymentResult() within Activity#onActivityResult(). The PaymentIntentResult returned in ApiResultCallback#onSuccess() has two getters:

  • getIntent() - a PaymentIntent object that has been retrieved after confirmation/authentication
  • getOutcome() - a StripeIntentResult.Status value that indicates the outcome of payment authentication
    • SUCCEEDED - payment authentication succeeded
    • FAILED - payment authentication failed
    • CANCELED - the customer canceled required payment authentication
    • TIMEDOUT - the payment authentication attempt timed-out

6 Test the integration

By this point you should have a basic card integration that collects card details and makes a payment.

There are several test cards you can use in test mode to make sure this integration is ready. Use them with any CVC, postal code, and future expiration date.

Number Description
4242424242424242 Succeeds and immediately processes the payment.
4000002500003155 Requires authentication. Stripe will trigger a modal asking for the customer to authenticate.
4000000000009995 Always fails with a decline code of insufficient_funds.

For the full list of test cards see our guide on testing.

Next steps

Now that you have a working integration, test your payment flows using the test card numbers and PaymentMethods:

Was this page helpful?
Questions? Contact us.
Developer tutorials on YouTube.
You can unsubscribe at any time. Read our privacy policy.