Bonaventure OgetoBy Bonaventure Ogeto|

Build a Grocery Delivery App with Rider Payouts

Build a grocery delivery app by connecting a product catalog with a Paystack checkout. When the customer pays, the webhook triggers order preparation and rider assignment. After the rider confirms delivery, use the Paystack Transfers API to pay them. Riders are registered as Transfer Recipients (bank account or M-Pesa) for automatic payout.

Cart Checkout and Payment

Tables: products (name, price, category, stock), orders (customer_id, items[], subtotal, delivery_fee, total, delivery_address, status, paystack_ref), riders (name, phone, paystack_recipient_code), deliveries (order_id, rider_id, assigned_at, delivered_at, payout_ref).

async function checkout(customerId, customerEmail, cartItems, deliveryAddress) {
  var subtotal = cartItems.reduce((sum, item) => sum + (item.price * item.qty), 0);
  var deliveryFee = 150000; // NGN 1,500 in kobo
  var total = subtotal + deliveryFee;
  var reference = 'GROC-' + Date.now();

  var order = await db.orders.create({
    customer_id: customerId, items: cartItems, subtotal, delivery_fee: deliveryFee,
    total, delivery_address: deliveryAddress, status: 'pending', paystack_ref: reference,
  });

  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: customerEmail, amount: total, currency: 'NGN', reference,
      metadata: { order_id: order.id, delivery_fee: deliveryFee },
    }),
  });
  return (await res.json()).data.authorization_url;
}

Rider Assignment and Payout

// Webhook: confirm payment and start fulfillment
if (event.event === 'charge.success') {
  var orderId = event.data.metadata.order_id;
  await db.orders.update(orderId, { status: 'confirmed' });
  var rider = await assignAvailableRider(orderId); // your assignment logic
  await db.deliveries.create({ order_id: orderId, rider_id: rider.id, assigned_at: new Date() });
  await notifyRider(rider.id, orderId);
  await notifyCustomer(orderId);
}

// Rider confirms delivery — trigger payout
async function confirmDelivery(deliveryId) {
  var delivery = await db.deliveries.findById(deliveryId);
  var order = await db.orders.findById(delivery.order_id);

  await db.deliveries.update(deliveryId, { delivered_at: new Date(), status: 'delivered' });
  await db.orders.update(order.id, { status: 'delivered' });

  // Pay rider via Transfer
  var rider = await db.riders.findById(delivery.rider_id);
  var payoutAmount = order.delivery_fee; // rider gets the delivery fee

  var res = 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: payoutAmount,
      recipient: rider.paystack_recipient_code,
      reason: 'Delivery payout #' + deliveryId,
      reference: 'RIDER-' + deliveryId + '-' + Date.now(),
    }),
  });
  var transfer = await res.json();
  await db.deliveries.update(deliveryId, { payout_ref: transfer.data.reference });
}

To register a rider as a Transfer Recipient: POST to /transferrecipient with type "mobile_money" (for M-Pesa) or "nuban" (for Nigerian bank), the rider's phone/account number, and bank code. Store the returned recipient_code in the riders table.

Learn More

See build a restaurant ordering and payment app for the same rider payout pattern in a restaurant delivery context.

Sign up for the McTaba newsletter

Key Takeaways

  • Pay riders via Paystack Transfers after delivery confirmation, not via cash handover.
  • Riders must be registered as Paystack Transfer Recipients with their bank or M-Pesa details.
  • Handle the charge.success webhook before doing anything — no fulfillment without payment confirmation.
  • Build a rider app or dashboard for status updates: assigned, picked_up, delivered.
  • Batch daily rider payouts using Paystack Bulk Transfers to reduce API calls and transfer fees.

Frequently Asked Questions

What if a transfer to the rider fails?
Listen for the transfer.failed webhook event. Log the failure and alert your operations team. Retry the transfer with corrected details (wrong account number is the most common cause). Have a fallback process for paying riders manually if automated transfer fails repeatedly.
Should I pay riders per delivery or accumulate and pay daily?
Accumulate and pay daily using Paystack Bulk Transfers — it reduces API calls, transfer fees, and operational complexity. Send one transfer per rider at end of day covering all their completed deliveries. Riders generally prefer daily payouts; this schedule is also easier to reconcile.
How do I handle a rider who picks up an order but never delivers?
Add a delivery timeout: if an order stays in "picked_up" status for more than 2 hours, flag it for operations review. Do not pay the rider until delivery is confirmed. If the order is confirmed stolen or abandoned, the rider does not get the payout, and you issue a refund to the customer using the Paystack Refund API.

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