Bonaventure OgetoBy Bonaventure Ogeto|

Build a Home Services Booking Marketplace

Build a home services marketplace by registering each provider as a Paystack Subaccount, routing service bookings through the subaccount split at payment time, and confirming the booking only after the charge.success webhook fires. The provider receives their share automatically; your platform keeps the service fee. Use Paystack Transfers for bonus payouts or adjustments.

Provider Onboarding and Service Booking

Tables: providers (name, phone, email, category, skills[], bio, avg_rating, paystack_subaccount_code, is_verified, is_available), services (provider_id, name, category, base_price, duration_hrs, description), bookings (customer_id, provider_id, service_id, address, scheduled_at, status, total_amount, paystack_ref).

var PLATFORM_FEE_PERCENT = 20; // platform keeps 20%, provider gets 80%

// Onboard a new provider with Paystack Subaccount
async function onboardProvider(providerData) {
  var provider = await db.providers.create({ ...providerData, is_verified: false });

  var res = await fetch('https://api.paystack.co/subaccount', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      business_name: providerData.name,
      settlement_bank: providerData.bank_code,
      account_number: providerData.account_number,
      percentage_charge: PLATFORM_FEE_PERCENT,
    }),
  });
  var subaccountCode = (await res.json()).data.subaccount_code;
  await db.providers.update(provider.id, { paystack_subaccount_code: subaccountCode });
  return provider;
}

// Customer books a service
async function bookService(customerEmail, customerId, serviceId, address, scheduledAt) {
  var service = await db.services.findById(serviceId);
  var provider = await db.providers.findById(service.provider_id);

  if (!provider.is_available) throw new Error('Provider is not currently available');

  // Travel surcharge based on distance from provider base
  var travelFee = await estimateTravelFee(provider, address);
  var total = service.base_price + travelFee;
  var reference = 'HOME-' + serviceId + '-' + Date.now();

  var booking = await db.bookings.create({
    customer_id: customerId, provider_id: provider.id, service_id: serviceId,
    address, scheduled_at: scheduledAt, status: 'pending_payment',
    total_amount: total, 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,
      subaccount: provider.paystack_subaccount_code,
      bearer: 'account', // platform bears transaction fee
      metadata: { booking_id: booking.id, customer_id: customerId, provider_id: provider.id, service: service.name },
    }),
  });
  return (await res.json()).data.authorization_url;
}

// Webhook: confirm booking
if (event.event === 'charge.success') {
  var meta = event.data.metadata;
  if (!meta.booking_id) return res.sendStatus(200);

  await db.bookings.update(meta.booking_id, { status: 'confirmed' });
  var booking = await db.bookings.findById(meta.booking_id);
  var provider = await db.providers.findById(meta.provider_id);
  await sendBookingConfirmation(meta.customer_id, provider.name, booking.scheduled_at, booking.address);
  await sendJobNotification(provider.phone, meta.booking_id, booking.scheduled_at, booking.address);
}

Job Completion and Provider Ratings

// Provider marks job complete
async function completeJob(providerId, bookingId) {
  var booking = await db.bookings.findById(bookingId);
  if (booking.provider_id !== providerId) throw new Error('Not your booking');
  if (booking.status !== 'confirmed') throw new Error('Booking not confirmed');

  await db.bookings.update(bookingId, { status: 'completed', completed_at: new Date() });

  // Send rating request to customer
  var customer = await db.customers.findById(booking.customer_id);
  await sendRatingRequest(customer.email, customer.name, booking.id, providerId);
}

// Customer rates the provider
async function rateProvider(customerId, bookingId, rating, review) {
  if (rating < 1 || rating > 5) throw new Error('Rating must be between 1 and 5');
  var booking = await db.bookings.findById(bookingId);
  if (booking.customer_id !== customerId) throw new Error('Not your booking');

  await db.reviews.create({ booking_id: bookingId, provider_id: booking.provider_id, customer_id: customerId, rating, review, created_at: new Date() });

  // Update provider average rating
  var allRatings = await db.reviews.findByProvider(booking.provider_id);
  var avgRating = allRatings.reduce(function(sum, r) { return sum + r.rating; }, 0) / allRatings.length;
  await db.providers.update(booking.provider_id, { avg_rating: Math.round(avgRating * 10) / 10 });
}

// Customer disputes the job — escalate to admin
async function disputeJob(customerId, bookingId, reason) {
  var booking = await db.bookings.findById(bookingId);
  if (booking.customer_id !== customerId) throw new Error('Not your booking');

  await db.bookings.update(bookingId, { status: 'disputed' });
  await db.disputes.create({ booking_id: bookingId, raised_by: customerId, reason, status: 'open' });
  await notifyAdmin('New dispute on booking #' + bookingId + ': ' + reason);
}

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 tutoring marketplace with split payments for the same provider subaccount split pattern applied to educational services.

Sign up for the McTaba newsletter

Key Takeaways

  • Create a Paystack Subaccount per service provider — revenue splitting is automated at payment time.
  • Set platform commission as the bearer's share (15-20%); providers receive the remainder automatically.
  • Confirm bookings only in the charge.success webhook — never before payment clears.
  • Rate providers after job completion — use average rating to rank providers in search results.
  • For multi-person jobs (e.g., cleaning crew of 3), assign a lead provider and split internally.

Frequently Asked Questions

How do I verify that providers are qualified (e.g., licensed electricians)?
Require document upload at onboarding: professional certification, national ID, and a selfie with the document. Run a manual review before setting is_verified: true. For high-risk services (electrical, gas), partner with professional bodies (e.g., ERB in Kenya, COREN in Nigeria) to verify membership numbers via their APIs or manual lookup. Display a "Verified Provider" badge prominently on the profile.
What if the customer is not home when the provider arrives?
Give the provider a 15-minute wait window. If the customer does not answer after 15 minutes, the provider marks the job as "customer_absent." No refund is issued — the provider's time was committed. The provider receives 50% of the service fee as a show-up payment via Paystack Transfers. The customer gets the remaining 50% as a platform credit for a future booking, not cash refund.
How do I handle services that take longer than the estimated duration?
Include an overtime surcharge in your terms: NGN X per additional 30 minutes beyond the booked duration. After the job, the provider submits the actual duration in the completion form. If overtime applies, generate a supplementary Paystack payment link for the extra amount and send it to the customer. The customer pays the overtime charge before rating the provider.

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