Build a Vehicle Inspection Booking and Payment App
Build a vehicle inspection app by managing inspection center slots, requiring fee payment via Paystack before the appointment is confirmed, and issuing a digital inspection certificate (PDF or QR-verifiable record) only after the inspector marks the inspection as completed. Webhook confirms payment; inspector app updates the inspection result and triggers certificate generation.
Slot Booking and Inspection Fee Payment
Tables: centers (name, location, capacity_per_slot, paystack_subaccount_code), inspection_slots (center_id, date, time, booked_count, capacity), inspections (owner_id, vehicle_id, center_id, slot_id, type, fee, status, paystack_ref, certificate_url), vehicles (owner_id, plate_number, make, model, year, color).
var INSPECTION_FEES = {
annual_roadworthiness: 350000, // NGN 3,500
pre_purchase: 500000, // NGN 5,000
insurance_valuation: 250000, // NGN 2,500
};
async function bookInspection(ownerEmail, ownerId, vehicleId, centerId, slotId, type) {
var fee = INSPECTION_FEES[type];
if (!fee) throw new Error('Invalid inspection type');
var slot = await db.inspectionSlots.findById(slotId);
if (slot.booked_count >= slot.capacity) throw new Error('Slot is fully booked');
var reference = 'INSP-' + Date.now();
var center = await db.centers.findById(centerId);
var inspection = await db.inspections.create({
owner_id: ownerId, vehicle_id: vehicleId, center_id: centerId, slot_id: slotId,
type, fee, status: 'pending_payment', 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: ownerEmail, amount: fee, currency: 'NGN', reference,
subaccount: center.paystack_subaccount_code, // center gets their share
metadata: { inspection_id: inspection.id, vehicle_id: vehicleId, slot_id: slotId },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: confirm inspection slot
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.inspection_id) return res.sendStatus(200);
await db.inspections.update(meta.inspection_id, { status: 'confirmed' });
await db.inspectionSlots.increment(meta.slot_id, 'booked_count', 1);
var inspection = await db.inspections.findById(meta.inspection_id);
var vehicle = await db.vehicles.findById(meta.vehicle_id);
var slot = await db.inspectionSlots.findById(meta.slot_id);
await sendBookingConfirmation(inspection.owner_id, vehicle.plate_number, slot.date, slot.time);
}
Inspection Results and Certificate Generation
// Inspector submits results via inspector app
async function submitInspectionResult(inspectorId, inspectionId, result, findings, mileage) {
var inspection = await db.inspections.findById(inspectionId);
var vehicle = await db.vehicles.findById(inspection.vehicle_id);
var owner = await db.owners.findById(inspection.owner_id);
await db.inspections.update(inspectionId, {
status: result === 'pass' ? 'passed' : 'failed',
inspector_id: inspectorId,
findings: findings,
inspected_at: new Date(),
mileage_at_inspection: mileage,
});
if (result === 'pass') {
// Generate certificate with unique verification code
var certCode = 'CERT-' + vehicle.plate_number + '-' + new Date().getFullYear() + '-' + Math.random().toString(36).slice(2, 8).toUpperCase();
var validUntil = new Date();
validUntil.setFullYear(validUntil.getFullYear() + 1); // 1-year certificate
var certificateUrl = await generateCertificatePdf({
inspectionId, vehicle, owner, certCode, validUntil, mileage,
});
await db.inspections.update(inspectionId, {
certificate_url: certificateUrl,
certificate_code: certCode,
certificate_valid_until: validUntil,
});
await sendCertificateEmail(owner.email, owner.name, vehicle.plate_number, certificateUrl);
} else {
// Failed inspection — send remediation report
await sendFailureReport(owner.email, vehicle.plate_number, findings);
}
}
// Public certificate verification (for police/insurers)
async function verifyCertificate(certCode) {
var inspection = await db.inspections.findByCertCode(certCode);
if (!inspection) return { valid: false };
return {
valid: new Date() < new Date(inspection.certificate_valid_until),
plate_number: inspection.vehicle.plate_number,
result: inspection.status,
valid_until: inspection.certificate_valid_until,
};
}
Learn More
See build a barbershop appointment and payment app for a simpler slot booking with deposit pattern without the certificate generation component.
Key Takeaways
- ✓Payment confirms the slot — no pay, no confirmation, no queue entry.
- ✓Issue digital inspection certificates only after the inspector marks results as passed or provide a conditional fail report.
- ✓Support multiple inspection types: annual roadworthiness, pre-purchase inspection, insurance valuation.
- ✓Use QR codes on certificates for instant roadside verification by traffic police or insurers.
- ✓For multi-center franchises, use Paystack Subaccounts to route each center's fees automatically.
Frequently Asked Questions
- How do I handle vehicle owners who want to reschedule?
- Allow rescheduling up to 24 hours before the appointment at no cost — just update the slot_id on the inspection record and adjust the booked_count on both the old and new slots. Within 24 hours, charge a rescheduling fee (NGN 500-1,000) via a new Paystack payment link. No-shows (no reschedule, no cancellation) forfeit the inspection fee.
- Can I integrate with NTSA or regulatory systems?
- NTSA (Kenya) has an open API for vehicle data and sticker issuance for approved inspection centers. Your app can query NTSA for vehicle registration validity before proceeding with the booking. After a passed inspection, submit the result to NTSA's API to update their records. Regulatory integration requires a formal partnership with NTSA or the relevant motor vehicle authority.
- What happens to vehicles that fail the inspection?
- Issue a detailed failure report with specific items that failed (e.g., worn brake pads, cracked windshield, expired fire extinguisher). Give the owner a 30-day window to rectify issues and return for a re-inspection at a discounted rate (50% of the original fee). Track failed-and-returned vehicles separately — a high re-inspection conversion rate means your failure reports are actionable and clear.
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