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. Build a Logistics Quote and Payment System
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build a Logistics Quote and Payment System

Build a logistics payment system by generating a dynamic quote based on route and cargo specs, creating a Paystack payment link for the accepted quote, triggering dispatch via webhook when payment is confirmed, and paying the assigned courier via Paystack Transfers after delivery is confirmed.

In this article

  1. 1. Quote Generation and Payment Collection
  2. 2. Shipment Tracking and Courier Payout
  3. 3. Learn More

Quote Generation and Payment Collection

Tables: zones (name, cities[]), pricing_matrix (origin_zone_id, dest_zone_id, base_rate, rate_per_kg), shipments (shipper_id, origin, destination, weight_kg, dimensions, quote_amount, status, paystack_ref, courier_id, tracking_number), couriers (name, phone, vehicle_type, paystack_recipient_code).

async function generateQuote(origin, destination, weightKg, packageType) {
  var originZone = await db.zones.findByCity(origin);
  var destZone = await db.zones.findByCity(destination);
  var pricing = await db.pricingMatrix.findByZones(originZone.id, destZone.id);

  var baseRate = pricing.base_rate;
  var weightCharge = Math.ceil(weightKg) * pricing.rate_per_kg;
  var total = baseRate + weightCharge;

  // Additional surcharges
  if (packageType === 'fragile') total = Math.floor(total * 1.15); // 15% fragile surcharge

  return { origin, destination, weight_kg: weightKg, quote_amount: total, expires_in_mins: 30 };
}

async function acceptQuoteAndPay(shipperEmail, shipperId, quoteData) {
  var trackingNumber = 'TRK-' + Date.now().toString(36).toUpperCase();
  var reference = 'LOG-' + Date.now();

  var shipment = await db.shipments.create({
    shipper_id: shipperId,
    origin: quoteData.origin,
    destination: quoteData.destination,
    weight_kg: quoteData.weight_kg,
    quote_amount: quoteData.quote_amount,
    status: 'pending_payment',
    paystack_ref: reference,
    tracking_number: trackingNumber,
  });

  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: shipperEmail,
      amount: quoteData.quote_amount,
      currency: 'NGN',
      reference,
      metadata: { shipment_id: shipment.id, shipper_id: shipperId, tracking_number: trackingNumber },
    }),
  });
  return { shipment, authorizationUrl: (await res.json()).data.authorization_url };
}

// Webhook: payment confirmed — dispatch job
if (event.event === 'charge.success') {
  var meta = event.data.metadata;
  if (!meta.shipment_id) return res.sendStatus(200);

  await db.shipments.update(meta.shipment_id, { status: 'awaiting_pickup' });
  var courier = await assignCourier(meta.shipment_id);
  await sendCourierJobNotification(courier.phone, meta.shipment_id, meta.tracking_number);
  await sendShipperConfirmation(meta.shipper_id, meta.tracking_number);
}

Shipment Tracking and Courier Payout

// Courier app updates shipment status
async function updateShipmentStatus(courierId, shipmentId, newStatus) {
  var validStatuses = ['picked_up', 'in_transit', 'delivered', 'failed_delivery'];
  if (!validStatuses.includes(newStatus)) throw new Error('Invalid status');

  var shipment = await db.shipments.findById(shipmentId);
  if (shipment.courier_id !== courierId) throw new Error('Not your shipment');

  await db.shipments.update(shipmentId, { status: newStatus, updated_at: new Date() });
  await notifyShipper(shipment.shipper_id, 'Your shipment ' + shipment.tracking_number + ' is now: ' + newStatus);

  // Pay courier on confirmed delivery
  if (newStatus === 'delivered') {
    var courier = await db.couriers.findById(courierId);
    var platformFee = Math.floor(shipment.quote_amount * 0.20); // 20% platform fee
    var courierPayout = shipment.quote_amount - platformFee;

    await fetch('https://api.paystack.co/transfer', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        source: 'balance', amount: courierPayout,
        recipient: courier.paystack_recipient_code,
        reason: 'Delivery payout: ' + shipment.tracking_number,
      }),
    });
  }
}

// Shipper can track their package
async function getTrackingInfo(trackingNumber) {
  var shipment = await db.shipments.findByTrackingNumber(trackingNumber);
  var history = await db.shipmentEvents.findByShipment(shipment.id);
  return { status: shipment.status, tracking_number: trackingNumber, events: history };
}

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Learn More

See build a grocery delivery app with rider payouts for a related delivery model with item-based orders instead of shipment quotes.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Generate quotes dynamically from weight, dimensions, and origin-destination zone matrix.
  • ✓Payment triggers dispatch — couriers only receive the job after the customer pays.
  • ✓Track shipment status updates (picked_up, in_transit, delivered) via courier app check-ins.
  • ✓Pay couriers via Paystack Transfers after delivery is confirmed, not before.
  • ✓Quote-to-dispatch conversion rate is your key business metric — optimize your pricing and UX around it.

Frequently Asked Questions

How do I handle failed deliveries (recipient not available)?
Define a re-delivery policy: one free re-delivery attempt within 24 hours. If the second attempt fails, the package returns to origin. Charge a return fee (or deduct it from the quote amount refund if the shipper requested the return). Mark the shipment as "returned" and notify the shipper via SMS. The courier receives partial pay for the failed delivery attempt.
How do I calculate quotes for same-day vs next-day delivery?
Add a delivery_type field (same_day, next_day, economy) with a multiplier per type. Same-day: 2x base rate; next-day: 1.3x; economy: 1x. Apply the multiplier after the zone-based calculation. Same-day orders also need a time cutoff (e.g., orders before 12:00 get same-day, after 12:00 are next-day).
What happens if the customer pays but no courier accepts the job?
Set a maximum assignment timeout (30 minutes). If no courier accepts within the timeout, cancel the job and issue a full refund via the Paystack Refund API. Notify the shipper with an apology and an option to retry. For high-demand routes, pre-assign couriers rather than broadcasting jobs — this prevents the "no courier available" scenario for popular routes.

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

Things You Can Build with Paystack: Project Guides

A complete map of real-world projects you can build using the Paystack API. From online store checkouts and subscription platforms to savings apps and solar pay-as-you-go systems, each category links to step-by-step build guides with working code.

Read more

Build a Parking Payment App

A build guide for a parking payment app where drivers scan a QR code on entry, pay for their session via Paystack (card or M-Pesa), and the system validates payment before the barrier lifts for exit. Covers time-based pricing, session management, and overstay handling.

Read more

Build a Referral Payout System

A build guide for a referral payout system where users share referral links, earn commissions when referrals convert, and withdraw their balance via Paystack Transfers to their bank account or M-Pesa wallet.

Read more

Build a Veterinary Clinic Booking and Payment System

A build guide for a veterinary clinic booking and payment system where pet owners book appointments online, pay a deposit via Paystack to confirm the slot, and pay the balance at the clinic after the consultation. Includes pet health records, vaccination schedules, and automated reminders.

Read more

Paystack in Kenya: What It Supports and What It Does Not

A complete map of what Paystack can and cannot do in Kenya. Covers every supported payment channel, settlement option, transfer destination, and the gaps where you need Daraja or another provider.

Read more

Building an Events Ticketing Platform with Splits

An events ticketing platform collects ticket revenue and routes it to event organizers after the event. This guide covers using Paystack splits to route organizer revenue and platform booking fees, handling multi-ticket types, and managing pre-event holds and post-event payouts.

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.