Build a Farmers Market Ordering Platform
Build a farmers market platform by creating a Paystack Subaccount per farmer, using Paystack Multi-Split to route each order line item's revenue to the correct farmer's subaccount at checkout, and then coordinating pickup or delivery separately. For complex carts with multiple farmers, collect the full cart payment first and use Transfers to pay each farmer their portion after the webhook confirms.
Multi-Farmer Cart and Payment
Tables: farmers (name, phone, bank_code, account_number, paystack_subaccount_code), products (farmer_id, name, unit, price_per_unit, stock_qty), orders (customer_id, status, total, paystack_ref), order_items (order_id, product_id, farmer_id, qty, unit_price, subtotal).
async function checkout(customerEmail, customerId, cartItems) {
var total = cartItems.reduce(function(sum, item) { return sum + (item.qty * item.unit_price); }, 0);
var reference = 'FARM-' + Date.now();
// Group items by farmer for split calculation
var farmerTotals = {};
for (var item of cartItems) {
if (!farmerTotals[item.farmer_id]) farmerTotals[item.farmer_id] = 0;
farmerTotals[item.farmer_id] += item.qty * item.unit_price;
}
var order = await db.orders.create({
customer_id: customerId, status: 'pending', total, paystack_ref: reference,
});
for (var item of cartItems) {
await db.orderItems.create({ ...item, order_id: order.id });
}
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, customer_id: customerId, farmer_totals: farmerTotals },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: confirm order and pay each farmer
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.order_id) return res.sendStatus(200);
await db.orders.update(meta.order_id, { status: 'confirmed' });
// Pay each farmer their share via Transfers
var farmerTotals = meta.farmer_totals;
for (var farmerId of Object.keys(farmerTotals)) {
var farmer = await db.farmers.findById(farmerId);
var platformFee = Math.floor(farmerTotals[farmerId] * 0.10); // 10% platform fee
var payout = farmerTotals[farmerId] - 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: payout,
recipient: farmer.paystack_recipient_code,
reason: 'Order #' + meta.order_id + ' payout',
}),
});
// Notify farmer of new order
await sendOrderNotification(farmer.phone, meta.order_id, farmerTotals[farmerId] / 100);
}
}
Inventory Management and Delivery Coordination
// Reduce stock on order confirmation
async function reduceStock(orderId) {
var items = await db.orderItems.findByOrder(orderId);
for (var item of items) {
var product = await db.products.findById(item.product_id);
if (product.stock_qty < item.qty) {
// Out of stock after payment — refund this item and notify
await refundOrderItem(item);
continue;
}
await db.products.update(item.product_id, { stock_qty: product.stock_qty - item.qty });
}
}
// Delivery: aggregate order by delivery zone
async function assignDeliveryDriver(orderId) {
var order = await db.orders.findById(orderId);
var customer = await db.customers.findById(order.customer_id);
var driver = await db.drivers.findAvailableInZone(customer.delivery_zone);
await db.orders.update(orderId, { driver_id: driver.id, status: 'out_for_delivery' });
await sendSMS(driver.phone, 'New order ' + orderId + ' for delivery to ' + customer.address);
await sendSMS(customer.phone, 'Your order is on its way! Driver: ' + driver.name + ' — ' + driver.phone);
}
// Cron: weekly farmer reconciliation report
async function sendFarmerWeeklyReport(farmerId) {
var weekStart = new Date();
weekStart.setDate(weekStart.getDate() - 7);
var items = await db.orderItems.findByFarmerAndPeriod(farmerId, weekStart, new Date());
var totalEarned = items.reduce(function(sum, i) { return sum + i.subtotal; }, 0);
var farmer = await db.farmers.findById(farmerId);
await sendFarmerReport(farmer.phone, totalEarned / 100, items.length);
}
Learn More
See build a grocery delivery app with rider payouts for a related model focused on rider-based delivery and per-delivery payouts.
Key Takeaways
- ✓Create one Paystack Subaccount per farmer — revenue splits happen automatically at checkout.
- ✓For mixed-farmer carts, collect full payment then use Paystack Transfers to pay each farmer.
- ✓Track inventory per farmer so customers see real-time stock levels before ordering.
- ✓Route orders to each farmer via SMS or WhatsApp after the webhook confirms payment.
- ✓Aggregate weekly payouts for farmers who prefer bank transfer over instant subaccount settlement.
Frequently Asked Questions
- What happens if a farmer runs out of stock after an order is paid?
- Check stock availability at checkout (before payment) and reserve the quantity for the pending order. When the webhook confirms, finalize the stock deduction. If a race condition results in a paid item going out of stock anyway, refund just that line item via the Paystack Refund API with a partial amount and notify the customer. Partial refunds work by specifying the amount parameter in the refund request.
- How do I handle minimum order quantities or box subscriptions?
- Add a minimum_order_qty field to the products table and validate at cart checkout. For box subscriptions (weekly vegetable box), use Paystack Plans for recurring billing — the same Plan-based subscription model as a subscription box business. Each week's delivery is triggered when the invoice.create webhook fires with a success status.
- How do I onboard farmers who do not have smartphones or bank accounts?
- Register farmers using mobile agent-assisted onboarding (a field agent visits the farm). For unbanked farmers, set up M-Pesa as the payout method (Paystack supports M-Pesa payouts in Kenya). For feature phones, send order notifications via SMS instead of app push. Farmers can check their earnings by texting a short code or having a family member access the farmer portal.
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