SAAS

Stripe Terminal iOS Integration

Building in-person payments into an iOS app sounds straightforward until you are three hours into the Stripe Terminal docs wondering why your reader is stuck in a perpetual "Connecting" state and your ConnectionToken endpoint keeps returning 403s. The official documentation is thorough but dense, and it assumes a familiarity with Stripe's server-side concepts that developers coming from a mobile background often do not have yet.

Stripe Terminal iOS Integration

Stripe Terminal brings Stripe’s payment infrastructure to physical card readers, letting you accept chip, contactless, and swipe payments from within your own iOS app. The result is a tightly branded, fully programmatic in-person payment experience without routing through a third-party POS system. Used by retail platforms, service businesses, event ticketing apps, and field sales teams, it is one of the cleaner in-person payment SDKs available in 2026.

The Architecture Before Writing Any Code

Stripe Terminal uses a client-server architecture that is different from standard Stripe payment flows and worth understanding before touching the SDK. Three components interact in every transaction: your iOS app (the client), your backend server, and the Stripe Terminal SDK running inside the app.

Your server handles two responsibilities that cannot move to the client for security reasons. First, it generates a ConnectionToken by calling the Stripe API. This token authorizes the Terminal SDK instance on a specific device. Second, it creates a PaymentIntent when a transaction begins. The PaymentIntent lives on Stripe’s servers and tracks the payment through confirmation. Your iOS app never calls the Stripe API directly for these operations. It calls your backend, which calls Stripe.

This server-in-the-middle requirement surprises developers building their first Terminal integration. It means you need a working server endpoint before your iOS app can do anything useful, and it means your server needs to be reachable from the device during testing, which rules out localhost unless you are using a tunnel like ngrok.

Setting Up Your Environment

Server-Side: ConnectionToken Endpoint

Your server needs one endpoint to start: a route that calls the Stripe API to create a ConnectionToken and returns it to your iOS app. In Node.js with Express, the implementation is minimal:

app.post('/connection_token', async (req, res) => {

  const token = await stripe.terminal.connectionTokens.create();

  res.json({ secret: token.secret });

});

The endpoint should require authentication in production. A device that can obtain a ConnectionToken can process real payments. Lock this endpoint down behind your app’s existing auth flow before going live. During development, a simple API key header check is sufficient.

iOS: Installing the Stripe Terminal SDK

The Stripe Terminal iOS SDK supports Swift Package Manager, CocoaPods, and Carthage. Swift Package Manager is the recommended approach for new projects in 2026. Add the package URL https://github.com/stripe/stripe-terminal-ios to your project in Xcode via File > Add Package Dependencies, and select the StripeTerminal product.

The SDK requires the following permissions in your Info.plist depending on which reader types you support. Bluetooth readers like the BBPOS Chipper 2X BT and the Stripe Reader M2 require NSBluetoothAlwaysUsageDescription and NSBluetoothPeripheralUsageDescription. Location permission (NSLocationWhenInUseUsageDescription) is required for all reader types as Stripe uses it for fraud detection. Missing a required permission entry does not crash the app at install. It causes silent initialization failures that are confusing to diagnose.

Initializing the SDK and Connecting a Reader

Setting Up the Terminal Instance

The Terminal SDK is a singleton. You initialize it once, typically in your AppDelegate or in a dedicated payment manager class, and access it throughout your app via Terminal.shared. Initialization requires a ConnectionToken provider, which is the bridge between the SDK and your server endpoint:

import StripeTerminal

class AppDelegate: UIResponder, UIApplicationDelegate, ConnectionTokenProvider {

  func applicationDidFinishLaunching(_ application: UIApplication) {

Terminal.setTokenProvider(self)

  }

  func fetchConnectionToken(_ completion: @escaping ConnectionTokenCompletionBlock) {

// Call your server endpoint and pass the secret to completion

YourAPIClient.createConnectionToken { secret, error in

   if let secret = secret {

     completion(secret, nil)

   } else {

     completion(nil, error)

   }

}

  }

}

Discovering and Connecting a Reader

Reader discovery uses a DiscoveryConfiguration object that specifies the discovery method and whether to use simulated readers. During development, the simulated reader lets you complete full payment flows without physical hardware:

let config = try? DiscoveryConfiguration(

  discoveryMethod: .bluetoothScan,

  simulated: false  // set true for development

)

Terminal.shared.discoverReaders(config!, delegate: self) { error in

  // discovery started

}

When your DiscoveryDelegate receives the didUpdateDiscoveredReaders callback, present the discovered readers to the user or auto-connect to a known reader by serial number. Connecting to a reader requires a ReaderSoftwareUpdateDelegate to handle firmware update prompts, which Stripe may push during the connection phase. Blocking or ignoring update prompts causes connection failures that surface as cryptic errors.

Stripe Terminal Reader Options for iOS 

ReaderConnectionAcceptsBest ForApprox. Price
Stripe Reader M2BluetoothChip, tap, swipeMobile, field use~$59
BBPOS Chipper 2X BTBluetoothChip, swipeLow-volume mobile~$49
BBPOS WisePOS EWi-Fi / LANChip, tap, swipeCounter-top, retail~$249
Stripe Reader S700Wi-Fi / LANChip, tap, swipe, displayHigh-volume retail~$349
Verifone P400Wi-Fi / LANChip, tap, swipe, displayEnterprise POS~$299

Collecting a Payment End to End

The payment flow has three steps: create a PaymentIntent on your server, collect a payment method on the reader, and confirm the payment. Each step has distinct error modes worth handling explicitly.

Step 1: Create the PaymentIntent on Your Server

Before anything happens on the device, your server creates a PaymentIntent with the amount, currency, and capture method. For most Terminal integrations, use capture_method: automatic so the payment confirms and captures in a single step:

const paymentIntent = await stripe.paymentIntents.create({

  amount: 2500,  // in cents

  currency: 'usd',

  payment_method_types: ['card_present'],

  capture_method: 'automatic',

});

Return the PaymentIntent’s client_secret to your iOS app. The SDK uses the client_secret to retrieve and confirm the PaymentIntent without exposing your Stripe secret key on the device.

Step 2: Collect the Payment Method

Pass the client_secret to Terminal.shared.collectPaymentMethod. The SDK takes control at this point, displaying prompts to the reader and waiting for the customer to tap, insert, or swipe their card:

Terminal.shared.collectPaymentMethod(clientSecret) { paymentIntent, error in

  guard let intent = paymentIntent else {

// Handle collection error

return

  }

  // Proceed to confirmation

}

The collect call is cancelable. Always provide a cancel button in your UI during collection. A customer who removes their card mid-swipe, a reader that loses Bluetooth connectivity mid-transaction, and a user who decides not to pay all require clean cancellation handling. Uncanceled collection calls block the reader from accepting new transactions.

Step 3: Confirm the Payment

Once collection succeeds, confirm the PaymentIntent. With automatic capture, this is a single SDK call that completes the charge:

Terminal.shared.confirmPaymentIntent(paymentIntent) { confirmedIntent, error in

  if let error = error as? NSError {

// Check error.code for specific failure type

  } else {

// Payment complete. Store confirmedIntent.stripeId for your records.

  }

}

Error Handling That Actually Covers the Real Failure Modes

The Stripe Terminal SDK surfaces errors through a typed error system. The most important errors to handle explicitly, rather than falling through to a generic failure message, are the ones your users will actually encounter.

  • Card declined (ErrorCode.stripeAPIDeclinedCard): Display a clear decline message and offer to try a different card. Do not expose the raw decline code to the customer.
  • Reader disconnected mid-transaction (ErrorCode.readerConnectionNotAvailable): Cancel the current collection, attempt reconnection, and prompt the user to try again once the reader is back online.
  • Network not available (ErrorCode.notConnectedToInternet): The confirmation step requires internet access. Queue the retry or inform the user that processing requires connectivity.
  • Payment intent already processed (ErrorCode.paymentIntentAlreadySucceeded): This occurs if your app retries a confirmation on an already-confirmed intent. Check PaymentIntent status server-side before retrying any failed confirmation.
  • Reader software update required (ErrorCode.readerSoftwareUpdateRequired): Do not suppress this. Show the update prompt, complete the update, then retry the connection. Updates typically take 60 to 90 seconds.

Offline Mode: Collecting Payments Without Connectivity

Stripe Terminal’s offline mode, available in SDK version 3.0 and later as of 2024, allows the reader to collect and store payments locally when internet connectivity is unavailable. Collected payments are forwarded to Stripe automatically when connectivity is restored. This is directly relevant for mobile deployments at events, markets, or outdoor venues where connectivity is unreliable.

Enabling offline mode requires explicit opt-in in your OfflineModeConfiguration and an understanding of its constraints. Offline payments have a per-transaction limit (configurable in your Stripe Dashboard, defaulting to $999) and a maximum stored transaction count before the reader requires connectivity to forward collected payments. Payments stored offline are not immediately confirmed, which means your order management system must handle the pending-to-confirmed state transition when sync occurs.

The risk profile of offline mode is different from online payments. Stripe accepts the chargeback risk for card-present transactions processed online. For offline transactions, you accept the risk that a stored payment may be declined when it is forwarded to Stripe after connectivity is restored. Design your business logic accordingly, particularly for high-value transactions or transactions where fraud risk is elevated.

Frequently Asked Questions

Can I use Stripe Terminal without a dedicated backend server?

No. The ConnectionToken and PaymentIntent creation steps require server-side Stripe API calls using your secret key. Embedding your Stripe secret key in an iOS app violates Stripe’s terms of service and creates a serious security vulnerability. If you do not have backend infrastructure, serverless functions on AWS Lambda, Google Cloud Functions, or Vercel are the lowest-overhead path to the required server-side endpoints.

How do I test Stripe Terminal without physical hardware?

Set simulated: true in your DiscoveryConfiguration during development. The simulated reader behaves identically to a real reader for all SDK calls and payment flows. You can also trigger specific test scenarios including card declines, network errors, and reader disconnections using Stripe’s test card numbers and the simulated reader’s test behavior settings. 

What is the difference between card_present and card_not_present payment types?

card_present is used exclusively for Stripe Terminal transactions where a physical card interacts with the reader. card_not_present covers manually keyed card numbers, online payments, and saved payment methods. The two types have different fraud liability rules: card_present transactions shift chargeback liability to the card issuer when EMV chip is used, which reduces your exposure significantly compared to card_not_present. 

How do I handle tipping within a Stripe Terminal iOS integration?

Stripe Terminal supports on-reader tipping for BBPOS WisePOS E and Stripe Reader S700 devices with display screens. Configure tip settings in your Stripe Dashboard under Terminal > Settings. For Bluetooth readers without displays, implement tip collection in your iOS app UI before initiating payment collection. 

Conclusion

The Stripe Terminal iOS SDK is genuinely well-built. The documentation is comprehensive. The simulated reader makes local development fast. What the docs do not emphasize enough is that in-person payments have failure modes that online payments do not: readers that lose Bluetooth pairing mid-transaction, customers who remove cards at precisely the wrong moment. 

Build your error handling before you demo to a client. Test with a real reader in a real location before launch, not on a desk with full Wi-Fi bars. And reconcile every transaction server-side rather than relying solely on SDK callbacks to determine whether a payment succeeded. The payment infrastructure is solid. 

Leave a Reply

Your email address will not be published. Required fields are marked *