Retail Checkout with Paystack Terminal and Barcode Scanning
Scan a product barcode to look up price and SKU from your inventory API. Build an order object with line items. Call POST /terminal/:terminal_id/event to send a purchase event to the Paystack Terminal. The terminal displays the amount and waits for card tap or PIN. When payment completes, Paystack fires a terminalaction.process.completed event — handle it to mark the sale as complete and print a receipt.
POS Architecture
The retail checkout flow with Paystack Terminal:
- Cashier scans product barcodes → your POS app builds a cart
- Cashier hits "Charge" → your backend calls POST /terminal/:id/event
- Terminal displays amount → customer taps card or enters PIN
- Terminal sends result → your backend receives terminalaction.process.completed webhook
- Receipt printed, inventory updated
Your POS app (web or React Native) talks to your backend, which talks to Paystack. The terminal never talks to your app directly.
Push Invoice to Terminal
// POST /api/checkout — your POS backend endpoint
app.post('/api/checkout', async (req, res) => {
var { terminalId, items } = req.body;
// items: [{ name, sku, quantity, unit_price }]
var total = items.reduce((sum, item) => sum + item.quantity * item.unit_price, 0);
// Push payment to terminal
var response = await fetch(
'https://api.paystack.co/terminal/' + terminalId + '/event',
{
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'purchase',
action: {
amount: Math.round(total * 100), // kobo
currency: 'NGN',
reference: 'pos_' + Date.now(),
description: items.map(i => i.name).join(', '),
}
}),
}
);
var data = await response.json();
if (!data.status) {
return res.status(400).json({ error: data.message });
}
// Store the sale in DB with status: 'awaiting_payment'
var sale = await db.sales.create({
reference: data.data.reference || ('pos_' + Date.now()),
terminal_id: terminalId,
items,
total,
status: 'awaiting_payment',
});
res.json({ sale_id: sale.id, total });
});
Webhook: Mark Sale Complete
// Handle terminal payment completion
if (event.event === 'terminalaction.process.completed') {
var action = event.data;
if (action.status === 'success') {
await db.sales.update(
{
status: 'paid',
paid_at: new Date().toISOString(),
terminal_reference: action.reference,
},
{ where: { reference: action.reference } }
);
// Update inventory
var sale = await db.sales.findByReference(action.reference);
for (var item of sale.items) {
await db.inventory.decrement({ sku: item.sku, by: item.quantity });
}
// Trigger receipt print via your receipt printer API
await printReceipt(sale);
}
}
Learn More
This guide is part of the Paystack Terminal guide series.
Key Takeaways
- ✓Use POST /terminal/:terminal_id/event with type: "purchase" and action.amount to push a payment request to a terminal.
- ✓Barcode scanning is done in your app — scan, look up price, build cart, then push total to the terminal.
- ✓The terminal fires a terminalaction.process.completed event when the customer completes payment.
- ✓Build an inventory lookup API that maps barcode → { name, price, sku }. This is your POS product catalog.
- ✓Each Terminal has a unique terminal_id (TRM_xxxx). Store terminal IDs against physical devices in your database.
- ✓Handle the webhook in under 500ms — the terminal waits for your acknowledgment before showing success to the cashier.
Frequently Asked Questions
- How do I get the terminal_id for a specific physical device?
- Call GET /terminal in the Paystack API to list all terminals on your account. Each device returns a terminal_id (TRM_xxxx), a serial_number, and a device_name. Store these in your database and let cashiers select which terminal to charge from your POS interface.
- Can I send line items to the Paystack Terminal display?
- The Terminal API purchase event accepts amount, currency, reference, and description. The description field can contain a summary of items. The terminal displays the total amount prominently. For a detailed receipt with line items, generate the receipt on your side after payment completes.
- What if the customer does not complete payment on the terminal?
- The terminal action will time out and Paystack fires terminalaction.process.completed with status: "timeout" or "cancelled". Handle this by marking the sale as cancelled and resetting the POS cart. You can also call POST /terminal/:id/event with type: "print" to clear the terminal screen.
- Can I use any barcode scanner with Paystack Terminal?
- Paystack Terminal does not include a built-in barcode scanner. Use a separate USB or Bluetooth barcode scanner connected to the cashier's POS device (tablet or computer). Your POS app reads the scan input and looks up the product. The terminal only handles the payment step.
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