Learn how to set up and use Stripe Terminal for in-person payments, from connecting card readers to processing payments, refunds, and receipts in your app.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
How to Use Stripe Terminal for In-Person Payments
Step 1: Understanding Stripe Terminal
Stripe Terminal is a programmable point-of-sale system that lets you build your own in-person checkout experience. It allows businesses to accept in-person payments with Stripe's pre-certified card readers while maintaining a single view of customers and payments across online and offline channels.
Step 2: Sign Up for a Stripe Account
Before you can use Stripe Terminal, you need a Stripe account:
Step 3: Set Up Your Stripe Terminal Account
To enable Terminal on your account:
Step 4: Create a Terminal Location
Locations represent physical places where you accept in-person payments:
Step 5: Set Up Your Backend Server
You'll need a backend server to securely create connection tokens. Here's how to implement it in Node.js with Express:
const express = require('express');
const stripe = require('stripe')('sk_test_YOUR_SECRET_KEY');
const app = express();
app.use(express.json());
// Create a Terminal connection token
app.post('/connection\_token', async (req, res) => {
try {
const connectionToken = await stripe.terminal.connectionTokens.create();
res.json({ secret: connectionToken.secret });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Step 6: Implement the Frontend Integration
Now you need to integrate the Terminal SDK into your frontend application. Here's an example using JavaScript:
// Initialize the Terminal object
const terminal = StripeTerminal.create({
onFetchConnectionToken: fetchConnectionToken,
onUnexpectedReaderDisconnect: unexpectedDisconnect,
});
// Fetch connection token from your backend
async function fetchConnectionToken() {
const response = await fetch('/connection\_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
return data.secret;
}
function unexpectedDisconnect() {
console.log('Reader disconnected');
// Handle reconnection logic
}
Step 7: Connect to a Reader
Now connect to your physical card reader:
// For Bluetooth readers
async function connectBluetoothReader() {
const config = { simulated: false };
const discoverResult = await terminal.discoverReaders(config);
if (discoverResult.error) {
console.error('Failed to discover readers', discoverResult.error);
return;
}
const selectedReader = discoverResult.discoveredReaders[0];
const connectResult = await terminal.connectReader(selectedReader);
if (connectResult.error) {
console.error('Failed to connect to reader', connectResult.error);
return;
}
console.log('Connected to reader:', connectResult.reader.label);
}
// For Internet readers
async function connectInternetReader() {
const config = {
simulated: false,
location: 'loc\_123456', // Your location ID from Step 4
};
const discoverResult = await terminal.discoverReaders(config);
if (discoverResult.error) {
console.error('Failed to discover readers', discoverResult.error);
return;
}
const selectedReader = discoverResult.discoveredReaders[0];
const connectResult = await terminal.connectReader(selectedReader);
if (connectResult.error) {
console.error('Failed to connect to reader', connectResult.error);
return;
}
console.log('Connected to reader:', connectResult.reader.label);
}
Step 8: Create a Payment Intent on Your Backend
Before collecting payment, create a PaymentIntent on your server:
// Add this endpoint to your Express server
app.post('/create_payment_intent', async (req, res) => {
try {
const amount = req.body.amount;
const currency = req.body.currency || 'usd';
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: currency,
payment_method_types: ['card\_present'],
capture\_method: 'automatic',
});
res.json({ client_secret: paymentIntent.client_secret });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Step 9: Collect Payment Using the Card Reader
With the PaymentIntent created, collect payment using the card reader:
async function collectPayment(amount) {
// 1. Create PaymentIntent on the server
const response = await fetch('/create_payment_intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: amount, // amount in cents
currency: 'usd',
}),
});
const data = await response.json();
const clientSecret = data.client\_secret;
// 2. Retrieve PaymentIntent on the client
const retrieveResult = await terminal.retrievePaymentIntent(clientSecret);
if (retrieveResult.error) {
console.error('Failed to retrieve payment intent', retrieveResult.error);
return;
}
// 3. Collect payment method
const result = await terminal.collectPaymentMethod(retrieveResult.paymentIntent);
if (result.error) {
console.error('Failed to collect payment method', result.error);
return;
}
// 4. Process payment
const processResult = await terminal.processPayment(result.paymentIntent);
if (processResult.error) {
console.error('Failed to process payment', processResult.error);
return;
}
if (processResult.paymentIntent.status === 'succeeded') {
console.log('Payment successful!');
// Handle successful payment (e.g., show receipt, update order status)
}
}
Step 10: Capture the Payment
If you set capture_method
to manual
when creating the PaymentIntent, you'll need to explicitly capture the payment:
// Add this endpoint to your Express server
app.post('/capture_payment_intent', async (req, res) => {
try {
const paymentIntentId = req.body.payment_intent_id;
const paymentIntent = await stripe.paymentIntents.capture(paymentIntentId);
res.json({ success: true, paymentIntent: paymentIntent });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Call this from your frontend
async function capturePayment(paymentIntentId) {
const response = await fetch('/capture_payment_intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
payment_intent_id: paymentIntentId,
}),
});
const data = await response.json();
if (data.success) {
console.log('Payment captured successfully');
} else {
console.error('Failed to capture payment', data.error);
}
}
Step 11: Handle Refunds
To process refunds for in-person payments:
// Add this endpoint to your Express server
app.post('/refund\_payment', async (req, res) => {
try {
const paymentIntentId = req.body.payment_intent_id;
const amount = req.body.amount; // Optional: for partial refunds
const refundParams = {
payment\_intent: paymentIntentId,
};
if (amount) {
refundParams.amount = amount;
}
const refund = await stripe.refunds.create(refundParams);
res.json({ success: true, refund: refund });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Call this from your frontend
async function refundPayment(paymentIntentId, amount = null) {
const body = { payment_intent_id: paymentIntentId };
if (amount) {
body.amount = amount;
}
const response = await fetch('/refund\_payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await response.json();
if (data.success) {
console.log('Payment refunded successfully');
} else {
console.error('Failed to refund payment', data.error);
}
}
Step 12: Implement Receipt Printing (Optional)
If your reader supports printing receipts (like the WisePOS E), you can print receipts:
async function printReceipt(paymentIntent) {
// Create receipt
const receipt = {
merchant: {
name: 'Your Store Name',
address: {
line1: '123 Main St',
line2: 'Suite 100',
city: 'San Francisco',
state: 'CA',
postalCode: '94111',
},
phoneNumber: '(555) 555-5555',
},
customer: {},
items: [
{
description: 'Product Name',
quantity: 1,
amount: paymentIntent.amount,
},
],
currency: paymentIntent.currency,
totalAmount: paymentIntent.amount,
paymentIntent: paymentIntent,
};
// Print the receipt
const printResult = await terminal.printReceipt(receipt);
if (printResult.error) {
console.error('Failed to print receipt', printResult.error);
} else {
console.log('Receipt printed successfully');
}
}
Step 13: Implement Test Mode
During development, use Stripe's test mode to simulate payments:
// Use a simulated reader
async function connectSimulatedReader() {
const config = { simulated: true };
const discoverResult = await terminal.discoverReaders(config);
if (discoverResult.error) {
console.error('Failed to create simulated reader', discoverResult.error);
return;
}
const simulatedReader = discoverResult.discoveredReaders[0];
const connectResult = await terminal.connectReader(simulatedReader);
if (connectResult.error) {
console.error('Failed to connect to simulated reader', connectResult.error);
return;
}
console.log('Connected to simulated reader');
}
Step 14: Implement Proper Error Handling
Handle potential errors that might occur during the payment process:
function handlePaymentError(error) {
switch (error.code) {
case 'card\_declined':
showError('Card was declined. Please try another card.');
break;
case 'expired\_card':
showError('Card is expired. Please try another card.');
break;
case 'processing\_error':
showError('An error occurred while processing the card. Please try again.');
break;
case 'incorrect\_cvc':
showError('Incorrect CVC. Please try again.');
break;
case 'insufficient\_funds':
showError('Insufficient funds. Please try another card.');
break;
default:
showError('An unexpected error occurred. Please try again.');
break;
}
}
function showError(message) {
// Display error message to the user
const errorElement = document.getElementById('error-message');
errorElement.textContent = message;
errorElement.style.display = 'block';
}
Step 15: Build a Complete UI
Here's a simple UI example to tie everything together:
Stripe Terminal Payment
Stripe Terminal Payment
Reader Status: Disconnected
Payment Details
And the corresponding JavaScript file:
let terminal;
let discoveredReaders = [];
let connectedReader = null;
document.addEventListener('DOMContentLoaded', () => {
// Initialize Stripe Terminal
terminal = StripeTerminal.create({
onFetchConnectionToken: fetchConnectionToken,
onUnexpectedReaderDisconnect: unexpectedDisconnect,
});
// Button event listeners
document.getElementById('discover-button').addEventListener('click', discoverReaders);
document.getElementById('connect-reader-button').addEventListener('click', connectToSelectedReader);
document.getElementById('simulate-button').addEventListener('click', connectSimulatedReader);
document.getElementById('collect-payment-button').addEventListener('click', collectPaymentHandler);
});
async function fetchConnectionToken() {
const response = await fetch('/connection\_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
return data.secret;
}
function unexpectedDisconnect() {
updateReaderStatus('Disconnected (Unexpected)');
connectedReader = null;
document.getElementById('collect-payment-button').disabled = true;
showError('Reader disconnected unexpectedly. Please reconnect.');
}
async function discoverReaders() {
updateReaderStatus('Discovering readers...');
clearMessages();
try {
const config = { simulated: false };
const discoverResult = await terminal.discoverReaders(config);
if (discoverResult.error) {
throw new Error(discoverResult.error.message);
}
discoveredReaders = discoverResult.discoveredReaders;
if (discoveredReaders.length === 0) {
updateReaderStatus('No readers found');
return;
}
updateReaderStatus(`Found ${discoveredReaders.length} readers`);
document.getElementById('connect-reader-button').disabled = false;
} catch (error) {
showError(`Error discovering readers: ${error.message}`);
updateReaderStatus('Discovery failed');
}
}
async function connectToSelectedReader() {
if (discoveredReaders.length === 0) {
showError('No readers discovered. Please discover readers first.');
return;
}
clearMessages();
updateReaderStatus('Connecting to reader...');
try {
const selectedReader = discoveredReaders[0]; // Use the first reader for simplicity
const connectResult = await terminal.connectReader(selectedReader);
if (connectResult.error) {
throw new Error(connectResult.error.message);
}
connectedReader = connectResult.reader;
updateReaderStatus(`Connected to ${connectedReader.label}`);
document.getElementById('collect-payment-button').disabled = false;
showSuccess(`Connected to reader: ${connectedReader.label}`);
} catch (error) {
showError(`Error connecting to reader: ${error.message}`);
updateReaderStatus('Connection failed');
}
}
async function connectSimulatedReader() {
clearMessages();
updateReaderStatus('Connecting to simulated reader...');
try {
const config = { simulated: true };
const discoverResult = await terminal.discoverReaders(config);
if (discoverResult.error) {
throw new Error(discoverResult.error.message);
}
const simulatedReader = discoverResult.discoveredReaders[0];
const connectResult = await terminal.connectReader(simulatedReader);
if (connectResult.error) {
throw new Error(connectResult.error.message);
}
connectedReader = connectResult.reader;
updateReaderStatus('Connected to simulated reader');
document.getElementById('collect-payment-button').disabled = false;
showSuccess('Connected to simulated reader');
} catch (error) {
showError(`Error connecting to simulated reader: ${error.message}`);
updateReaderStatus('Simulated connection failed');
}
}
async function collectPaymentHandler() {
if (!connectedReader) {
showError('No reader connected. Please connect to a reader first.');
return;
}
clearMessages();
const amountInput = document.getElementById('amount');
const currencySelect = document.getElementById('currency');
const amount = parseInt(amountInput.value, 10);
const currency = currencySelect.value;
if (isNaN(amount) || amount <= 0) {
showError('Please enter a valid amount.');
return;
}
updateReaderStatus('Processing payment...');
try {
// Create PaymentIntent on server
const response = await fetch('/create_payment_intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: amount,
currency: currency,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to create payment intent');
}
const clientSecret = data.client\_secret;
// Retrieve PaymentIntent
const retrieveResult = await terminal.retrievePaymentIntent(clientSecret);
if (retrieveResult.error) {
throw new Error(retrieveResult.error.message);
}
updateReaderStatus('Collecting payment method...');
// Collect payment method
const result = await terminal.collectPaymentMethod(retrieveResult.paymentIntent);
if (result.error) {
throw new Error(result.error.message);
}
updateReaderStatus('Processing payment...');
// Process payment
const processResult = await terminal.processPayment(result.paymentIntent);
if (processResult.error) {
throw new Error(processResult.error.message);
}
if (processResult.paymentIntent.status === 'succeeded') {
updateReaderStatus('Payment successful');
showSuccess(`Payment successful! Amount: ${amount/100} ${currency.toUpperCase()}`);
} else {
updateReaderStatus(`Payment status: ${processResult.paymentIntent.status}`);
showError(`Payment not successful. Status: ${processResult.paymentIntent.status}`);
}
} catch (error) {
showError(`Payment error: ${error.message}`);
updateReaderStatus('Payment failed');
}
}
function updateReaderStatus(status) {
document.getElementById('reader-status').textContent = `Reader Status: ${status}`;
}
function showError(message) {
const errorElement = document.getElementById('error-message');
errorElement.textContent = message;
errorElement.style.display = 'block';
document.getElementById('success-message').style.display = 'none';
}
function showSuccess(message) {
const successElement = document.getElementById('success-message');
successElement.textContent = message;
successElement.style.display = 'block';
document.getElementById('error-message').style.display = 'none';
}
function clearMessages() {
document.getElementById('error-message').style.display = 'none';
document.getElementById('success-message').style.display = 'none';
}
Step 16: Test Thoroughly
Before going live, thoroughly test your integration:
Step 17: Go Live
When you're ready to go live:
Step 18: Monitor and Maintain
After going live:
Conclusion
Stripe Terminal provides a powerful way to accept in-person payments while maintaining a unified payment experience across channels. By following this guide, you've learned how to set up and integrate Stripe Terminal into your application, from connecting readers to processing payments and handling refunds. Remember to thoroughly test your integration before going live and to keep your software and hardware up to date for the best experience.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.