Common iOS Workflows Invite Only
Learn about common workflows with the Stripe Terminal iOS SDK.
Handling reader input
To use the collectPaymentMethod
and readSource
methods, you must provide an implementation of ReaderInputDelegate to handle reader input events.
When reading a card begins, the didBeginWaitingForReaderInput
method is called with a set of input options (e.g., Swipe, Insert, Tap). Your app should prompt the user to present a payment method using one of these input options.
func terminal(_ terminal: Terminal, didBeginWaitingForReaderInput options: ReaderInputOptions) {
// Placeholder for displaying the available input options
inputOptionsLabel.text = Terminal.stringFromReaderInputOptions(options)
}
As reading a card proceeds, the SDK might request that additional prompts be displayed to your user via the didRequestReaderInputPrompt
method (e.g., Retry Card. Your app should display these prompts.
func terminal(terminal: Terminal, didRequestReaderInputPrompt prompt: ReaderInputPrompt) {
// Placeholder for displaying the prompt
promptLabel.text = Terminal.stringFromReaderInputPrompt(prompt)
}
To detect when a card is inserted or removed from the reader, you can use the didReportReaderEvent
method on TerminalDelegate
.
func terminal(_ terminal: Terminal, didReportReaderEvent event: ReaderEvent, info: [AnyHashable : Any]?) {
switch (event) {
case .cardInserted:
break;
case .cardRemoved:
break;
}
}
Status changes
Before starting a payment, you can use the paymentStatus property to determine whether the SDK is ready to begin a payment. While the payment is in progress, you can use the didChangePaymentStatus
delegate method to update your UI as the payment proceeds.
func terminal(_ terminal: Terminal, didChangePaymentStatus status: PaymentStatus) {
// Placeholder for displaying the payment status
label.text = Terminal.stringFromPaymentStatus(status)
}
To determine whether your app is connected to a reader, you can use the connectionStatus property, or the didChangeConnectionStatus
delegate method. For more information about the connected reader (e.g., its battery level), use the connectedReader
property.
func terminal(_ terminal: Terminal, didChangeConnectionStatus status: ConnectionStatus) {
// Placeholder for displaying the connection status
label.text = Terminal.stringFromConnectionStatus(status)
}
To detect when your app unexpectedly disconnects from a reader, use the didDisconnectUnexpectedlyFromReader
method. In your implementation of this method, notify your user that the reader disconnected.
In these cases, you can also call discoverReaders
to begin scanning for readers, and can attempt to automatically reconnect to the disconnected reader. Be sure to either set a timeout or make it possible to cancel calls to discoverReaders
.
func terminal(_ terminal: Terminal, didDisconnectUnexpectedlyFromReader reader: Reader) {
self.reconnectSerialNumber = reader.serialNumber
let config = DiscoveryConfiguration()
// be sure to set a timeout, or make it possible to cancel discoverReaders
config.timeout = 10
Terminal.shared.discoverReaders(config, delegate: self) { error in
if let error = error {
print("failed to reconnect: \(error)")
}
}
}
Refunds
You can cancel a PaymentIntent before it has been captured using the cancelPaymentIntent
method. The iOS SDK supports canceling PaymentIntents directly, but you can also cancel the PaymentIntent on your server. Canceling a PaymentIntent releases all uncaptured funds. A canceled PaymentIntent can no longer be used to perform charges.
Terminal.shared.cancel(paymentIntent) { intent, error in
if let intent = intent {
self.print("Canceled intent: \(intent)")
}
else if let error = error {
self.print("Failed to cancel intent: \(error)")
}
}
To refund a PaymentIntent that has already been captured, you must refund the charge created by the PaymentIntent using the refunds API on your server.
Server-side PaymentIntent creation
If the information required to create a PaymentIntent isn’t readily available in your application, you can also create the PaymentIntent on your server, and retrieve it via the SDK.
First, create a PaymentIntent object. A PaymentIntent represents your intent to collect payment from a customer, tracking the lifecycle of the payment process through each step.
Each PaymentIntent typically correlates with a single cart or customer session in your application. When you create a PaymentIntent, specify the currency, the permitted source types, and the amount to collect from the customer.
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: []*string{
stripe.String("card"),
},
CaptureMethod: stripe.String("manual"),
}
paymentIntent.New(params)
The payment_method_types
parameter must include card_present
for Stripe Terminal payments. To ensure that a card present payment is not unintentionally captured, only a capture_method
of manual
is allowed for card_present
PaymentIntents.
You can choose to cancel stale, uncaptured PaymentIntents. Canceling a PaymentIntent releases all uncaptured funds. A canceled PaymentIntent can no longer be used to perform charges.
After creating a PaymentIntent on your server, you can interact with the PaymentIntent client-side by exposing its client_secret
to your application.
let paymentIntent = Terminal.shared.retrievePaymentIntent(clientSecret) { intent, error in
if let intent = intent {
Terminal.shared.collectPaymentMethod( ... )
}
else if let error = error {
self.handleError(error)
}
}
Reading a card without charging
You can read a card without charging it using the readSource
method. This can be useful if you want to re-use the payment method online. Like attachSource
, this method requires a ReadCardDelegate
to respond to events related to reading a card.
If reading a source fails, the completion block is called with an error. If reading a source succeeds, the completion block is called with a CardPresentSource
, which you can send to your backend for further processing.
let params = ReadSourceParameters()
let cancelable = Terminal.shared.readSource(params, delegate: self) { source, error in
if let source = source {
// Placeholder for sending the source to your server for processing
self.apiClient.saveCardPresentSource(sourceId: source.stripeId)
}
else if let error = error {
self.print("Failed to read source: \(error)")
}
}
Saving a card present source
You cannot charge or reuse a card_present
source created by the Stripe Terminal SDK. To save a card_present
source for reuse online (e.g., for a subscription), you must convert it to a card
source on your backend.
curl https://api.stripe.com/v1/sources \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d type=card \
-d card[card_present_source]="{SOURCE ID}"
Switching Stripe accounts
To connect to a reader, the SDK uses the fetchConnectionToken
method (defined in your app) to fetch a connection token, if it does not already have one. It then uses the connection token and reader information to create a reader session.
To switch Stripe accounts in your app, you’ll need to:
- Disconnect from the currently connected reader, if one is connected.
- Call clearCachedCredentials to clear the connection token & reader session.
- Configure your app to use the new account.
- Connect to a reader.
Receipts
If you provide a receiptEmail
when creating a PaymentIntent, a receipt is emailed to your customer when the PaymentIntent is captured. If you need to print a physical receipt, you can use the receiptData
property on the confirmed PaymentIntent’s cardPresentSource
to get a dictionary containing receipt fields for the transaction.
Field | Name | Requirement |
---|---|---|
applicationPreferredName |
Application Name | Required |
dedicatedFileName |
AID | Required |
authorizationResponseCode |
ARC | Optional |
applicationCryptogram |
Application Cryptogram | Optional |
terminalVerificationResults |
TVR | Optional |
transactionStatusInformation |
TSI | Optional |
Using Connect
Stripe Terminal supports several approaches for creating charges on behalf of a connected account. One approach is a direct charge, where the connected account is responsible for the cost of the Stripe fees, refunds, and chargebacks. To perform a direct charge, provide the connected account’s ID while creating the PaymentIntent:
curl https://api.stripe.com/v1/payment_intents \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d amount=1099 \
-d currency=usd \
-d payment_method_types[]=card_present \
-d capture_method=manual \
-H "Stripe-Account: {CONNECTED_ACCOUNT_ID}"
Another approach is a destination charge, in which the payment creates a transfer to a connected account automatically and sets a connected account as the business of record for the payment.
The transfer_data[destination]
is the ID of the connected account that should receive the transfer.
The on_behalf_of
parameter is the ID of the connected account to use as the business of record for the payment. When on_behalf_of
is set, Stripe automatically:
- Settles charges in the country of the specified account, thereby minimizing declines and avoiding currency conversions
- Uses the fee structure for the connected account’s country
- Lists the connected account’s address and phone number on the customer’s credit card statement, as opposed to the platform’s address and phone number. This only occurs if the account is in a different country than the platform.
Finally, you may withhold an application fee by providing the application_fee_amount parameter:
curl https://api.stripe.com/v1/payment_intents \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d amount=1099 \
-d currency=usd \
-d payment_method_types[]=card_present \
-d capture_method=manual \
-d application_fee_amount=200 \
-d on_behalf_of="{CONNECTED_ACCOUNT_ID}" \
-d transfer_data[destination]="{CONNECTED_ACCOUNT_ID}"
Grouping Terminal Objects
When using destination charges, objects belonging to your connected accounts will be created on your platform account. To group Terminal API objects on your platform account by connected account, set the operator_account
parameter to the connected account. This parameter can be used on all Terminal API endpoints.
Locations
To create a Location on your platform account that is also associated with a connected account, use the optional operator_account
parameter.
curl https://api.stripe.com/v1/terminal/locations \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d display_name=HQ \
-d operator_account="{CONNECTED_ACCOUNT_ID}"
Readers
When registering a reader, you can use the optional location
parameter to assign it to a specific location. If the location was created with an operator_account, you must specify the same operator_account when registering the reader.
curl https://api.stripe.com/v1/terminal/readers \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d registration_code="{REGISTRATION_CODE" \
-d label="Test Reader" \
-d location="{LOCATION_ID}" \
-d operator_account="{CONNECTED_ACCOUNT_ID}"
Note that if you don’t specify a location
when registering a reader it will be assigned to your account’s default location. If you don’t specify a location
and you do specify an operator_account
, the reader will be assigned to the connected account’s default location.
Connection Tokens
When creating a connection token for the Terminal SDK, set operator_account
to be the connected account operating your application.
curl https://api.stripe.com/v1/terminal/connection_tokens \
-u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
-d operator_account="{CONNECTED_ACCOUNT_ID}"
The SDK will inherit the operator account that was specified when creating the connection token. You do not need to specify an operator account in any client-side calls to the SDK.
Reader updates
You can update a reader from your app using the SDK’s checkForReaderSoftwareUpdate
method. For a reference implementation, you can use our example app on GitHub, which includes a workflow for updating readers.
To check for available any available reader software updates, call the checkForReaderSoftwareUpdate
method. Your app must implement the ReaderSoftwareUpdateDelegate
protocol. If an update is available, the readerSoftwareUpdateAvailable
delegate method is called with a ReaderSoftwareUpdate
object containing additional details about the update, including an estimate of how long the update will take. Your app should notify your user that an update is available, and display a prompt to continue with the update.
After the user selects an option, use the installUpdate
block to proceed. If the user chooses to proceed with the update, call installUpdate(true)
begin the installation. If the user does not wish to proceed, call installUpdate(false)
.
As the update proceeds, your app should block the user from leaving the page, and instruct them to keep the reader in range and powered on until the update is complete. The SDK will notify your delegate when the update is complete, via the didCompleteReaderSoftwareUpdate:
delegate method.
func terminal(_ terminal: Terminal, readerSoftwareUpdateAvailable update: ReaderSoftwareUpdate, installUpdate: @escaping BoolCompletionBlock) {
// Ask the user if they want to proceed with the update.
// If they want to proceed, call:
installUpdate(true)
}
func terminal(_ terminal: Terminal, didReportReaderUpdateProgress progress: Float) {
// Update your UI with the progress of the update.
updateProgressView.value = progress
}
func terminal(_ terminal: Terminal, didCompleteReaderUpdate error: Error?) {
// Notify the user that the update completed.
if let e = error {
presentAlert(error: e)
}
else {
presentAlert(title: "Update succeeded", message: "")
}
updateProgressView.value = 0
}