Stripe Terminal lets you accept in-person card payments using physical card readers. Set up Terminal in the Dashboard, register a reader, create a connection token from your server, and use the Stripe Terminal SDK to collect payments. All transactions flow through your existing Stripe account alongside your online payments.
Accepting In-Person Payments with Stripe Terminal
Stripe Terminal extends your Stripe integration to physical locations. You connect a card reader, create a PaymentIntent, and collect the payment in person. The reader handles chip, tap (NFC), and swipe payments. All transactions appear in the same Stripe Dashboard as your online payments, giving you a unified view of your revenue.
Prerequisites
- A Stripe account with Terminal enabled
- A compatible Stripe card reader (BBPOS WisePOS E or Stripe Reader S700)
- A backend server running Node.js with Express
- The Stripe Terminal JavaScript SDK or a mobile SDK for iOS/Android
Step-by-step guide
Order and register a card reader
Order and register a card reader
Go to Stripe Dashboard → Terminal → Readers. Order a reader if you do not have one (Stripe ships directly). Once received, the reader appears in your Dashboard after powering on and connecting to Wi-Fi.
Expected result: Your card reader appears as 'Online' in the Terminal → Readers section of the Dashboard.
Create a connection token endpoint
Create a connection token endpoint
The Terminal SDK needs a connection token to authenticate with Stripe. Create a server endpoint that generates one. This token is short-lived and must be fetched fresh each time.
1// server.js2const express = require('express');3const Stripe = require('stripe');4const stripe = Stripe(process.env.STRIPE_SECRET_KEY);5const app = express();6app.use(express.json());78app.post('/api/connection-token', async (req, res) => {9 try {10 const token = await stripe.terminal.connectionTokens.create();11 res.json({ secret: token.secret });12 } catch (err) {13 res.status(500).json({ error: err.message });14 }15});1617app.listen(3001);Expected result: The endpoint returns a connection token secret that the Terminal SDK uses to connect to the reader.
Initialize the Terminal SDK
Initialize the Terminal SDK
Load the Stripe Terminal JavaScript SDK and initialize it with a function that fetches the connection token from your server.
1const terminal = StripeTerminal.create({2 onFetchConnectionToken: async () => {3 const res = await fetch('/api/connection-token', { method: 'POST' });4 const data = await res.json();5 return data.secret;6 },7 onUnexpectedReaderDisconnect: () => {8 console.log('Reader disconnected unexpectedly');9 }10});Expected result: The Terminal SDK is initialized and ready to discover and connect to readers.
Discover and connect to a reader
Discover and connect to a reader
Use the SDK to discover available readers on your network and connect to one. The Internet discovery method finds readers registered to your Stripe account.
1const discoverResult = await terminal.discoverReaders({2 simulated: false // set to true for testing without a physical reader3});45if (discoverResult.error) {6 console.error('Discovery failed:', discoverResult.error);7} else {8 const reader = discoverResult.discoveredReaders[0];9 const connectResult = await terminal.connectReader(reader);10 if (connectResult.error) {11 console.error('Connection failed:', connectResult.error);12 } else {13 console.log('Connected to:', connectResult.reader.label);14 }15}Expected result: The SDK connects to your card reader. The reader's status changes to connected.
Create a PaymentIntent and collect payment
Create a PaymentIntent and collect payment
Create a PaymentIntent on the server, then use the Terminal SDK to collect the payment on the reader. The customer taps or inserts their card on the reader.
1// Server: create PaymentIntent2app.post('/api/create-terminal-intent', async (req, res) => {3 const intent = await stripe.paymentIntents.create({4 amount: req.body.amount, // in cents5 currency: 'usd',6 payment_method_types: ['card_present'],7 capture_method: 'manual' // optional: authorize first, capture later8 });9 res.json({ client_secret: intent.client_secret });10});1112// Client: collect payment13const res = await fetch('/api/create-terminal-intent', {14 method: 'POST',15 headers: { 'Content-Type': 'application/json' },16 body: JSON.stringify({ amount: 2000 })17});18const { client_secret } = await res.json();1920const collectResult = await terminal.collectPaymentMethod(client_secret);21if (collectResult.error) {22 console.error('Collection failed:', collectResult.error);23} else {24 const confirmResult = await terminal.confirmPaymentIntent(collectResult.paymentIntent);25 if (confirmResult.error) {26 console.error('Confirmation failed:', confirmResult.error);27 } else {28 console.log('Payment succeeded:', confirmResult.paymentIntent.id);29 }30}Expected result: The reader prompts the customer to tap or insert their card. After confirmation, the PaymentIntent status is 'succeeded' (or 'requires_capture' if using manual capture).
Complete working example
1// terminal-server.js2// Node.js Express server for Stripe Terminal in-person payments34const express = require('express');5const Stripe = require('stripe');6const stripe = Stripe(process.env.STRIPE_SECRET_KEY);7const app = express();89app.use(express.static('public'));10app.use(express.json());1112// Connection token for Terminal SDK13app.post('/api/connection-token', async (req, res) => {14 try {15 const token = await stripe.terminal.connectionTokens.create();16 res.json({ secret: token.secret });17 } catch (err) {18 res.status(500).json({ error: err.message });19 }20});2122// Create PaymentIntent for in-person payment23app.post('/api/create-terminal-intent', async (req, res) => {24 try {25 const intent = await stripe.paymentIntents.create({26 amount: req.body.amount,27 currency: 'usd',28 payment_method_types: ['card_present'],29 description: req.body.description || 'In-person payment'30 });31 res.json({ client_secret: intent.client_secret, id: intent.id });32 } catch (err) {33 res.status(400).json({ error: err.message });34 }35});3637// Capture a PaymentIntent (if using manual capture)38app.post('/api/capture-intent/:id', async (req, res) => {39 try {40 const intent = await stripe.paymentIntents.capture(req.params.id);41 res.json({ status: intent.status });42 } catch (err) {43 res.status(400).json({ error: err.message });44 }45});4647// Register a new reader location48app.post('/api/create-location', async (req, res) => {49 try {50 const location = await stripe.terminal.locations.create({51 display_name: req.body.name,52 address: {53 line1: req.body.line1,54 city: req.body.city,55 state: req.body.state,56 postal_code: req.body.postal_code,57 country: req.body.country58 }59 });60 res.json({ location_id: location.id });61 } catch (err) {62 res.status(400).json({ error: err.message });63 }64});6566app.listen(3001, () => console.log('Terminal server on port 3001'));Common mistakes when using Stripe Terminal for in-person payments
Why it's a problem: Using 'card' instead of 'card_present' as the payment method type
How to avoid: In-person Terminal payments use 'card_present', not 'card'. The 'card' type is for online payments only.
Why it's a problem: Not creating a connection token endpoint
How to avoid: The Terminal SDK requires a fresh connection token for each session. Create a server endpoint that calls stripe.terminal.connectionTokens.create().
Why it's a problem: Forgetting to call confirmPaymentIntent after collectPaymentMethod
How to avoid: collectPaymentMethod only reads the card. You must call confirmPaymentIntent to actually process the payment.
Why it's a problem: Testing without a simulated reader when no physical reader is available
How to avoid: Set simulated: true in discoverReaders to use a simulated reader during development.
Best practices
- Use simulated readers during development and only connect to physical readers in staging and production
- Create a Terminal Location for each physical store to organize readers in the Dashboard
- Handle the onUnexpectedReaderDisconnect callback to automatically attempt reconnection
- Use manual capture_method if you need to authorize first and charge later (e.g., restaurants adding a tip)
- Store the PaymentIntent ID for each transaction to enable refunds and lookups
- Test with the physical reader in test mode — use test card 4242424242424242 on a real card would not work, but the simulated reader accepts any input
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to accept in-person card payments using Stripe Terminal with a BBPOS WisePOS E reader. Show me how to create a connection token endpoint in Node.js, initialize the Terminal SDK, discover and connect to a reader, and collect a payment by creating a PaymentIntent with card_present and confirming it through the reader.
Build a Node.js Express server for Stripe Terminal with endpoints for connection tokens and PaymentIntents. Show the client-side code to initialize StripeTerminal, discover readers, connect to one, collect a payment method, and confirm the PaymentIntent.
Frequently asked questions
Which card readers work with Stripe Terminal?
Stripe Terminal supports the BBPOS WisePOS E (countertop, Wi-Fi), Stripe Reader S700 (touchscreen, Wi-Fi/Ethernet), and BBPOS Chipper 2X BT (mobile, Bluetooth). Availability varies by country.
Can I use Stripe Terminal without internet?
Stripe Terminal requires an internet connection for payment processing. There is no offline mode — the reader must be connected to process transactions.
How do I test Stripe Terminal without a physical reader?
Set simulated: true when calling discoverReaders. The simulated reader mimics the behavior of a real reader, including presenting cards and confirming payments.
Are Terminal payments shown in the same Stripe Dashboard as online payments?
Yes. All Terminal payments appear in the Payments section of your Stripe Dashboard alongside your online transactions. They are identified by the card_present payment method type.
What are the fees for Stripe Terminal?
Stripe Terminal charges 2.7% + 5 cents per successful in-person transaction in the US. International and currency conversion fees may apply. Check Stripe's pricing page for your country.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation