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. Connecting a POS Terminal to Your Inventory System
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Connecting a POS Terminal to Your Inventory System

When you receive a charge.success webhook from a terminal transaction, extract the order items from your database (linked via the payment reference), and deduct each item quantity from your inventory table. Use database transactions to ensure the payment status update and inventory deduction happen atomically. Build low-stock alerts that trigger when items fall below a threshold.

In this article

  1. 1. Inventory Database Schema
  2. 2. Deducting Stock on Payment Confirmation
  3. 3. Low-Stock Alerts
  4. 4. Restoring Inventory on Refunds
  5. 5. Barcode Scanner Integration
  6. 6. Inventory Reconciliation

Inventory Database Schema

-- inventory tables
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  sku TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  price BIGINT NOT NULL, -- in kobo
  stock_quantity INTEGER NOT NULL DEFAULT 0,
  low_stock_threshold INTEGER NOT NULL DEFAULT 10,
  category TEXT,
  barcode TEXT UNIQUE,
  active BOOLEAN NOT NULL DEFAULT true,
  created_at TIMESTAMP NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE inventory_movements (
  id SERIAL PRIMARY KEY,
  product_id INTEGER NOT NULL REFERENCES products(id),
  quantity_change INTEGER NOT NULL, -- negative for sales, positive for restocks
  reason TEXT NOT NULL, -- 'sale', 'restock', 'refund', 'adjustment'
  reference TEXT, -- order ID or restock batch
  created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

inventory_movements is your audit trail. Every stock change, whether from a sale, restock, refund, or manual adjustment, gets a row. This lets you trace exactly how stock levels arrived at their current value.

stock_quantity on the products table is the current count. It is updated by triggers or application code whenever a movement is recorded. Having it directly on the product row makes lookups fast.

Deducting Stock on Payment Confirmation

// inventory-deduction.js
async function deductInventoryForOrder(orderId) {
  var client = await db.pool.connect();

  try {
    await client.query('BEGIN');

    // Get order items
    var order = await client.query(
      'SELECT items FROM orders WHERE id = $1 AND status = $2',
      [orderId, 'paid']
    );

    if (order.rows.length === 0) {
      await client.query('ROLLBACK');
      return;
    }

    var items = JSON.parse(order.rows[0].items);

    for (var i = 0; i < items.length; i++) {
      var item = items[i];

      // Deduct stock
      await client.query(
        'UPDATE products SET stock_quantity = stock_quantity - $1, updated_at = NOW() WHERE id = $2',
        [item.quantity, item.product_id]
      );

      // Record movement
      await client.query(
        'INSERT INTO inventory_movements (product_id, quantity_change, reason, reference) VALUES ($1, $2, $3, $4)',
        [item.product_id, -item.quantity, 'sale', 'ORDER-' + orderId]
      );
    }

    // Mark inventory deduction as complete
    await client.query(
      'UPDATE orders SET inventory_deducted = true WHERE id = $1',
      [orderId]
    );

    await client.query('COMMIT');

    // Check for low stock after deduction
    await checkLowStock(items.map(function(i) { return i.product_id; }));

  } catch (error) {
    await client.query('ROLLBACK');
    console.error('Inventory deduction failed for order ' + orderId + ': ' + error.message);
    throw error;
  } finally {
    client.release();
  }
}

Database transaction is critical. If you deduct stock for 3 items and the 3rd fails, you need to roll back all deductions. Partial deductions create inaccurate inventory.

Idempotency. The inventory_deducted flag prevents double deduction if the webhook fires twice or if you retry the deduction after a failure.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Low-Stock Alerts

// low-stock.js
async function checkLowStock(productIds) {
  var lowStock = await db.query(
    'SELECT id, name, stock_quantity, low_stock_threshold FROM products ' +
    'WHERE id = ANY($1) AND stock_quantity <= low_stock_threshold AND active = true',
    [productIds]
  );

  if (lowStock.rows.length > 0) {
    var message = 'Low stock alert:\n';
    lowStock.rows.forEach(function(product) {
      message += product.name + ': ' + product.stock_quantity + ' remaining (threshold: ' + product.low_stock_threshold + ')\n';
    });

    await notifyManager(message);
  }
}

// Check all products daily for anything that dropped below threshold
async function dailyStockCheck() {
  var lowStock = await db.query(
    'SELECT name, stock_quantity, low_stock_threshold FROM products ' +
    'WHERE stock_quantity <= low_stock_threshold AND active = true ORDER BY stock_quantity ASC'
  );

  if (lowStock.rows.length > 0) {
    await notifyManager('Daily stock report: ' + lowStock.rows.length + ' items below threshold.');
  }
}

Restoring Inventory on Refunds

// refund-restore.js
async function restoreInventoryForRefund(orderId, refundedItems) {
  var client = await db.pool.connect();

  try {
    await client.query('BEGIN');

    for (var i = 0; i < refundedItems.length; i++) {
      var item = refundedItems[i];

      await client.query(
        'UPDATE products SET stock_quantity = stock_quantity + $1, updated_at = NOW() WHERE id = $2',
        [item.quantity, item.product_id]
      );

      await client.query(
        'INSERT INTO inventory_movements (product_id, quantity_change, reason, reference) VALUES ($1, $2, $3, $4)',
        [item.product_id, item.quantity, 'refund', 'REFUND-ORDER-' + orderId]
      );
    }

    await client.query('COMMIT');
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

Not all refunds restore inventory. If the customer returns damaged goods, you might refund the money but not add the items back to sellable stock. Your refund flow should let the operator choose whether to restock.

Barcode Scanner Integration

Barcode scanners speed up both order building and inventory management. When a product is scanned, look it up by barcode and add it to the order.

// barcode-lookup.js
router.get('/api/products/barcode/:code', async function(req, res) {
  var barcode = req.params.code;

  var product = await db.query(
    'SELECT id, name, price, stock_quantity FROM products WHERE barcode = $1 AND active = true',
    [barcode]
  );

  if (product.rows.length === 0) {
    return res.status(404).json({ error: 'Product not found for barcode: ' + barcode });
  }

  var item = product.rows[0];

  if (item.stock_quantity <= 0) {
    return res.json({
      product: item,
      warning: 'This product is out of stock.',
    });
  }

  return res.json({ product: item });
});

Out-of-stock handling. When a scanned product has zero stock, show a warning to the cashier. Let them decide whether to proceed (perhaps the stock count is wrong) or skip the item.

For the full barcode scanning implementation, see the retail checkout guide.

Inventory Reconciliation

// reconciliation.js
async function reconcileInventory(productId, physicalCount) {
  var product = await db.query(
    'SELECT stock_quantity FROM products WHERE id = $1',
    [productId]
  );

  var systemCount = product.rows[0].stock_quantity;
  var difference = physicalCount - systemCount;

  if (difference !== 0) {
    // Record the adjustment
    await db.query(
      'UPDATE products SET stock_quantity = $1, updated_at = NOW() WHERE id = $2',
      [physicalCount, productId]
    );

    await db.query(
      'INSERT INTO inventory_movements (product_id, quantity_change, reason, reference) VALUES ($1, $2, $3, $4)',
      [productId, difference, 'adjustment', 'RECON-' + new Date().toISOString().slice(0, 10)]
    );
  }

  return {
    product_id: productId,
    system_count: systemCount,
    physical_count: physicalCount,
    difference: difference,
    adjusted: difference !== 0,
  };
}

Reconcile weekly. Physical counts should happen regularly. The difference between system count and physical count reveals theft, damage, missed scans, or software bugs. Track the difference percentage over time. If it grows, investigate the cause.

Key Takeaways

  • ✓Deduct inventory on webhook confirmation, not when the order is created. The customer has not paid until the webhook confirms the charge.
  • ✓Use database transactions to update payment status and inventory atomically. If either fails, roll back both.
  • ✓Track inventory by SKU or product ID. Link order line items to inventory records for automatic deduction.
  • ✓Build low-stock alerts. When a product drops below a threshold, notify the store manager via email or push notification.
  • ✓Reconcile inventory counts against terminal transaction history weekly to catch drift between actual stock and system records.
  • ✓Handle refunds by restoring inventory. When you process a Paystack refund, add the refunded items back to stock.

Frequently Asked Questions

Should I deduct inventory when the order is created or when the payment is confirmed?
Deduct on payment confirmation (webhook). If you deduct when the order is created and the customer does not pay, your inventory shows less stock than you actually have. Only commit the stock reduction when money has been received.
How do I handle items with variants (size, colour)?
Create a separate product record (and SKU) for each variant. A "T-Shirt Size M Blue" and "T-Shirt Size L Red" are two different inventory items. Each has its own stock count and barcode.
What if the inventory deduction fails after the payment succeeds?
Log the failure and alert the operations team. The payment has already been processed, so you cannot undo it. Retry the inventory deduction. If retries fail, manually adjust the inventory. This scenario is rare with proper database transactions.
Can I reserve stock when the order is created to prevent overselling?
Yes. When the cashier adds items to the order, mark those quantities as "reserved" in a separate column. If the payment completes, convert the reservation to a deduction. If the payment fails or times out, release the reservation. This prevents two cashiers from selling the last unit simultaneously.

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 Product API for Simple Storefronts

The Paystack Product API lets you create products with names, prices, and descriptions, then generate payment links for each one. For simple storefronts selling a handful of products, this is faster than building a full e-commerce backend. This guide covers creating products, linking them to payment pages, and knowing when to outgrow this approach.

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.