Stripe Terminal is Stripe's product for in-person card payments inside software you build. Your app talks to a pre-certified card reader, or to the phone itself with Tap to Pay, through a Terminal SDK for JavaScript, iOS, Android or React Native, or through a server-driven API. Every sale is a PaymentIntent created on your server, collected on the reader and confirmed by the SDK or reader, so online and in-person payments share one model.

This guide is the overview we wish every team had before starting a Terminal project. It explains how the pieces fit, how to pick an SDK and a reader, what Tap to Pay on iPhone and Android requires, how connection tokens and locations work, the card-present payment flow, offline mode, receipts and refunds, syncing payments to a CRM or POS, Terminal with Connect, what people mean by a "virtual terminal", testing with simulated readers and a go-live checklist. If you are building inside Flutter or a web-based kiosk, our separate Stripe Terminal guide for Flutter and Android kiosks goes deeper on that architecture. Stripe's devices, countries and SDK versions change often, so every requirement below links to the relevant page of Stripe's documentation, which remains the source of truth.

What is Stripe Terminal and how does it work?

Stripe Terminal connects your point-of-sale software to a card reader so that in-person payments run through the same Stripe account, API and Dashboard as your online payments. The reader handles card data and encryption; your code decides what is being sold, for how much, and what happens after the payment.

Four parties take part in every Terminal payment. Your app or POS is the interface staff or customers use: a native mobile app, a web app at a counter, a kiosk. The reader is a Stripe pre-certified device, or the phone's own NFC chip when you use Tap to Pay. Your server holds the secret API key, creates PaymentIntents and connection tokens, captures payments and receives webhooks. Stripe authorizes the payment with the card network and issuer, and tells your server what happened. Stripe describes its readers as pre-certified for PCI and EMV and notes that they encrypt sensitive card information, so your application never handles raw card data. That is the main reason teams use Terminal instead of building their own card-acceptance hardware path.

People searching "terminal stripe" may mean the readers or the developer product, but they are one system: the integration is what lets a payment carry your order ID and be refunded from your own tools. Stripe's no-code options, covered later, suit businesses that only need to take a payment.

The card-present payment sequence. The secret key never leaves your server, and the order is marked paid from the server's view of the PaymentIntent, not from the screen.

Which Stripe Terminal SDK should you use: JavaScript, iOS, Android, React Native or server-driven?

Pick the SDK that matches where your checkout runs and which readers you need. Stripe's comparison says the iOS, Android and React Native SDKs work with all reader types including Tap to Pay, while the JavaScript SDK and the server-driven integration work with smart readers only.

Stripe's Terminal setup guide has a variant for each platform: server-driven, JavaScript, iOS, Android and React Native, plus Java, .NET and Apps on Devices variants. The practical choice for most teams comes down to five options.

  • Stripe Terminal Android SDK. For native Android apps using mobile readers over Bluetooth or USB, smart readers, Tap to Pay on Android, or Apps on Devices. This is the answer to most "stripe terminal android" and "stripe android sdk" questions in an in-person context. Note that the general Stripe Android SDK for online payments is a different library; Terminal has its own.
  • Stripe Terminal iOS SDK. For native iOS apps with mobile readers, smart readers or Tap to Pay on iPhone.
  • Stripe Terminal React Native SDK. One codebase for iOS and Android Terminal apps. Stripe's platform comparison listed it as public preview when we checked, so confirm its status and changelog before committing a product to it.
  • Stripe Terminal JavaScript SDK. For browser-based POS systems with smart readers. It requires the POS and reader on the same local network with working local DNS, and the script must load directly from js.stripe.com rather than being bundled or self-hosted.
  • Server-driven integration. Your server drives a smart reader through the Stripe API with no client SDK. Stripe recommends it over the JavaScript SDK for smart readers, because it avoids the local-network requirement and can be simpler in complex network environments.

What about Flutter, web kiosks and other stacks?

Stripe does not list a Flutter SDK for Terminal. A Flutter app, or a kiosk whose interface is a web page, reaches Terminal through a native layer, typically a Kotlin or Swift module that owns the Terminal SDK and is called from the interface. We cover that pattern, and the payment-state problems it introduces, in the Flutter and Android kiosk guide. If your POS is a web app and your hardware can be a smart reader, the server-driven integration avoids the native layer entirely.

A payment terminal SDK for value-added services

Some teams search for a payment terminal SDK because they want the device to do more than take a card: show a cart, collect a tip, ask a question on screen, or run their own app. Stripe's reader comparison lists on-reader tipping and on-screen input collection for smart readers, and Apps on Devices, which lets your Android or React Native app run directly on a compatible smart reader; Stripe marks custom on-device apps as a paid capability. There is also a no-code standalone mode on some smart readers. Which of these fits depends on whether your value-added feature belongs on the customer-facing screen or in your staff app. Check each feature against the reader you plan to buy, because support differs by device.

How do you choose a Stripe Terminal reader?

Start from where the payment physically happens and which SDK you can use. Stripe groups hardware into smart readers (internet-connected, with their own touchscreen), mobile readers (paired over Bluetooth, or USB on Android, to a phone or tablet running a mobile SDK) and Tap to Pay (no separate reader at all).

We deliberately do not reproduce a device list here, because models and country availability change. Stripe's Select a reader page keeps the current comparison, a use-case table and a per-country availability matrix. Work through these questions with that page open.

  1. Where does the customer pay? At a fixed counter, at a self-service kiosk, at the door, at a table, or on a staff member's phone in the field.
  2. Which countries? Reader models and Tap to Pay availability differ by country, and some are in preview in some markets. This is the first thing to check, not the last.
  3. Which SDK can you use? A web POS limits you to smart readers (JavaScript SDK or server-driven). Mobile readers need a mobile SDK. Tap to Pay needs the iOS, Android or React Native SDK.
  4. Which card types must you accept? Tap to Pay is contactless only; there is no chip insert. In markets where some cards need a chip and PIN, Stripe recommends falling back to a card reader or another method.
  5. Do you need features beyond the payment? On-screen tipping, inputs, a cart display, cellular connectivity or your own app on the device narrow the list quickly.
  6. Do you need offline payments? Support differs by reader and SDK, and Tap to Pay on Android does not support offline mode.
A first-pass decision path. The final choice also depends on country availability, card types you must accept and features such as tipping or offline mode.

What does Tap to Pay on iPhone and Android require?

Tap to Pay turns a compatible iPhone or Android device into a contactless reader. On iPhone it needs an Apple entitlement and an iPhone XS or later on a supported iOS version; on Android it needs an NFC-equipped device that passes Stripe's security checks, including Android 13 or later and a locked, unrooted bootloader.

Stripe's Tap to Pay documentation is split by platform, and both pages warn that device and minimum SDK requirements can change because of compliance or security updates. Stripe suggests subscribing to its Terminal announcements list for that reason. As of October 2026 the requirements were as follows.

Tap to Pay on iPhone

  • Available through the Terminal iOS SDK and the Terminal React Native SDK.
  • You must request the Tap to Pay on iPhone development entitlement from your Apple Developer account, then a distribution entitlement after internal testing. Stripe describes the process as complex and requiring app submission to Apple for approval.
  • Devices: iPhone XS or later on a supported iOS version. Tap to Pay does not work on beta releases of iOS. PIN entry needs iOS 16.4 or later.
  • Apple requires a "How to Tap" instructional overlay before app review; Stripe documents using Apple's ProximityReaderDiscovery API for it.
  • For platforms, use is subject to Apple's acceptance platform terms, which Stripe links from the page.

Tap to Pay on Android

  • Available through the Terminal Android SDK (with the Tap to Pay dependency) and the Terminal React Native SDK.
  • The device must have a working integrated NFC sensor and an ARM-based processor, run Android 13 or later with a security update from the past 12 months, use Google Mobile Services with the Play Store installed, have a hardware-backed keystore, and run the unmodified manufacturer OS.
  • It must not be rooted, its bootloader must be locked, and Developer options must be disabled. It must not itself be a certified PCI PTS payment device.
  • Android emulators are not supported, and Stripe enforces the same device requirements for the simulated Tap to Pay reader as for production. Plan for real test devices early.
  • PIN collection fails if accessibility services are running, the screen is being recorded or overlaid, or someone tries to take a screenshot. Kiosk and MDM setups need checking against this list.

Two consequences surprise teams. First, Tap to Pay only reads contactless cards and wallets, and in some markets certain cards require a chip insert and PIN, which Tap to Pay cannot do. Stripe's own recommendation is to ask for another card or use a different method, such as a card reader or a Payment Link, so your UI needs a fallback path. Second, Stripe notes you can only have one active connection to a reader at a time, which matters if you planned to share one device between apps.

Connection tokens and locations: how does your app get access to a reader?

Your server creates a short-lived ConnectionToken with your secret key and returns only its secret to the SDK, which uses it to talk to readers. Locations group readers by physical site; every reader must be registered to one, and a token can be scoped to a location to limit which smart readers it can reach.

Stripe is direct about the risk: the secret from a connection token lets the holder connect to any Terminal reader and take payments on your Stripe account. So the endpoint that creates tokens must be authenticated, protected from cross-site request forgery, and only answer clients you trust, such as a logged-in staff session or an enrolled device. The SDK calls your token function whenever it needs to authenticate, including on reconnects, and Stripe's guidance is to never cache or hardcode the token. The SDK manages its lifecycle.

Connection token endpoint (Node.js, Express)
import express from "express";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const app = express();

// requireEnrolledDevice is your own middleware: an authenticated staff
// session or device credential, with CSRF protection for browser clients.
app.post("/terminal/connection_token", requireEnrolledDevice, async (req, res) => {
  const token = await stripe.terminal.connectionTokens.create({
    // Optional: limit this token to smart readers at the device's site.
    // Stripe notes the location has no effect for mobile readers.
    location: req.device.stripeLocationId,
  });
  res.json({ secret: token.secret }); // return the secret, nothing else
});

Locations represent physical sites. Before a reader can take payments it must be registered to a location, and the location also defines the reader's country settings. Required address fields vary by country, and a location's country cannot be changed after creation; you create a new location and re-register readers instead. Locations matter in three practical ways: they are how you limit smart-reader access through token scoping, how Tap to Pay and mobile reader connections are tied to a site (the mobile SDKs take a location ID when connecting), and how offline mode is configured, since it is enabled per location configuration. Stripe also offers optional zones for grouping locations, managed in the Dashboard.

How does a card-present payment flow work: create, collect, process, capture?

A Terminal payment is a PaymentIntent that includes `card_present`. You create it, the reader collects the card, the SDK or reader processes it so the issuer can authorize, and then you capture it, automatically or manually. Each step can be retried safely if you keep using the same PaymentIntent.

Stripe's collect card payments guide breaks the flow into steps that survive failures. Here is what each one means in a real integration.

  1. Create the PaymentIntent. Usually on your server, from your own order total, with `card_present` in the payment method types. The JavaScript SDK requires server-side creation; the mobile SDKs can also create it on the device. Choose `capture_method`: `manual` lets you add a reconciliation step before finalizing, `automatic` authorizes and captures in one go. Newer mobile SDK versions default to automatic asynchronous capture when the SDK creates the intent, so set manual explicitly if you rely on it.
  2. Collect a payment method. The connected reader waits for a tap or insert. Optionally, you can inspect the card details before authorizing, but Stripe requires you to authorize or cancel within 30 seconds of collection.
  3. Process (confirm) the payment. Authorization happens here. Stripe is explicit that confirmation must go through the Terminal SDK, not your server, because server-side confirmation bypasses steps such as PIN prompts. In the server-driven integration the reader does it when you call its process action.
  4. Capture. With manual capture, your app tells your server, and your server captures. Stripe says a manually captured PaymentIntent must be captured within 2 days or the authorization expires and the funds are released to the customer.
Collect and process a payment with the Terminal JavaScript SDK (smart reader)
const terminal = StripeTerminal.create({
  onFetchConnectionToken: async () => {
    const res = await fetch("/terminal/connection_token", { method: "POST", credentials: "include" });
    const { secret } = await res.json();
    return secret;
  },
  onUnexpectedReaderDisconnect: () => showReaderDisconnectedBanner(),
});

// Assumes the reader is already discovered and connected.
export async function takePayment(orderId) {
  // 1. Your server creates the PaymentIntent from the order and returns its client secret.
  const { clientSecret } = await postJson("/terminal/payment_intents", { orderId });

  // 2. The reader waits for a card.
  const collected = await terminal.collectPaymentMethod(clientSecret);
  if (collected.error) return showCollectError(collected.error);

  // 3. Authorize on the reader. On failure, inspect the PaymentIntent status
  //    and retry with the SAME intent; never create a second one.
  const processed = await terminal.processPayment(collected.paymentIntent);
  if (processed.error) return handleProcessError(processed.error, orderId);

  // 4. Your server captures (manual capture) and marks the order paid.
  await postJson("/terminal/capture", { orderId, paymentIntentId: processed.paymentIntent.id });
}

Failure handling that prevents double charges

Stripe's guidance comes down to one rule: one PaymentIntent per transaction. When processing fails, the SDK returns an error with the updated PaymentIntent, and its status tells you what to do. `requires_payment_method` means the card was declined, so collect again on the same intent. `requires_confirmation` means a temporary connectivity problem, so process the same intent again. If the request to Stripe timed out and the status is unknown, retry the original intent; Stripe warns that creating a new one could lead to multiple authorizations on the cardholder's card. Your server should enforce this too, by creating payment intents with an idempotency key per order attempt and refusing to create a second live intent for an order that already has one.

Stripe also recommends reconciling PaymentIntents against your orders at the end of each day. An intent left in `requires_capture` is either an abandoned checkout, which you should cancel to release the hold, or a completed sale whose capture request never reached your server, which you should capture if no other payment was taken for the order. Building that nightly job is part of the integration, not an optional extra. The same discipline applies to webhooks; our guide to Stripe webhooks, idempotency and retries covers signature checks and duplicate events in detail.

Receipts, refunds and cancellations

Stripe requires Terminal users to offer customers a printed or email receipt. Before capture, you cancel a PaymentIntent to release the hold; after capture, you refund it through the API or Dashboard, and for most card networks the customer does not need to present the card again.

Receipts. Stripe's Terminal receipt documentation says card network rules for in-person payments differ from online ones and that you must offer a physical or email receipt. The simplest route is setting `receipt_email` on the PaymentIntent; Stripe then sends a compliant receipt when the payment is captured. If you print or email your own, card network rules require certain EMV fields, such as the application name and the AID, which Stripe exposes on the charge's card-present receipt details once the payment is confirmed. Receipts for test-mode payments are not sent automatically.

Cancellations and refunds. Stripe's refund documentation separates the two. You can cancel a `card_present` PaymentIntent at any time before capture, which releases the uncaptured funds, for example when a customer decides to pay in cash. After capture, you create a refund by PaymentIntent or charge ID, fully or partially. Online refunds are available on all card networks except Interac; Interac in Canada uses in-person refunds on supported readers. Single-message networks such as Interac and eftpos are captured automatically, so your app should offer a refund at the end of checkout rather than a cancel. Wire refunds to your own order system so that the person issuing a refund in your back office sees the same state the customer sees.

Can Stripe Terminal take payments offline?

Yes, on supported readers with the Terminal mobile SDKs: payments are stored on the device and forwarded when the connection returns. But authorization only happens after forwarding, so you carry the risk of declines, and Stripe says there is no way to recover funds for a payment the issuer later declines.

Stripe's offline payments overview lists which readers, card brands and SDKs support it. A few rules shape the design:

  • The server-driven and JavaScript integrations do not support offline card payments. Tap to Pay on Android does not support offline mode; Tap to Pay on iPhone offline was in private preview.
  • Offline mode is enabled through a Terminal Configuration assigned to a location. For mobile readers, you must have connected online to a reader of the same type at the same location in the last 30 days and updated its software in that time.
  • Stripe enforces an offline maximum of US$10,000 or the equivalent per transaction, and gives you SDK properties for the count and total of stored payments so you can set lower limits. Use the `REQUIRE_ONLINE` behavior to refuse offline payments above your own threshold.
  • Offline PaymentIntents have no ID until forwarded, so put your own identifier in metadata and reconcile in the forwarding callback.
  • Clearing the app's storage or reinstalling it loses any payments not yet forwarded. Payments created and confirmed offline cannot be cancelled or refunded until they reach Stripe.
  • Swiping is not allowed offline, and in markets with Strong Customer Authentication, tapping is not supported offline either.

Offline mode is a business decision as much as a technical one: decide who approves the risk and the maximum stored total, and tell staff to keep devices online until the queue drains.

How do you connect a payment terminal to a CRM or POS?

Put your own identifiers, such as order, customer and store IDs, in the PaymentIntent's metadata when your server creates it, then let a webhook handler update the CRM or POS from Stripe's events. The reader never talks to your CRM directly; your server is the bridge.

A Terminal payment is an ordinary PaymentIntent once it reaches Stripe, which is good news: everything you know about online payment syncing applies. The pattern we use has four parts.

  1. Create intents server-side from your order. The order, not the screen, sets the amount. Attach metadata such as `order_id`, the location or store, the staff member and the CRM contact ID. Do not put card data or sensitive personal data in metadata.
  2. Listen to webhooks. `payment_intent.succeeded`, `payment_intent.payment_failed`, `payment_intent.canceled` and refund events drive state. For server-driven integrations, Stripe also sends `terminal.reader.action_succeeded`, `terminal.reader.action_failed` and `terminal.reader.action_updated`, and recommends a dedicated endpoint for those because they are in the critical payment path.
  3. Make the handler idempotent. Verify the signature, skip event IDs you have already processed, and look up the order by your own ID before writing. Events can arrive more than once and out of order.
  4. Reconcile nightly. Compare every captured and uncaptured intent from the day with your orders. Anything that does not match one order exactly is a support ticket waiting to happen.
How payment metadata flows from the counter to your CRM. The webhook handler, not the device, writes to the system of record.

If your CRM or POS offers a ready-made Stripe connector, check what it actually syncs: a connector may not map a payment to the right order without your metadata, or may ignore location and reader. When it falls short, a small webhook service in your own stack is usually more reliable. Our Stripe integration guide covers what data to keep in your own database.

How does Stripe Terminal work with Connect platforms?

Terminal works with all three Connect charge types. With direct charges, you make Terminal requests with the connected account in the `Stripe-Account` header; with destination charges, the platform owns intents and locations and identifies the seller through metadata and transfer data; with separate charges and transfers, you charge on the platform and transfer later.

Stripe's Terminal with Connect guide adds some details worth knowing before you design a multi-merchant POS:

  • Connected accounts taking Terminal payments must have the `card_payments` capability.
  • With destination charges, create connection tokens with the platform's key and without the `Stripe-Account` header, and scope them by location to control which readers each seller's devices can reach. Locations belong to the platform, so store the connected account reference in location metadata.
  • On destination-charge Terminal payments, set `on_behalf_of`, `transfer_data[destination]` and, if you take a fee, `application_fee_amount`. Stripe says `on_behalf_of` is required for Terminal when the platform and connected account are in different countries.
  • With manual capture, Stripe suggests inspecting each PaymentIntent and adjusting the application fee if needed before capturing.

Which charge type to use is a business question about who the customer is buying from and who carries refunds and disputes. We cover it in Stripe Connect direct vs destination charges, and onboarding sellers in Stripe Connect onboarding for marketplaces. Decide that first; it changes where locations and readers live.

Is there a virtual terminal for Stripe?

If by virtual terminal you mean typing a customer's card details into a screen, Stripe lets you charge a card manually from the Stripe Dashboard, including the Dashboard mobile app. That is a card-not-present payment, not a Stripe Terminal payment, and it does not need a reader or an integration.

The distinction matters for three reasons. A keyed-in payment is card-not-present, so it is treated like an online payment rather than a chip or contactless one. It is a manual staff action in the Dashboard, so it does not carry your order IDs or update your systems unless someone adds them. And it is easy to confuse with the no-code in-person options Stripe documents: Tap to Pay inside the Stripe Dashboard mobile app, a Stripe Terminal app for iPhone and Android in private preview, and standalone mode on compatible smart readers. Those take real card-present payments without code, but they also sit outside your software.

A useful rule: if someone only needs to take a payment, use the no-code options. If the payment must belong to an order, a booking, a customer or a seller on your platform, build a Terminal integration.

How do you test Stripe Terminal and go live?

Build against the simulated reader that ships with every Terminal SDK and the server-driven API, then test the full hardware path with a physical test card on real devices in a sandbox, and only then run a controlled live pilot. Each stage catches failures the previous one cannot.

Stripe's testing reference describes the tools. In the JavaScript SDK you discover a simulated reader with `discoverReaders({ simulated: true })` and set a test card with `setSimulatorConfiguration`; the mobile SDKs have equivalent simulator configuration. Test card numbers produce specific outcomes, including declines for insufficient funds, lost or stolen cards and refund failures, plus online and offline PIN flows. For the server-driven integration, a test helper endpoint simulates the cardholder presenting a card. With a physical test card on a real reader, the amount's last two digits select the outcome: amounts ending in 00 are approved, while endings such as 01, 05 and 55 return specific declines. Stripe notes mobile wallets cannot be used with Terminal in test mode, and that physical test cards only work in a sandbox. Remember that Tap to Pay on Android cannot be tested on an emulator at all.

From here, this article is about Cevrynt

How Cevrynt builds Stripe Terminal and Tap to Pay integrations

Cevrynt, Inc. is a Stripe partner in the Stripe Partner Ecosystem, listed in the Stripe Partner Directory, and our engineering team builds in-person payment integrations for POS and kiosk products. Three members of the team hold individual Stripe professional certifications: Arindam Pradhan, our founder (Implementation Architect), Anupam Pradhan (Payments Architect) and Subhadip Giri (Payments Architect). For Megapos Ltd, a point-of-sale company, we built a Stripe Terminal Tap to Pay kiosk: a web interface inside a Flutter WebView, a Node.js and Express backend, and a native Kotlin bridge to Tap to Pay, with the PaymentIntent and its client secret passed from the web layer to Flutter to Kotlin so the payment is collected natively without exposing keys in the web app.

A Terminal engagement starts with a device and country check against Stripe's documentation and a written integration design covering the layers, where payment state lives and how failures are handled. Every project is scoped after a technical review, and we build in your repository and your Stripe account so the code is yours. See Stripe Terminal and Tap to Pay engineering, our wider Stripe services, or tell us about your in-person payments project.