McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Building a Terminal App with Flutter
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Building a Terminal App with Flutter

Build a Flutter app with three layers: a UI layer for the cashier (order building, payment status), a service layer that communicates with your backend API, and a state management layer (Provider, Riverpod, or Bloc) that tracks terminal state and order status. The Flutter app never talks to Paystack directly. It sends requests to your backend, which handles Paystack Terminal API calls and webhook processing.

In this article

  1. 1. Flutter Terminal App Architecture
  2. 2. Project Structure
  3. 3. API Service Layer
  4. 4. State Management for Terminal Status
  5. 5. Designing the Tablet UI
  6. 6. Handling Offline Scenarios
  7. 7. Testing the Flutter Terminal App

Flutter Terminal App Architecture

Your Flutter app sits between the cashier and your backend. It never touches Paystack APIs directly.

Frontend (Flutter): Displays the product catalog, lets the cashier build orders, shows the payment status, and triggers charge requests.

Backend (Node.js/Express): Stores products, orders, and terminal assignments. Calls the Paystack Terminal API to push payments. Receives webhooks and pushes status updates to the Flutter app.

Why not call Paystack from Flutter? The Terminal API requires your secret key. Secret keys must never exist in frontend code, mobile or web. Your backend is the only component that holds the secret key and makes Paystack API calls.

The Flutter app communicates with your backend over HTTP. For real-time updates (payment status changes), use Server-Sent Events (SSE) or periodic polling. WebSockets are another option but add complexity.

Project Structure

Organize your Flutter project around features, not layers.

// Flutter project structure
// lib/
//   main.dart
//   models/
//     product.dart
//     order.dart
//     terminal_status.dart
//   services/
//     api_service.dart        // HTTP calls to your backend
//     terminal_service.dart   // Terminal-specific API calls
//   providers/
//     order_provider.dart     // Order state management
//     terminal_provider.dart  // Terminal state management
//   screens/
//     pos_screen.dart         // Main POS interface
//     payment_screen.dart     // Payment status screen
//     settings_screen.dart    // Terminal selection, config
//   widgets/
//     product_grid.dart
//     order_summary.dart
//     payment_status_card.dart

models/ define your data structures. services/ handle HTTP communication. providers/ manage state. screens/ are full-page views. widgets/ are reusable components.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

API Service Layer

The API service handles all communication with your backend. Here is the core structure in Dart-like pseudocode that maps to your Node.js backend endpoints.

// The Flutter app calls your backend like this:
// POST /api/orders - create an order
// POST /api/terminal/charge - push payment to terminal
// GET  /api/terminal/:id/status - check terminal state
// GET  /api/orders/:id - get order details

// Your backend (Node.js) handles these and calls Paystack Terminal API:
// api-service.js (backend)
var express = require('express');
var router = express.Router();

router.post('/api/orders', async function(req, res) {
  var items = req.body.items;
  var terminalId = req.body.terminal_id;

  var total = items.reduce(function(sum, item) {
    return sum + (item.price * item.quantity);
  }, 0);

  var order = await db.query(
    'INSERT INTO orders (items, total_amount, terminal_id, status) ' +
    'VALUES ($1, $2, $3, $4) RETURNING *',
    [JSON.stringify(items), total, terminalId, 'draft']
  );

  return res.json(order.rows[0]);
});

router.post('/api/terminal/charge', async function(req, res) {
  var orderId = req.body.order_id;
  var result = await chargeOrderAtTerminal(orderId);
  return res.json(result);
});

router.get('/api/terminal/:id/status', async function(req, res) {
  var state = await getTerminalState(req.params.id);
  return res.json(state);
});

The Flutter app makes HTTP calls to these endpoints. It receives JSON responses and updates the UI accordingly.

State Management for Terminal Status

The terminal can be in several states. Your state management needs to track the current state and update the UI reactively.

// terminal-state-backend.js
// Backend endpoint that Flutter polls for status
router.get('/api/terminal/:id/payment-status/:reference', async function(req, res) {
  var reference = req.params.reference;

  var payment = await db.query(
    'SELECT status, amount, completed_at FROM terminal_transactions WHERE reference = $1',
    [reference]
  );

  if (payment.rows.length === 0) {
    return res.json({ status: 'unknown' });
  }

  return res.json(payment.rows[0]);
});

// Flutter polls this endpoint every 2 seconds while waiting for payment
// States the Flutter app tracks:
// - idle: terminal ready for new payment
// - pushing: sending payment request to terminal
// - waiting_for_card: terminal displaying amount, waiting for customer
// - processing: card presented, payment being processed
// - success: payment completed
// - failed: payment declined or error
// - timeout: no response within the timeout window

Polling interval. Poll every 2 seconds while a payment is active. Stop polling once you get a terminal status (success, failed, or timeout). Do not poll when the terminal is idle.

Optimistic UI. When the cashier taps "Charge," immediately show "Sending to terminal..." without waiting for the API response. Update based on what comes back. This makes the app feel responsive.

Designing the Tablet UI

POS apps live on tablets, not phones. Design for a landscape orientation on a 10-inch screen.

Split view layout. Left side: product catalog as a grid of large, tappable cards. Right side: current order summary with item list, quantities, and total. Bottom right: "Charge" button, large and prominent.

Minimum touch target: 56x56 logical pixels. Cashiers tap quickly and sometimes inaccurately. Make buttons big. Product cards should be at least 100x100 pixels.

Visual feedback on every tap. Ripple effects, color changes, haptic feedback. The cashier needs to know their tap registered, especially in a noisy environment.

Status colours. Use consistent colours for payment states: blue for waiting, green for success, red for failure, yellow for warning. The cashier should be able to read the status from across the counter.

Keep it simple. A POS screen should have no more than 3 to 5 actions visible at any time. Hide advanced features (reports, settings, refunds) behind a menu. The main screen is for building orders and charging customers.

Handling Offline Scenarios

Internet connectivity in African retail environments is not guaranteed. Build your Flutter app to handle interruptions.

Cache the product catalog. Store products locally using SQLite or Hive. The cashier can build orders without internet. When connectivity returns, sync any pending orders to the backend.

Queue payment requests. If the network is down when the cashier taps "Charge," queue the payment locally. Show "Payment will be sent when internet is available." Process the queue when connectivity returns.

Show connectivity status. Display a small indicator (green dot for online, red dot for offline) in the app bar. The cashier should always know whether the app is connected.

// connectivity-check.js (backend health endpoint)
router.get('/api/health', function(req, res) {
  return res.json({ status: 'ok', timestamp: Date.now() });
});

// The Flutter app pings this endpoint every 10 seconds
// If it fails 3 times in a row, show the offline indicator

Do not allow charges when offline. Even though you can queue the request, the terminal needs internet to process card payments. Tell the cashier: "Terminal payments require internet. Accept cash or wait for connectivity."

Testing the Flutter Terminal App

Test at three levels: unit tests for business logic, widget tests for UI, and integration tests for the full flow.

Mock the backend. Your Flutter tests should not hit your real backend. Use a mock HTTP client that returns predefined responses for each endpoint. This lets you test every state transition without a running server.

Test every payment state. Simulate success, failure, timeout, and network error. Verify the UI shows the correct message and buttons for each state.

Test on a real tablet. Emulators do not replicate the feel of tapping a physical screen. Test on an actual 10-inch tablet to verify touch targets, scroll behaviour, and readability.

Test with a real terminal. Before going live, test the full flow with a physical Paystack Terminal device and test cards. Verify that the Flutter app shows the correct status at each stage.

For broader testing strategies, see the complete testing guide.

Key Takeaways

  • ✓The Flutter app is the cashier frontend. It talks to your backend API, not to Paystack directly. The backend handles Terminal API calls and webhooks.
  • ✓Use a state management solution (Provider, Riverpod, or Bloc) to track order state and terminal status across the app.
  • ✓Design for tablets. POS apps are typically used on 10-inch tablets. Use a split-view layout: item catalog on the left, order summary on the right.
  • ✓Handle offline scenarios. Cache the product catalog locally so the cashier can build orders even without internet. Queue the payment push for when connectivity returns.
  • ✓Use Server-Sent Events or polling to get real-time payment status updates from your backend.
  • ✓Build large, finger-friendly touch targets. Cashiers use the app standing up, often with one hand.

Frequently Asked Questions

Should I use Flutter or a web app for my POS frontend?
Flutter gives you a native feel, better offline support, and access to hardware features like barcode scanners and Bluetooth printers. A web app (PWA) is faster to develop and deploy. For a basic POS, start with a web app. For a feature-rich POS with offline capability and hardware integration, choose Flutter.
Can the Flutter app communicate directly with the Paystack Terminal device?
No. The Flutter app communicates with your backend, which communicates with the Paystack Terminal API. The Terminal API is the only way to send commands to the device. There is no direct Bluetooth or local network connection between your app and the terminal.
Which state management solution should I use?
For a POS app with moderate complexity, Riverpod or Provider work well. Bloc is more structured but adds boilerplate. If you are already comfortable with one solution, use that. The choice matters less than consistent usage across the app.
How do I handle app updates without disrupting active payments?
Check for updates on app launch, not during active transactions. If an update is available, show a notification and let the cashier update when they are between customers. Never force an update while a payment is in progress.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Paystack Terminal and In-Person Payments: Complete Guide

Paystack Terminal lets you accept card payments at a physical location using Paystack POS devices. You push a payment request from your server to the terminal, the customer taps or inserts their card, and you get a webhook when it succeeds. This guide covers the full integration.

Read more

Terminal Invoice Payments via API

When a customer walks in with an invoice to pay, your system needs to push the exact invoice amount to the terminal, track which invoice the payment covers, and mark it as settled once the card payment completes. This guide covers the full flow: from invoice lookup to terminal push to webhook reconciliation.

Read more

Building a Terminal App with React Native

If your team already knows React, React Native is the fastest path to a native POS app. This guide shows you how to structure a React Native terminal app, communicate with your backend, manage payment state, and design a tablet-first interface that cashiers can operate under real-world conditions.

Read more

Retail Checkout with Paystack Terminal and Barcode Scanning

Build a retail POS checkout by combining barcode scanning with Paystack Terminal. This guide covers looking up products by barcode from your inventory API, creating an invoice with line items, pushing it to the Paystack Terminal device, and handling the charge.success webhook to print a receipt.

Read more

Paystack Payment Pages: When No-Code Beats Code

Paystack Payment Pages let you collect payments without writing any code. Create a page, share the link, and customers can pay. This guide covers when payment pages are the right tool, how to create and customize them, and when you should switch to a code-based integration.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.