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:
        case .cardRemoved:

    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)")


    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 \
      -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'
      "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>();
    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 ""
    stripe.Key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc"
    params := &stripe.PaymentIntentParams{
      Amount: stripe.Int64(1000),
      Currency: stripe.String(string(stripe.CurrencyUSD)),
      AllowedSourceTypes: []*string{
      CaptureMethod: stripe.String("manual"),

    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 {

    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 \
      -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:

    1. Disconnect from the currently connected reader, if one is connected.
    2. Call clearCachedCredentials to clear the connection token & reader session.
    3. Configure your app to use the new account.
    4. Connect to a reader.


    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 \
      -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 \
      -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.


    To create a Location on your platform account that is also associated with a connected account, use the optional operator_account parameter.

    curl \
      -u sk_test_4eC39HqLyjWDarjtT1zdp7dc: \
      -d display_name=HQ \
      -d operator_account="{CONNECTED_ACCOUNT_ID}"


    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 \
      -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 \
      -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:
    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


    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


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