Collecting Payments Beta

    Prepare your application and backend to collect payments using the Stripe Terminal SDK.

    Collecting payments with Stripe Terminal requires writing a payment flow in your application. Use the Stripe Terminal SDK to create and update a PaymentIntent, an object representing a single payment session.

    Designed to be robust to failures, the Terminal integration splits the payment process into several steps, each of which can be retried safely:

    1. Create a PaymentIntent
    2. Collect a payment method
    3. Process the payment
    4. Capture the PaymentIntent

    Authorization on the customer’s card takes place in Step 3, when the SDK processes the payment.

    Step 1: Create a Payment Intent Client-side Server-side

    The first step in collecting payments is starting the payment flow. When a customer begins checking out, your application must create a PaymentIntent object. This represents a new payment session on Stripe.

    With the iOS and Android SDKs, you can create a PaymentIntent on the client or server. The JavaScript SDK only supports server-side creation.

    Client-side

    Create a PaymentIntent from your client using the iOS or Android SDK:

    Client-side PaymentIntent creation is possible with the iOS or Android SDKs.
    If you’re using the Javascript SDK for Stripe Terminal, you’ll need to create
    PaymentIntents server-side.
    let params = PaymentIntentParameters(amount: 1000, currency: "usd")
    Terminal.shared.createPaymentIntent(params) { paymentIntent in
        // Placeholder for continuing with the created PaymentIntent
    }
    PaymentIntentParameters params = new PaymentIntentParameters.Builder()
      .setAmount(100)
      .setCurrency("usd")
      .build();
    Terminal.getInstance().createPaymentIntent(params, new PaymentIntentCallback() {
      // Placeholder for continuing payment with the created PaymentIntent
    })

    Server-side

    The JavaScript SDK requires you to create the PaymentIntent on your server. For iOS or Android, you might choose to create the PaymentIntent on your server if the information required to start a payment isn’t readily available in your app.

    The following example shows how to create a PaymentIntent on your server:

    curl https://api.stripe.com/v1/payment_intents \
      -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
      -d amount=1000 \
      -d currency=usd \
      -d payment_method_types[]=card_present \
      -d capture_method=manual
    
    
    require 'stripe'
    Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
    
    intent = Stripe::PaymentIntent.create({
      amount: 1000,
      currency: 'usd',
      payment_method_types: ['card_present'],
      capture_method: 'manual',
    }, {
      idempotency_key: my_order_id,
    })
    
    import stripe
    stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
    
    stripe.PaymentIntent.create(
      amount=1000,
      currency='usd',
      payment_method_types=['card_present'],
      capture_method='manual'
    )
    
    \Stripe\Stripe::setApiKey("sk_test_4eC39HqLyjWDarjtT1zdp7dc");
    
    \Stripe\PaymentIntent::create([
      "amount" => 1000,
      "currency" => "usd",
      "payment_method_types" => ["card_present"],
      "capture_method" => "manual",
    ]);
    
    import stripe
    Stripe.apiKey = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
    
    final List<String> allowedSourceTypes = new ArrayList<String>();
    allowedSourceTypes.add("card_present");
    final Map<String, Object> params = new HashMap<>();
    params.put("payment_method_types", allowedSourceTypes);
    params.put("amount", 1000);
    params.put("currency", "usd");
    params.put("capture_method", "manual");
    
    final PaymentIntent paymentIntent = PaymentIntent.create(params);
    
    var stripe = require("stripe")("sk_test_4eC39HqLyjWDarjtT1zdp7dc")
    
    (async function() {
      const intent = await stripe.paymentIntents.create({
        amount: 1000,
        currency: 'usd',
        payment_method_types: ['card_present'],
        capture_method: 'manual',
      });
    })();
    
    import "github.com/stripe/stripe-go"
    
    stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"
    
    params := &stripe.PaymentIntentParams{
      Amount: stripe.Int64(1000),
      Currency: stripe.String(string(stripe.CurrencyUSD)),
      AllowedSourceTypes: stripe.StringSlice([]string{
        "card",
      }),
      CaptureMethod: stripe.String("manual"),
    }
    paymentintent.New(params)
    

    For Terminal payments, the payment_method_types parameter must include card_present. To precisely control the payment flow, only a capture_method of manual is allowed for card_present PaymentIntents.

    After creating a PaymentIntent on your server, you can interact with the object from the client by passing its client_secret to your application. See the Payment Intents API reference for details.

    For JavaScript, the client_secret is all you need in your client-side application to proceed to payment method collection. For iOS or Android, you use the PaymentIntent itself to call collectPaymentMethod, so use the client_secret to retrieve the PaymentIntent:

    Client-side PaymentIntent creation is possible with the iOS or Android SDKs.
    If you’re using the Javascript SDK for Stripe Terminal, you’ll need to create
    PaymentIntents server-side.
    let paymentIntent = Terminal.shared.retrievePaymentIntent(clientSecret) { intent, error in
        // Placeholder for continuing payment with the retrieved PaymentIntent
    }
    Terminal.getInstance().retrievePaymentIntent(clientSecret, new PaymentIntentCallback() {
        // Placeholder for continuing payment with the retrieved PaymentIntent
    })

    Step 2: Collect a payment method Client-side

    After you’ve created a PaymentIntent, the next step is to collect a payment method with the SDK.

    When your app is ready to begin collecting an in-person card payment, call collectPaymentMethod, and the connected reader begins waiting for a card.

    async () => {
      // clientSecret is the client_secret from the PaymentIntent you created in Step 1.
      const result = await terminal.collectPaymentMethod(clientSecret);
      if (result.error) {
        // Placeholder for handling the error
      } else {
        // Placeholder for processing the updated PaymentIntent
      }
    }
    // clientSecret is the client_secret from the PaymentIntent you created in Step 1.
    terminal.collectPaymentMethod(clientSecret).then(function(result) {
      if (result.error) {
        // Placeholder for handling the error
      } else {
        // Placeholder for processing the updated PaymentIntent
      }
    });
    let cancelable = Terminal.shared.collectPaymentMethod(paymentIntent, delegate: self) { intent, error in
        if let intent = intent {
            // Placeholder for processing the updated PaymentIntent
        }
        else if let error = error {
            // Placeholder for handling the error
        }
    }
    Cancelable cancelable = Terminal.getInstance().collectPaymentMethod(paymentIntent,
      new MyReaderListener(),
      new PaymentIntentCallback() {
        @Override
        public void onSuccess(PaymentIntent intent) {
          // Placeholder for processing the updated PaymentIntent
        }
    
        @Override
        public void onFailure(TerminalException e) {
          // Placeholder for handling the error
        }
      });

    This method collects encrypted payment method data using the connected card reader, and associates the encrypted data with the local PaymentIntent. There’s no authorization or updates to the PaymentIntent API object until the next step, processing.

    Handling events

    When collecting a payment method using a reader like the BBPOS Chipper 2X BT, without a built-in display, your app must be able to display events from the payment method collection process to users. These events help users successfully collect payments (e.g., retrying a card, trying a different card, or using a different read method).

    When reading a card begins, your app’s listener will be passed a ReaderInputOptions object denoting the acceptable types of input (e.g., Swipe, Insert, Tap). Your app’s checkout UI should prompt the user to present a card using one of these input options.

    The JavaScript SDK only supports the Verifone P400, which has a built-in display.
    You app does not need to display events from the payment method collection process
    to users, as these will be displayed on the reader.
    func terminal(_ terminal: Terminal, didRequestReaderInput options: ReaderInputOptions) {
        // Placeholder for updating your app's checkout UI
        inputOptionsLabel.text = Terminal.stringFromReaderInputOptions(options)
    }
    @Override
    public void onRequestReaderInput(ReaderInputOptions options) {
        // Placeholder for updating your app's checkout UI
        Toast.makeText(getActivity(), options.toString(), Toast.LENGTH_SHORT).show();
    }

    During the reading process, the SDK might request your app to display additional prompts (e.g., Retry Card) to your user via the RequestReaderDisplayMessage method. Make sure your checkout UI displays these prompts to the user.

    The JavaScript SDK only supports the Verifone P400, which has a built-in display.
    You app does not need to display events from the payment method collection process
    to users, as these will be displayed on the reader
    func terminal(terminal: Terminal, didRequestReaderDisplayMessage message: ReaderDisplayMessage) {
        // Placeholder for displaying the message
        messageLabel.text = Terminal.stringFromReaderDisplayMessage(message)
    }
    @Override
    public void onRequestReaderDisplayMessage(ReaderDisplayMessage message) {
        Toast.makeText(getActivity(), prompt.toString(), Toast.LENGTH_SHORT).show();
    }

    Step 3: Process the payment Client-side

    After successfully collecting a payment method from the customer, the next step is to process the payment with the SDK. You can either process automatically or display a confirmation screen, where the customer can choose to proceed with the payment or cancel (e.g., to pay with cash, or use a different payment method).

    When you’re ready to proceed with the payment, call processPayment with the updated PaymentIntent from Step 2.

    async {
      const result = await terminal.processPayment(paymentIntent);
      if (result.error) {
        // Placeholder for handling the error
      } else if (result.paymentIntent) {
        // Placeholder for notifying your backend to capture the PaymentIntent
      }
    }
    terminal.processPayment(paymentIntent).then(function(result) {
      if (result.error) {
        // Placeholder for handling the error
      } else if (confirmResult.paymentIntent) {
        // Placeholder for notifying your backend to capture the PaymentIntent
      }
    });
    Terminal.shared.processPayment(paymentIntent) { intent, error in
        if let intent = intent {
            // Placeholder for notifying your backend to capture the PaymentIntent
        }
        else if let error = error {
            // Placeholder for handling the error
        }
    }
    Cancelable cancelable = Terminal.getInstance().processPayment(paymentIntent,
      new PaymentIntentCallback() {
        @Override
        public void onSuccess(PaymentIntent intent) {
          // Placeholder for notifying your backend to capture the PaymentIntent
        }
    
        @Override
        public void onFailure(TerminalException e) {
          // Placeholder for handling the error
        }
    });

    Handling processing failures

    When processing a PaymentIntent fails, the SDK returns an error that includes the updated PaymentIntent. Your application should inspect the PaymentIntent to decide how to deal with the error.

    PaymentIntent Status Meaning Resolution
    requires_payment_method Payment method declined Try collecting a different payment method by calling collectPaymentMethod again with the same PaymentIntent.
    requires_confirmation Temporary connectivity problem Call processPayment again with the same PaymentIntent to retry the request.
    PaymentIntent is nil Request to Stripe timed out, unknown PaymentIntent status Retry processing the original PaymentIntent. Do not create a new one, as that could result in multiple authorizations for the cardholder.

    If you encounter multiple, consecutive timeouts, there might be a problem with your connectivity. Make sure that your app is able to communicate with the internet.

    Step 4: Capture the Payment Intent Server-side

    Stripe Terminal uses a two-step process to prevent unintended and duplicate payments. When the SDK returns a processed PaymentIntent to your app, the payment is authorized but not captured. Read our auth and capture documentation for more information about the difference.

    When your app receives a processed PaymentIntent from the SDK, make sure it notifies your backend to capture the PaymentIntent. Create an endpoint on your backend that accepts a PaymentIntent ID and sends a request to the Stripe API to capture it:

    require 'stripe'
    Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
    
    # Using Sinatra
    post '/capture/:payment_intent_id' do
      payment_intent = Stripe::PaymentIntent.retrieve(params["payment_intent_id"])
      payment_intent.capture
      # Optionally reconcile the PaymentIntent with your internal order system.
      status 200
    end
    
    from django.http import HttpResponse
    import stripe
    stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc'
    
    # Using Django
    def my_payment_intent_capture_view(request, payment_intent_id):
      payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
      payment_intent.capture()
      # Optionally reconcile the PaymentIntent with your internal order system.
      return HttpResponse('')
    
    // Using the Spark framework (http://sparkjava.com)
    public Object handle(Request request, Response response) {
      String paymentIntentId = request.queryParams("paymentIntentId");
      PaymentIntent paymentIntent = PaymentIntent.retrieve(paymentIntentId);
      paymentIntent.capture();
      // Optionally reconcile the PaymentIntent with your internal order system.
      response.status(200);
      return "";
    }
    
    // Using Express
    const app = require('express')();
    const bodyParser = require('body-parser');
    const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc');
    
    // Retrieve the raw body json and match all content types:
    app.use(bodyParser.json({ type: '*/*' }));
    
    app.post('/capture', async function(request, response) {
      var payment_intent_id = request.body.payment_intent_id;
      await stripe.paymentIntents.capture(payment_intent_id);
      // Optionally reconcile the PaymentIntent with your internal order system.
      response.send(200);
    });
    
    package main
    
    import (
        "github.com/stripe/stripe-go"
        "github.com/stripe/stripe-go/paymentintent"
        "net/http"
    )
    
    func main() {
        stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"
    
        http.HandleFunc("/capture", func(w http.ResponseWriter, req *http.Request) {
            err := req.ParseForm()
            if err != nil {
                w.WriteHeader(http.StatusBadRequest)
                return
            }
            payment_intent_id := req.Form.Get("payment_intent_id")
            payment_intent_params := &stripe.PaymentIntentCaptureParams{}
            paymentintent.Capture(payment_intent_id, payment_intent_params)
            // Optionally reconcile the PaymentIntent with your internal order system.
    
            w.WriteHeader(http.StatusOK)
        })
    
        http.ListenAndServe(":4567", nil)
    }
    
    

    Reconciling payments

    To monitor the payments activity of your business, you may want to reconcile PaymentIntents with your internal orders system on your server at the end of a day’s activity.

    A PaymentIntent that retains a requires_capture status may represent two things:

    Unnecessary authorization on your customer’s card statement

    • Cause: User abandons your app’s checkout flow in the middle of a transaction
    • Solution: If the uncaptured PaymentIntent is not associated with a completed order on your server, you can cancel it. A canceled PaymentIntent can no longer be used to perform charges.

    Incomplete collection of funds from a customer

    • Cause: Failure of the request from your app notifying your backend to capture the PaymentIntent
    • Solution: If the uncaptured PaymentIntent is associated with a completed order on your server, and no other payment has been taken for the order (e.g., a cash payment), you can capture it.

    Next steps

    Congratulations! Your Terminal integration is now set up to collect in-person payments. Next, configure the customer-facing checkout experience in your app, or test your current integration with a physical reader.

    Questions?

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

    Was this page helpful? Yes No

    Send

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

    On this page