Paystack Terminal and In-Person Payments: Complete Guide
Paystack Terminal is a POS device that accepts card payments at physical locations. You integrate it by pushing payment requests from your backend to the terminal via API. The customer taps or inserts their card. Paystack processes the payment and sends you a webhook with the result. You can also build custom Android apps for the terminal hardware or use the Virtual Terminal for phone and email-based card collection.
What Paystack Terminal Is (and Is Not)
Paystack Terminal is a POS (Point of Sale) device that connects to your Paystack account and accepts card payments at physical locations. It is the bridge between online payment infrastructure and in-person transactions.
What it is:
- A physical Android-based device that accepts chip-and-pin, contactless (NFC), and magnetic stripe card payments.
- An API-controlled device. You push payment requests to it from your backend. You do not manually type amounts on the device (unless you build a custom app that does that).
- Part of the Paystack ecosystem. Transactions appear in your Paystack Dashboard alongside your online payments. Same settlement, same webhooks, same reporting.
What it is not:
- It is not a standalone POS like the bank-issued terminals you see at supermarkets. Those work independently. Paystack Terminal is tied to your Paystack integration and controlled via API.
- It is not available in every country Paystack operates in. Check the Paystack Terminal page for current availability.
- It does not handle cash. It is a card payment device only.
How to get one. You request a terminal through your Paystack Dashboard. Paystack ships the device to your business address. Each terminal has a unique ID that you use in API calls. You can register multiple terminals for different locations or checkout counters.
For the full picture of how Terminal fits into the Paystack platform, see the complete Paystack engineering guide.
Pushing Payment Requests to the Terminal
The core flow is simple. Your backend sends a payment request to a specific terminal. The terminal displays the amount. The customer taps or inserts their card. Paystack processes the payment and sends you a webhook.
// push-payment.ts
import axios from 'axios';
async function pushPaymentToTerminal(
terminalId: string,
amountInKobo: number,
reference: string
) {
const response = await axios.post(
'https://api.paystack.co/terminal/commission_device/send_event',
{
serial_number: terminalId,
type: 'invoice',
action: 'process',
data: {
id: reference,
offline_reference: reference,
amount: amountInKobo,
},
},
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
}
);
return response.data;
}
// Push a KES 1,500 payment to terminal
// Note: amount is in the smallest currency unit
// For NGN, that's kobo (150000 = NGN 1,500)
// For KES, check Paystack docs for the unit used
await pushPaymentToTerminal(
'TERMINAL_SERIAL_HERE',
150000,
`order_${Date.now()}`
);
The push model. This is different from traditional POS terminals where the cashier types the amount on the device. With Paystack Terminal, your system calculates the total (from your inventory, cart, or invoice) and pushes it to the device. The cashier does not need to type anything. This eliminates keying errors and connects the payment directly to the order in your system.
What happens on the terminal. The device screen shows the amount and prompts the customer to tap, insert, or swipe their card. The customer enters their PIN if required. The terminal communicates with Paystack's servers, which communicate with the card network and issuing bank.
What you get back. The API call itself returns a 200 confirming the event was sent to the terminal. The actual payment result comes via webhook. Do not treat the 200 as "payment successful." It only means "the terminal received the instruction."
Terminal Invoice Payments
For businesses that generate invoices before collecting payment (restaurants, repair shops, professional services), Paystack Terminal supports invoice-based flows.
The pattern:
- Your system creates an invoice or order with a total amount and a unique reference.
- When the customer is ready to pay, your cashier clicks "Collect Payment" in your POS interface.
- Your backend pushes the invoice amount to the terminal.
- The customer pays on the terminal.
- Your webhook handler marks the invoice as paid.
// invoice-terminal-flow.ts
// Step 1: Create the invoice in your system
const invoice = await db.invoices.create({
customerId: 'cust_123',
items: [
{ name: 'Laptop Repair', amount: 350000 }, // KES 3,500 in smallest unit
{ name: 'Screen Protector', amount: 50000 }, // KES 500
],
total: 400000, // KES 4,000
reference: `inv_${Date.now()}`,
status: 'pending',
terminalId: 'TERMINAL_COUNTER_1',
});
// Step 2: Push to terminal when customer is ready
await pushPaymentToTerminal(
invoice.terminalId,
invoice.total,
invoice.reference
);
// Step 3: Handle the webhook
app.post('/webhooks/paystack', async (req, res) => {
// Verify signature first
const event = req.body;
if (event.event === 'charge.success') {
const reference = event.data.reference;
// Find and update the invoice
await db.invoices.update({
where: { reference },
data: {
status: 'paid',
paidAt: new Date(),
paystackTransactionId: event.data.id,
},
});
// Trigger receipt printing, inventory update, etc.
}
res.sendStatus(200);
});
Partial payments. Paystack Terminal does not natively support splitting a payment across two cards. If a customer wants to pay half with one card and half with another, your application needs to handle this by pushing two separate payment requests with adjusted amounts.
Tips and service charges. If your business collects tips (common in restaurants), calculate the tip in your application before pushing the final amount to the terminal. The terminal does not have a "add tip" prompt built in. You need to build that into your cashier-facing UI.
Custom Terminal Apps
Paystack Terminal runs Android. This means you can build custom applications that run directly on the terminal hardware. Instead of using the default Paystack app that just shows an amount and waits for a card, you can build a full checkout experience.
Use cases for custom terminal apps:
- Retail checkout with barcode scanning. Scan product barcodes on the terminal, build a cart, calculate totals with discounts, and process the payment, all on one device.
- Restaurant ordering. Display a menu, let the customer select items, show the total, and collect payment. No separate POS computer needed.
- Loyalty points. Check and display the customer's loyalty balance before payment. Apply points as a discount. Update the balance after payment.
- Branded experience. Show your company logo, colours, and custom prompts instead of the generic Paystack checkout screen.
Building a custom terminal app. You build an Android app using the Paystack Terminal SDK. The SDK provides methods to initiate a card payment from within your app.
// Example: Android (Kotlin) terminal payment initiation
// This runs ON the terminal device itself
// Initialize the terminal SDK
val terminal = PaystackTerminal.getInstance()
// When user clicks "Pay" in your custom UI
fun processPayment(amountInKobo: Long, reference: String) {
val paymentRequest = PaymentRequest.Builder()
.setAmount(amountInKobo)
.setReference(reference)
.build()
terminal.processPayment(paymentRequest, object : PaymentCallback {
override fun onSuccess(transaction: Transaction) {
// Payment successful
// Update your local UI
showReceipt(transaction)
}
override fun onError(error: TerminalError) {
// Payment failed
showError(error.message)
}
})
}
Deployment. You deploy your custom app to your terminals through the Paystack Dashboard or Terminal Management API. You can push updates to all your terminals at once or target specific devices.
Important limitation. Custom terminal apps require approval from Paystack. You cannot just sideload any APK onto the device. Submit your app for review, and Paystack will deploy it to your terminals once approved.
For a step-by-step tutorial on building a terminal app with Flutter, see the Flutter terminal app guide.
Virtual Terminal: Card Payments Without Hardware
Paystack Virtual Terminal lets you accept card payments without a physical device. You (the merchant) enter the customer's card details into a form on your Paystack Dashboard. The payment processes as a card-not-present transaction.
When to use Virtual Terminal:
- Phone orders. A customer calls to place an order and reads their card number over the phone. You enter it into the Virtual Terminal.
- Email orders. A customer emails their order and authorises payment. You enter their card details.
- Backup when hardware fails. If your physical terminal breaks or loses connectivity, the Virtual Terminal lets you keep accepting payments from a laptop.
How it works:
- Log into your Paystack Dashboard.
- Go to the Virtual Terminal section.
- Enter the payment amount and the customer's card details (number, expiry, CVV).
- Submit. Paystack processes the payment.
- The transaction appears in your dashboard and triggers webhooks like any other transaction.
Security considerations. Virtual Terminal transactions are card-not-present (CNP). They carry higher fraud risk because the card is not physically present. Card networks charge higher interchange fees for CNP transactions. Chargeback rates tend to be higher too.
Best practices for Virtual Terminal:
- Verify the customer's identity before processing. Ask for their name, address, and phone number.
- Keep a record of the customer's verbal or written authorization.
- Do not process Virtual Terminal payments for walk-in customers who have their card with them. Use the physical terminal instead.
- Set a maximum transaction amount for Virtual Terminal payments. This limits your exposure if something goes wrong.
API access. Virtual Terminal is primarily a Dashboard feature, not an API feature. You cannot programmatically submit card details through an API without PCI DSS compliance. If you need programmatic card-not-present payments, use the standard Paystack Charge API with a tokenized card.
Connecting Terminal to Your Inventory System
The real power of Paystack Terminal comes when you connect it to your existing business systems. A standalone POS that just accepts payments is useful. A POS that also updates inventory, prints receipts, and syncs with your accounting system is a business tool.
Architecture pattern:
// System architecture for terminal + inventory
//
// [Cashier UI] --> [Your Backend API] --> [Paystack Terminal API]
// | | |
// | v |
// | [Your Database] |
// | (orders, inventory) |
// | ^ |
// | | |
// | [Webhook Handler] <--- [Paystack Webhook]
// | |
// v v
// [Receipt Printer] [Inventory Update]
The flow in detail:
- Cashier scans items or selects them in your POS UI.
- Your backend creates an order record with line items and a total.
- Your backend pushes the total to the terminal.
- Customer pays on the terminal.
- Paystack sends a
charge.successwebhook to your backend. - Your webhook handler marks the order as paid and decrements inventory for each line item.
- Your backend sends a print command to the receipt printer (or displays a digital receipt).
// inventory-update-on-payment.ts
async function handleTerminalPaymentSuccess(
reference: string,
transactionId: number
) {
// Find the order by reference
const order = await db.orders.findUnique({
where: { reference },
include: { items: true },
});
if (!order) {
console.error(`Order not found for reference: ${reference}`);
return;
}
if (order.status === 'paid') {
// Already processed (idempotency)
return;
}
// Update order status
await db.orders.update({
where: { id: order.id },
data: {
status: 'paid',
paidAt: new Date(),
paystackTransactionId: transactionId,
},
});
// Decrement inventory for each item
for (const item of order.items) {
await db.products.update({
where: { id: item.productId },
data: {
stockQuantity: { decrement: item.quantity },
},
});
}
// Trigger receipt
await printReceipt(order);
}
Multi-terminal setups. If you have multiple terminals at different counters or locations, include the terminal ID in your order record. This lets you track which terminal processed each payment and troubleshoot issues at specific locations.
Terminal Event Webhooks
Terminal payments trigger the same webhooks as online payments. The primary event you care about is charge.success. But there are terminal-specific details in the webhook payload.
// Terminal charge.success webhook payload (simplified)
{
"event": "charge.success",
"data": {
"id": 1234567890,
"reference": "order_1689012345",
"amount": 400000,
"currency": "NGN",
"channel": "card",
"status": "success",
"authorization": {
"authorization_code": "AUTH_abc123",
"bin": "539923",
"last4": "7890",
"card_type": "mastercard DEBIT",
"bank": "Guaranty Trust Bank",
"country_code": "NG",
"brand": "Mastercard",
"reusable": false,
"signature": "SIG_xyz789"
},
"metadata": {
"terminal_id": "TERM_001",
"custom_fields": []
}
}
}
Key differences from online payments:
authorization.reusableis usuallyfalse. Terminal card authorizations are tied to the physical card being present. You cannot reuse them for recurring charges. For recurring payments, use online tokenization instead.- The
channelis always"card". Terminal only supports card payments. You will not see bank transfer, mobile money, or USSD through a terminal. - Terminal metadata. You can include the terminal ID or location in the metadata when pushing the payment. This helps with reporting and reconciliation across multiple locations.
Failed terminal payments. If the card is declined, the terminal shows an error to the customer. Your webhook handler receives a charge.failed event (if enabled). In practice, most failed terminal payments do not need webhook handling because the cashier and customer are standing right there and can try again immediately.
Timeout handling. If a customer walks away without completing the payment, the terminal eventually times out. Your order remains in "pending" status. Build a cleanup job that marks stale pending terminal orders as "expired" after a reasonable period (15 to 30 minutes).
Offline Behaviour and Connectivity
Paystack Terminal requires an internet connection to process card payments. The card data needs to reach Paystack's servers, which then communicate with the card network and the issuing bank. This is not optional.
What happens when connectivity drops:
- Before a transaction starts: The terminal shows a "no connection" indicator. You cannot push new payment requests to it. Your cashier UI should detect this (the API call to push the payment will fail) and show a message.
- During a transaction: If the connection drops after the card is read but before the authorization completes, the terminal will attempt to retry. The transaction may time out. The customer's bank may or may not have placed a hold on the funds. This is the worst scenario.
- After a transaction: If the payment was authorized but the webhook fails to reach your server, Paystack will retry the webhook. Your order stays in "pending" until the webhook arrives or you manually verify via the API.
Mitigating connectivity issues:
- Wired ethernet. If your terminal supports it, use a wired connection instead of WiFi. It is more reliable.
- Backup connectivity. Keep a mobile hotspot as backup. If your primary internet goes down, switch the terminal to the hotspot.
- Connection monitoring. Add a health check in your cashier UI that pings your backend every 30 seconds. If the backend is unreachable, show a warning before the cashier tries to process a payment.
- Fallback payment method. Accept mobile money (M-Pesa, MoMo) via your phone as a fallback when the terminal cannot connect. This is not ideal, but it means you do not lose the sale.
// connection-health-check.ts (cashier UI)
let isOnline = true;
async function checkConnection() {
try {
const response = await fetch('/api/health', {
signal: AbortSignal.timeout(5000),
});
isOnline = response.ok;
} catch {
isOnline = false;
}
}
// Check every 30 seconds
setInterval(checkConnection, 30000);
// Before pushing a payment
async function initiateTerminalPayment(orderId: string) {
if (!isOnline) {
showWarning(
'Internet connection appears to be down. ' +
'Terminal payments may not work. ' +
'Consider using M-Pesa as a fallback.'
);
return;
}
// Proceed with terminal payment push
await fetch(`/api/orders/${orderId}/pay-terminal`, { method: 'POST' });
}
Store-and-forward is limited. Unlike some traditional POS systems that can authorize payments offline and batch them later, Paystack Terminal does not support true offline authorization. The terminal needs to be online at the time of the transaction. If you need offline payment capability, you are looking at a different architecture (possibly mobile money or cash with manual reconciliation).
For the full set of Paystack integration patterns, including how Terminal fits alongside online payments, see the complete engineering guide.
Want to build full-stack applications with payment integrations? The Full-Stack AI Engineering course covers payment systems, APIs, and deployment at your own pace.
Key Takeaways
- ✓Paystack Terminal is a POS device you control via API. Push a payment request from your server, the customer taps their card on the terminal, and you get a webhook when the payment completes.
- ✓The Terminal API works through a push model. Your backend sends the amount and reference to the terminal. The terminal displays the amount and waits for the card. No polling required since webhooks notify you of the result.
- ✓Custom terminal apps let you build Android applications that run directly on the terminal hardware. Use this for inventory lookup, barcode scanning, or branded checkout experiences.
- ✓Virtual Terminal lets you accept card payments over the phone or email without a physical device. You enter the card details into a web form on your Paystack Dashboard.
- ✓Offline behaviour is limited. The terminal needs an internet connection to process card payments. If connectivity drops mid-transaction, the terminal queues the transaction and retries when the connection returns.
Frequently Asked Questions
- Is Paystack Terminal available in Kenya?
- Paystack Terminal availability varies by country and changes over time. Check the Paystack Terminal page at paystack.com/terminal for current availability in your country. Even if the physical terminal is not available in your country, you may be able to use the Virtual Terminal feature through your Dashboard for card-not-present transactions.
- Can I build my own app for the Paystack Terminal?
- Yes. Paystack Terminal runs Android, and you can build custom apps using the Paystack Terminal SDK. You need to submit your app for approval before deploying it to the device. This lets you build branded checkout experiences, inventory management, barcode scanning, and other features directly on the terminal hardware.
- Does Paystack Terminal work offline?
- No. Paystack Terminal requires an internet connection to process card payments. The device communicates with Paystack servers, which then contact the card network and issuing bank. If connectivity drops mid-transaction, the terminal retries but may time out. Keep a backup internet connection (mobile hotspot) and consider M-Pesa as a fallback for when the terminal cannot connect.
- What is the difference between Paystack Terminal and Virtual Terminal?
- Paystack Terminal is a physical POS device that accepts cards by tap, chip, or swipe. Virtual Terminal is a web form in your Paystack Dashboard where you manually enter card details for phone or email orders. Terminal is for in-person payments with the card present. Virtual Terminal is for remote payments when the customer reads their card details to you.
- Can I use Paystack Terminal for recurring payments?
- Terminal card authorizations are typically not reusable for recurring charges because they are tied to the physical card being present. For recurring payments, use online tokenization through the standard Paystack checkout flow. The customer pays once online, you store the authorization code, and you charge it on a schedule.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon