Bonaventure OgetoBy Bonaventure Ogeto|

Build a Pharmacy Ordering and Delivery App

Build a pharmacy ordering app by separating OTC (over-the-counter) and prescription medicine flows: OTC medicines pay immediately via Paystack; prescription medicines require a prescription photo upload and pharmacist approval before payment is collected. Use webhooks to confirm payment and trigger order fulfillment.

OTC vs Prescription Order Flow

Tables: medicines (name, category, price, requires_prescription, stock), orders (customer_id, items[], total, prescription_url, pharmacist_id, status, paystack_ref), deliveries (order_id, address, scheduled_time, rider_id, status).

OTC flow (most orders): customer selects medicines → cart total calculated → pay via Paystack → webhook confirms → order goes to pharmacy for picking.

Prescription flow: customer uploads prescription photo → order status: "awaiting_review" → pharmacist reviews (approves/rejects/modifies) → Paystack payment initialized → customer pays → order fulfilled.

async function createOrder(customerId, cartItems, prescriptionUrl) {
  var hasPrescriptionItems = cartItems.some(item => item.requires_prescription);
  var total = cartItems.reduce((sum, item) => sum + (item.price * item.qty), 0);

  var order = await db.orders.create({
    customer_id: customerId,
    items: cartItems,
    total,
    prescription_url: prescriptionUrl || null,
    status: hasPrescriptionItems ? 'awaiting_review' : 'pending_payment',
  });

  if (!hasPrescriptionItems) {
    // OTC: go straight to payment
    return await initializePayment(order);
  }
  // Prescription: wait for pharmacist to call initiatePaymentAfterApproval(orderId)
  return { order, message: 'Prescription submitted for review. You will receive a payment link once approved.' };
}

async function approveAndCharge(orderId, pharmacistId, finalAmount) {
  await db.orders.update(orderId, { status: 'approved', pharmacist_id: pharmacistId, total: finalAmount });
  return await initializePayment(await db.orders.findById(orderId));
}

Webhook Confirmation and Delivery Scheduling

if (event.event === 'charge.success') {
  var order = await db.orders.findByPaystackRef(event.data.reference);
  await db.orders.update(order.id, { status: 'paid' });

  // Notify pharmacy to start picking
  await notifyPharmacy(order.id);

  // Schedule delivery
  await db.deliveries.create({
    order_id: order.id,
    address: order.delivery_address,
    status: 'pending',
  });
}

// Track delivery stages
async function updateDelivery(deliveryId, newStatus, riderId) {
  var delivery = await db.deliveries.findById(deliveryId);
  await db.deliveries.update(deliveryId, { status: newStatus, rider_id: riderId });

  var statusMessages = {
    'picked_up': 'Your order has been picked up by our rider',
    'en_route': 'Your medicines are on the way',
    'delivered': 'Your order has been delivered',
  };

  await sendSMS(delivery.customer_phone, statusMessages[newStatus]);
}

For controlled substances and high-value prescription medicines, require an OTP or digital signature from the customer at delivery. This creates an audit trail showing the customer received their medicine.

Learn More

See build a grocery delivery app with rider payouts for the delivery and rider payout side of this architecture.

Sign up for the McTaba newsletter

Key Takeaways

  • Separate OTC and prescription flows — prescription orders require pharmacist verification before payment.
  • For prescription orders, collect prescription photo at order time but only initialize Paystack payment after pharmacist approval.
  • Include medicine names, quantities, and dosage in Paystack metadata for dispute and audit purposes.
  • Support delivery scheduling: some customers want morning delivery, some prefer pickup.
  • Handle substitutions: if a medicine is out of stock, the pharmacist suggests an alternative and updates the order amount before payment.

Frequently Asked Questions

Do I need special licensing to operate a pharmacy delivery service in Nigeria or Kenya?
In Nigeria, pharmacy operations are regulated by the Pharmacists Council of Nigeria (PCN). An online pharmacy must be run by a licensed pharmacist and registered with PCN. In Kenya, the Pharmacy and Poisons Board (PPB) governs pharmacy operations. Consult a healthcare regulatory lawyer before launching — this is not just a payment integration question.
How do I handle out-of-stock substitutions?
When the pharmacist finds an item out of stock, they can suggest a substitution in the order management panel. This triggers a notification to the customer (SMS or push notification) with the substitution details and new price. The customer accepts or rejects. If rejected, that item is removed and the order total is updated before the Paystack payment is initialized.
Can customers save their prescriptions for refills?
Yes. Store prescription photos in cloud storage (AWS S3, Cloudinary, or Supabase Storage) linked to the customer account. When they return for a refill, they can reference a past prescription instead of uploading a new one. Add an expiry date to prescriptions (most prescriptions are valid for 6 months) and prompt re-upload when expired.

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