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);
}
Learn More
See build a tutoring marketplace with split payments for the same provider subaccount split pattern applied to educational services.
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.
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