Terminal
Saving cards

Saving cards for online payments

Learn how to use your Stripe Terminal integration to create online payments.

Stripe Terminal lets you save payment methods for online use. Use an in-person card to initiate an online subscription using Billing, save payment details to a customer’s online account, or defer payment.

There are two ways to collect reusable card details with Terminal:

Saving a card from a PaymentIntent US Only

Save card details for online reuse from a customer’s in-person transaction. When you successfully process a PaymentIntent, the returned object contains a list of attempted charges in reverse chronological order—so the first is the successful one. This charge contains a generated_card ID, which represents the ID of a card PaymentMethod that can be used to charge the saved card.

The initial, in-person payment benefits from the liability shift and lower pricing given to standard Terminal payments, but subsequent payments happen online and are treated as card-not-present. For example, a gym customer pays for an initial session in person and begins a membership in the same transaction. Or a clothing store collects a customer’s email address and payment method at the checkout counter, and the customer can log in later and easily use it again.

Reading a reusable card US Only

Alternatively, collect payment details in person and defer any payment until later. Use the readReusableCard method instead of collectPaymentMethod and processPayment. For example, a customer orders flowers in your store, but you only want to charge them when the order ships.

With this method, all payments are treated as card-not-present. Even though you’re presenting a card in person, the transactions happen later and don’t benefit from the lower pricing and liability shift given to standard Terminal payments.

terminal.readReusableCard().then(function(result) { if (result.error) { // Placeholder for handling result.error } else { // Placeholder for sending result.payment_method.id to your backend. } });
async () => { const result = await terminal.readReusableCard() if (result.error) { // Placeholder for handling result.error } else { // Placeholder for sending result.payment_method.id to your backend. } }
var readCancelable: Cancelable? = nil // Action for a "Subscribe" button func subscribeAction() { let params = ReadReusableCardParameters() self.readCancelable = Terminal.shared.readReusableCard(params, delegate: self) { readResult, readError in if let error = readError { print("readReusableCard failed: \(error)") } else if let paymentMethod = readResult { print("readReusableCard succeeded") // Notify your backend to attach the PaymentMethod to a Customer APIClient.shared.attachPaymentMethod(paymentMethod.stripeId) { attachError in if let error = attachError { print("attach failed: \(error)") } else { print("attach succeeded") } } } } }
// Action for a "Subscribe" button - (void)subscribeAction { SCPReadReusableCardParameters *params = [SCPReadReusableCardParameters new]; self.readCancelable = [[SCPTerminal shared] readReusableCard:params delegate:self completion:^(SCPPaymentMethod *readResult, NSError *readError) { if (readError) { NSLog(@"readReusableCard failed: %@", readError); } else { NSLog(@"readReusableCard succeeded"); // Notify your backend to attach the PaymentMethod to a Customer [[APPAPIClient shared] attachPaymentMethod:readResult.stripeId completion:^(NSError *attachError) { if (attachError) { NSLog(@"attach failed: %@", attachError); } else { NSLog(@"attach succeeded"); } }]; } }]; }
ReadReusableCardParameters params = new ReadReusableCardParameters.Builder() .build(); Cancelable cancelable = Terminal.getInstance().readReusableCard(params, new MyReaderDisplayListener(), new PaymentMethodCallback() { @Override public void onSuccess(PaymentMethod paymentMethod) { // Placeholder for sending paymentMethod.id to your backend. } @Override public void onFailure(TerminalException exception) { // Placeholder for handling the exception } });
val params = ReadReusableCardParameters.Builder() .build() val cancelable = Terminal.getInstance().readReusableCard(params, MyReaderDisplayListener(), object : PaymentMethodCallback { override fun onSuccess(paymentMethod: PaymentMethod) { // Placeholder for sending paymentMethod.id to your backend. } override fun onFailure(exception: TerminalException) { // Placeholder for handling the exception } })

The connected reader waits for a card to be presented. When the customer presents a card to the reader, readReusableCard collects encrypted card data and tokenizes it as a PaymentMethod but does not create any payments.

You can cancel reading a card using the Cancelable object returned by the iOS or Android SDK, or calling cancelReadReusableCard in the JavaScript SDK.

Charging a saved card US Only

You can use the generated card PaymentMethod to charge customers later.

For one-time use, create a PaymentIntent and attach the saved PaymentMethod. Once you attach the PaymentMethod you can’t reuse it for another payment unless you collect payment details again by either saving a card from a PaymentIntent or reading a reusable card.

curl https://api.stripe.com/v1/payment_intents \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d "payment_method_types[]"=card \ -d amount=1099 \ -d currency=usd \ -d payment_method="{{PAYMENT_METHOD_ID}}"
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' Stripe::PaymentIntent.create({ payment_method_types: ['card'], amount: 1099, currency: 'usd', payment_method: '{{PAYMENT_METHOD_ID}}', })
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' stripe.PaymentIntent.create( payment_method_types=['card'], amount=1099, currency='usd', payment_method='{{PAYMENT_METHOD_ID}}', )
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); \Stripe\PaymentIntent::create([ 'payment_method_types' => ['card'], 'amount' => 1099, 'currency' => 'usd', 'payment_method' => '{{PAYMENT_METHOD_ID}}', ]);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .addPaymentMethodType("card") .setPaymentMethod("{{PAYMENT_METHOD_ID}}") .build(); PaymentIntent.create(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const paymentIntent = await stripe.paymentIntents.create({ payment_method_types: ['card'], amount: 1099, currency: 'usd', payment_method: '{{PAYMENT_METHOD_ID}}', });
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.PaymentIntentParams{ PaymentMethodTypes: stripe.StringSlice([]string{ "card", }), Amount: stripe.Int64(1099), Currency: stripe.String(string(stripe.CurrencyUSD)), PaymentMethod: stripe.String("{{PAYMENT_METHOD_ID}}"), } paymentintent.New(params)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new PaymentIntentCreateOptions { PaymentMethodTypes = new List<string> { "card" }, Amount = 1099, Currency = "usd", PaymentMethod = "{{PAYMENT_METHOD_ID}}", }; var service = new PaymentIntentService(); service.Create(options);

For future reuse, like recurring payments, attach the PaymentMethod to a Customer first.

curl https://api.stripe.com/v1/payment_methods/{{PAYMENT_METHOD_ID}}/attach \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d customer="{{CUSTOMER_ID}}"
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' Stripe::PaymentMethod.attach( '{{PAYMENT_METHOD_ID}}', { customer: '{{CUSTOMER_ID}}', } )
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' stripe.PaymentMethod.attach('{{PAYMENT_METHOD_ID}}', customer='{{CUSTOMER_ID}}')
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); $payment_method = \Stripe\PaymentMethod::retrieve('{{PAYMENT_METHOD_ID}}'); $payment_method->attach(['customer' => '{{CUSTOMER_ID}}']);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; PaymentMethod paymentMethod = PaymentMethod.retrieve("{{PAYMENT_METHOD_ID}}"); PaymentMethodAttachParams params = PaymentMethodAttachParams.builder() .setCustomer("{{CUSTOMER_ID}}") .build(); paymentMethod.attach(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const paymentMethod = await stripe.paymentMethods.attach( '{{PAYMENT_METHOD_ID}}', { customer: '{{CUSTOMER_ID}}', } );
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.PaymentMethodAttachParams{ Customer: stripe.String("{{CUSTOMER_ID}}"), } paymentMethod, err := paymentmethod.Attach("{{PAYMENT_METHOD_ID}}", params)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new PaymentMethodAttachOptions { Customer = "{{CUSTOMER_ID}}", }; var service = new PaymentMethodService(); service.Attach("{{PAYMENT_METHOD_ID}}", options);

Later, when creating a PaymentIntent for this Customer, provide both the Customer and the PaymentMethod.

curl https://api.stripe.com/v1/payment_intents \ -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \ -d "payment_method_types[]"=card \ -d amount=1099 \ -d currency=usd \ -d customer="{{CUSTOMER_ID}}" \ -d payment_method="{{PAYMENT_METHOD_ID}}"
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' Stripe::PaymentIntent.create({ payment_method_types: ['card'], amount: 1099, currency: 'usd', customer: '{{CUSTOMER_ID}}', payment_method: '{{PAYMENT_METHOD_ID}}', })
# Set your secret key. Remember to switch to your live secret key in production! # See your keys here: https://dashboard.stripe.com/account/apikeys stripe.api_key = 'sk_test_4eC39HqLyjWDarjtT1zdp7dc' stripe.PaymentIntent.create( payment_method_types=['card'], amount=1099, currency='usd', customer='{{CUSTOMER_ID}}', payment_method='{{PAYMENT_METHOD_ID}}', )
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys \Stripe\Stripe::setApiKey('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); \Stripe\PaymentIntent::create([ 'payment_method_types' => ['card'], 'amount' => 1099, 'currency' => 'usd', 'customer' => '{{CUSTOMER_ID}}', 'payment_method' => '{{PAYMENT_METHOD_ID}}', ]);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys Stripe.apiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setAmount(1099L) .setCurrency("usd") .addPaymentMethodType("card") .setCustomer("{{CUSTOMER_ID}}") .setPaymentMethod("{{PAYMENT_METHOD_ID}}") .build(); PaymentIntent.create(params);
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys const stripe = require('stripe')('sk_test_4eC39HqLyjWDarjtT1zdp7dc'); const paymentIntent = await stripe.paymentIntents.create({ payment_method_types: ['card'], amount: 1099, currency: 'usd', customer: '{{CUSTOMER_ID}}', payment_method: '{{PAYMENT_METHOD_ID}}' });
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" params := &stripe.PaymentIntentParams{ PaymentMethodTypes: stripe.StringSlice([]string{ "card", }), Amount: stripe.Int64(1099), Currency: stripe.String(string(stripe.CurrencyUSD)), Customer: stripe.String("{{CUSTOMER_ID}}"), PaymentMethod: stripe.String("{{PAYMENT_METHOD_ID}}"), } paymentintent.New(params)
// Set your secret key. Remember to switch to your live secret key in production! // See your keys here: https://dashboard.stripe.com/account/apikeys StripeConfiguration.ApiKey = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"; var options = new PaymentIntentCreateOptions { PaymentMethodTypes = new List<string> { "card" }, Amount = 1099, Currency = "usd", Customer = "{{CUSTOMER_ID}}", PaymentMethod = "{{PAYMENT_METHOD_ID}}", }; var service = new PaymentIntentService(); service.Create(options);

Handling events in your app

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 a transaction begins, the SDK passes a ReaderInputOptions value to your app’s reader display handler, denoting the acceptable types of input (e.g., Swipe, Insert, Tap). In your app’s checkout UI, prompt the user to present a card using one of these options.

During the transaction, the SDK might request your app to display additional prompts (e.g., Retry Card) to your user by passing a ReaderDisplayMessage value to your app’s reader display handler. Make sure your checkout UI displays these messages to the user.

// MARK: ReaderDisplayDelegate func terminal(_ terminal: Terminal, didRequestReaderInput inputOptions: ReaderInputOptions = []) { readerMessageLabel.text = Terminal.stringFromReaderInputOptions(inputOptions) } func terminal(_ terminal: Terminal, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) { readerMessageLabel.text = Terminal.stringFromReaderDisplayMessage(displayMessage) }
#pragma mark - SCPReaderDisplayDelegate - (void)terminal:(SCPTerminal *)terminal didRequestReaderInput:(SCPReaderInputOptions)inputOptions { self.readerMessageLabel.text = [SCPTerminal stringFromReaderInputOptions:inputOptions]; } - (void)terminal:(SCPTerminal *)terminal didRequestReaderDisplayMessage:(SCPReaderDisplayMessage)displayMessage { self.readerMessageLabel.text = [SCPTerminal stringFromReaderDisplayMessage:displayMessage]; }
@Override public void onRequestReaderInput(ReaderInputOptions options) { // Placeholder for updating your app's checkout UI Toast.makeText(getActivity(), options.toString(), Toast.LENGTH_SHORT).show(); } @Override public void onRequestReaderDisplayMessage(ReaderDisplayMessage message) { Toast.makeText(getActivity(), message.toString(), Toast.LENGTH_SHORT).show(); }
override fun onRequestReaderInput(options: ReaderInputOptions) { // Placeholder for updating your app's checkout UI Toast.makeText(activity, options.toString(), Toast.LENGTH_SHORT).show() } override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { Toast.makeText(activity, message.toString(), Toast.LENGTH_SHORT).show() }

Tracking customer behavior with card fingerprints

The Stripe API makes it easy to recognize repeat customers across online and retail channels by correlating transactions by the same card. Like card payment methods, each card_present payment method has a fingerprint attribute that uniquely identifies a particular card number. Note that cards from mobile wallets like Apple Pay or Google Pay do not share a fingerprint with cards used online.

Starting with API version 2018-01-23, Connect platforms see a fingerprint on card_present and card PaymentMethods that is uniform across all connected accounts. You can use this fingerprint to look up charges across your platform from a particular card.

Was this page helpful?
Questions? Contact us.
Developer tutorials on YouTube.