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. Restaurant Ordering and Terminal Payment Flow
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Restaurant Ordering and Terminal Payment Flow

Build an ordering system with open tabs: orders start when the customer sits down and grow as items are added. When the customer is ready to pay, the server closes the tab, optionally adds a tip, and pushes the total to the Paystack Terminal. For split bills, calculate each person's portion and push separate terminal payments for each. The backend manages tab state while the terminal handles payment collection.

In this article

  1. 1. How Restaurant POS Differs from Retail
  2. 2. Table and Tab Management
  3. 3. Splitting the Bill
  4. 4. Handling Tips
  5. 5. Kitchen Display Integration
  6. 6. Closing the Tab and Clearing the Table

How Restaurant POS Differs from Retail

In retail, the customer picks items, pays, and leaves. The order is complete before payment. In a restaurant, the order is open for the entire duration of the meal.

Open tabs. The customer sits down, and a tab opens. Over the next 30 to 90 minutes, items are added: appetisers, drinks, entrees, desserts. The total changes multiple times before the customer asks for the bill.

Payment comes last. Unlike retail where payment is immediate, restaurant payment happens after the service is complete. This means your system must track open tabs, display the running total, and only push to the terminal when the customer requests the bill.

Split bills. A table of 4 people might want to pay separately. Your system needs to divide the bill and process multiple terminal payments for the same table.

Tips. In many markets, customers add a tip. Your system needs to handle this: either adding it to the bill before the terminal charge, or processing it separately.

Table and Tab Management

// restaurant-tables.js
async function openTab(tableNumber, serverId) {
  var tab = await db.query(
    'INSERT INTO tabs (table_number, server_id, status, opened_at) ' +
    'VALUES ($1, $2, $3, NOW()) RETURNING id',
    [tableNumber, serverId, 'open']
  );

  await db.query(
    'UPDATE tables SET status = $1, current_tab_id = $2 WHERE table_number = $3',
    ['occupied', tab.rows[0].id, tableNumber]
  );

  return { tabId: tab.rows[0].id };
}

async function addItemToTab(tabId, productId, quantity, notes) {
  var product = await db.query('SELECT name, price FROM products WHERE id = $1', [productId]);
  var price = product.rows[0].price;

  await db.query(
    'INSERT INTO tab_items (tab_id, product_id, product_name, price, quantity, notes, status, added_at) ' +
    'VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())',
    [tabId, productId, product.rows[0].name, price, quantity, notes || '', 'pending']
  );

  // Send to kitchen display
  await sendToKitchen(tabId, productId, quantity, notes);

  // Recalculate tab total
  var total = await db.query(
    'SELECT SUM(price * quantity) as total FROM tab_items WHERE tab_id = $1',
    [tabId]
  );

  return { total: parseInt(total.rows[0].total) };
}

async function requestBill(tabId) {
  var total = await db.query(
    'SELECT SUM(price * quantity) as total FROM tab_items WHERE tab_id = $1',
    [tabId]
  );

  await db.query(
    'UPDATE tabs SET status = $1, total_amount = $2 WHERE id = $3',
    ['bill_requested', parseInt(total.rows[0].total), tabId]
  );

  return { tabId: tabId, total: parseInt(total.rows[0].total) };
}

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Splitting the Bill

// split-bill.js
async function splitBillEqually(tabId, numberOfPeople) {
  var tab = await db.query('SELECT total_amount FROM tabs WHERE id = $1', [tabId]);
  var total = tab.rows[0].total_amount;

  var perPerson = Math.ceil(total / numberOfPeople); // Round up to avoid underpayment

  var splits = [];
  var remaining = total;

  for (var i = 0; i < numberOfPeople; i++) {
    var amount = (i === numberOfPeople - 1) ? remaining : perPerson;
    splits.push({ person: i + 1, amount: amount });
    remaining -= perPerson;
  }

  return splits;
}

async function processTabSplit(tabId, splits, terminalId) {
  var results = [];

  for (var i = 0; i < splits.length; i++) {
    var split = splits[i];
    var reference = 'TAB-' + tabId + '-SPLIT-' + (i + 1) + '-' + Date.now().toString(36);

    await db.query(
      'INSERT INTO tab_payments (tab_id, split_number, amount, reference, status) ' +
      'VALUES ($1, $2, $3, $4, $5)',
      [tabId, i + 1, split.amount, reference, 'pending']
    );

    // Push to terminal - wait for each payment before starting the next
    await pushPaymentToTerminal(terminalId, split.amount, reference);

    // Wait for webhook... (in practice, this is handled asynchronously)
    results.push({ person: i + 1, amount: split.amount, reference: reference });
  }

  return results;
}

Process splits sequentially. The terminal can only handle one payment at a time. Push the first person's payment, wait for the webhook, then push the second person's payment. Show the server: "Person 1 of 4: tap card now."

Handle partial completion. If 3 of 4 people pay but the 4th card is declined, your system needs to track which splits are paid and which are not. Do not close the tab until all splits are settled.

Handling Tips

// tips.js
async function addTipToTab(tabId, tipAmount) {
  await db.query(
    'UPDATE tabs SET tip_amount = $1, total_with_tip = total_amount + $1 WHERE id = $2',
    [tipAmount, tabId]
  );

  var tab = await db.query('SELECT total_with_tip FROM tabs WHERE id = $1', [tabId]);
  return { totalWithTip: tab.rows[0].total_with_tip };
}

async function chargeTabWithTip(tabId, terminalId) {
  var tab = await db.query(
    'SELECT total_amount, tip_amount, COALESCE(total_with_tip, total_amount) as charge_amount FROM tabs WHERE id = $1',
    [tabId]
  );

  var chargeAmount = tab.rows[0].charge_amount;
  var reference = 'TAB-' + tabId + '-' + Date.now().toString(36);

  await pushPaymentToTerminal(terminalId, chargeAmount, reference);

  return { reference: reference, amount: chargeAmount };
}

Tip before or after payment? In some restaurants, the customer writes the tip on the receipt after the card is charged (post-auth tip adjustment). This is more complex and requires card preauthorization. The simpler approach: ask for the tip before pushing to the terminal. The server enters the tip on the POS app, the system adds it to the total, and the combined amount is charged in one transaction.

Tip reporting. Track tips separately from the base bill for accounting. Tips may have different tax treatment than service charges. Report tips per server for payroll purposes.

Kitchen Display Integration

When a server adds items to a tab, those items should appear on the kitchen display so the cooks can prepare them. This is separate from the payment flow but part of the same system.

// kitchen-display.js
async function sendToKitchen(tabId, productId, quantity, notes) {
  var tab = await db.query('SELECT table_number FROM tabs WHERE id = $1', [tabId]);
  var product = await db.query('SELECT name, category FROM products WHERE id = $1', [productId]);

  var kitchenOrder = {
    table: tab.rows[0].table_number,
    item: product.rows[0].name,
    quantity: quantity,
    notes: notes,
    category: product.rows[0].category, // 'appetizer', 'entree', 'dessert', 'drink'
    time: new Date().toISOString(),
  };

  // Broadcast to kitchen display screens via SSE or WebSocket
  broadcastToKitchen(kitchenOrder);
}

// Mark item as ready (kitchen taps "done")
async function markItemReady(tabItemId) {
  await db.query(
    'UPDATE tab_items SET status = $1, ready_at = NOW() WHERE id = $2',
    ['ready', tabItemId]
  );

  // Notify server's device
  // broadcastToServer(tabItemId, 'item_ready');
}

Kitchen display is not a payment feature. It is an operational feature. But it lives in the same POS system and shares the same tab data. Design your data model to support both workflows.

Closing the Tab and Clearing the Table

// close-tab.js
async function closeTab(tabId) {
  // Verify all payments are complete
  var unpaid = await db.query(
    'SELECT COUNT(*) as count FROM tab_payments WHERE tab_id = $1 AND status != $2',
    [tabId, 'completed']
  );

  if (parseInt(unpaid.rows[0].count) > 0) {
    return { error: true, message: 'Not all payments are complete' };
  }

  // Close the tab
  await db.query(
    'UPDATE tabs SET status = $1, closed_at = NOW() WHERE id = $2',
    ['closed', tabId]
  );

  // Free the table
  var tab = await db.query('SELECT table_number FROM tabs WHERE id = $1', [tabId]);
  await db.query(
    'UPDATE tables SET status = $1, current_tab_id = NULL WHERE table_number = $2',
    ['available', tab.rows[0].table_number]
  );

  return { error: false, message: 'Tab closed, table available' };
}

Table turnover tracking. Record how long each table was occupied (opened_at to closed_at). This helps restaurant managers optimize seating and estimate wait times.

Key Takeaways

  • ✓Restaurant orders are open tabs that grow over time. Items are added throughout the meal. The total is calculated at payment time, not order creation time.
  • ✓Support bill splitting: split equally among N people, split by specific items per person, or custom amounts per person.
  • ✓Handle tips by adding the tip amount to the tab total before pushing to the terminal, or by processing the tip as a separate charge.
  • ✓Send orders to the kitchen display system (KDS) when items are added. Payment is a separate step that happens later.
  • ✓Assign tables to servers. The server who manages the table is the one who processes the payment on their section's terminal.
  • ✓Track table status: available, occupied, ordering, bill_requested, paying, cleared.

Frequently Asked Questions

How do I handle a customer who wants to add items after requesting the bill?
Allow it. The server adds items to the tab, which updates the total. If the payment has not been pushed to the terminal yet, simply recalculate. If it has been pushed, cancel the pending terminal payment, add the items, and push a new payment with the updated total.
Can I use Paystack Terminal for pay-at-table (the terminal goes to the customer)?
This depends on the terminal model. If the terminal is portable (battery-powered with wireless connectivity), the server can bring it to the table. Push the payment from your POS system and let the customer tap their card at the table. Not all terminal models support this portability.
How do I handle service charges vs tips?
A service charge is a mandatory fee added to the bill (common for large parties). Add it as a line item on the tab. A tip is voluntary and entered by the customer or server after the bill is presented. Track them separately in your database for accounting purposes.
What if the terminal payment fails for one person in a split bill?
Keep the tab open for the unpaid split. The other splits that were paid successfully remain paid. The server can retry the failed payment with a different card, or the customer can pay their portion in cash. Close the tab only when all splits are settled.

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 Customer Objects: Create, Fetch, Update, Whitelist

Paystack creates a customer object every time a new email hits your checkout. You can also create customers explicitly, update their details, fetch their transaction history, and whitelist or blacklist them for risk control. This guide covers every operation with working Node.js code.

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.