/stripe-guides

How to use Stripe Terminal for in-person payments?

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.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to use Stripe Terminal for in-person payments?

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:

  • Go to stripe.com and click "Start now"
  • Fill in your email, name, and create a password
  • Complete the verification process
  • Add your business details in the Dashboard

 

Step 3: Set Up Your Stripe Terminal Account

 

To enable Terminal on your account:

  • Log in to your Stripe Dashboard
  • Navigate to "Terminal" in the left sidebar
  • Follow the onboarding flow to activate Terminal for your account
  • Order a compatible card reader (Stripe offers several options like BBPOS WisePOS E, Verifone P400, or BBPOS Chipper 2X BT)

 

Step 4: Create a Terminal Location

 

Locations represent physical places where you accept in-person payments:

  • In your Stripe Dashboard, go to Terminal > Locations
  • Click "Add location"
  • Enter the address and details of your physical store/location
  • Save the location

 

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:

  • Test with simulated readers first
  • Use Stripe's test cards (e.g., 4242 4242 4242 4242 for success)
  • Test edge cases: refunds, declines, network issues
  • Test different payment amounts and currencies
  • Verify that receipts are generated correctly (if applicable)

 

Step 17: Go Live

 

When you're ready to go live:

  • Switch from test to live mode in your Stripe Dashboard
  • Update your API keys to use the live keys instead of test keys
  • Perform a final end-to-end test with a real card and small amount
  • Monitor your transactions in the Stripe Dashboard

 

Step 18: Monitor and Maintain

 

After going live:

  • Set up webhooks to receive real-time notifications of payment events
  • Regularly update your Stripe Terminal SDK to get the latest features and security updates
  • Monitor transaction logs and address any issues promptly
  • Keep your card readers updated with the latest firmware

 

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.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022