McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Build a University Application Fee Portal
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build a University Application Fee Portal

Build a university application fee portal by requiring payment of the application fee via Paystack before generating an application number and unlocking the document upload and submission sections. Use the charge.success webhook to create the application record, issue the application number, and send submission instructions. Block document upload and application submission until payment is confirmed.

In this article

  1. 1. Application Form and Fee Payment
  2. 2. Document Upload and Admissions Status
  3. 3. Learn More

Application Form and Fee Payment

Tables: applicants (name, email, phone, id_number, nationality, dob), applications (applicant_id, intake_year, intake_month, programme_choices[], status, application_number, paystack_ref, paid_at, submitted_at), documents (application_id, type, file_url, verified), intake_periods (year, month, application_fee, deadline).

async function startApplication(applicantEmail, applicantData, intakePeriodId, programmeChoices) {
  var intake = await db.intakePeriods.findById(intakePeriodId);
  if (new Date() > new Date(intake.deadline)) throw new Error('Application deadline has passed');

  var applicant = await db.applicants.upsert({ email: applicantEmail, ...applicantData });
  var reference = 'APP-' + Date.now();

  var application = await db.applications.create({
    applicant_id: applicant.id,
    intake_year: intake.year,
    intake_month: intake.month,
    programme_choices: programmeChoices,
    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: applicantEmail,
      amount: intake.application_fee,
      currency: 'KES', // Kenya example
      reference,
      metadata: {
        application_id: application.id,
        applicant_id: applicant.id,
        intake: intake.year + '-' + intake.month,
        programmes: programmeChoices.join(', '),
      },
    }),
  });
  return { application, authorizationUrl: (await res.json()).data.authorization_url };
}

// Webhook: generate application number and unlock submission
if (event.event === 'charge.success') {
  var meta = event.data.metadata;
  if (!meta.application_id) return res.sendStatus(200);

  var appYear = new Date().getFullYear().toString().slice(-2);
  var seq = await db.applications.nextSequenceNumber(meta.intake);
  var applicationNumber = 'APP-' + appYear + '-' + seq.toString().padStart(5, '0');

  await db.applications.update(meta.application_id, {
    status: 'payment_confirmed',
    application_number: applicationNumber,
    paid_at: new Date(),
  });

  await sendApplicationConfirmation(meta.applicant_id, applicationNumber, meta.programmes);
}

Document Upload and Admissions Status

// Gate document upload behind payment
async function uploadDocument(applicationId, applicantId, docType, fileUrl) {
  var application = await db.applications.findById(applicationId);
  if (application.applicant_id !== applicantId) throw new Error('Not your application');
  if (application.status === 'pending_payment') throw new Error('Pay the application fee first');

  var requiredDocs = ['kcse_certificate', 'id_copy', 'passport_photo', 'recommendation_letter'];
  if (!requiredDocs.includes(docType)) throw new Error('Invalid document type');

  await db.documents.upsert({ application_id: applicationId, type: docType, file_url: fileUrl, verified: false });
}

// Applicant submits complete application
async function submitApplication(applicationId, applicantId) {
  var application = await db.applications.findById(applicationId);
  if (application.applicant_id !== applicantId) throw new Error('Not your application');
  if (application.status !== 'payment_confirmed') throw new Error('Payment required before submission');

  var requiredDocs = ['kcse_certificate', 'id_copy', 'passport_photo', 'recommendation_letter'];
  var uploadedDocs = await db.documents.findByApplication(applicationId);
  var uploadedTypes = uploadedDocs.map(function(d) { return d.type; });
  var missing = requiredDocs.filter(function(d) { return !uploadedTypes.includes(d); });
  if (missing.length > 0) throw new Error('Missing documents: ' + missing.join(', '));

  await db.applications.update(applicationId, { status: 'submitted', submitted_at: new Date() });
  await sendSubmissionAcknowledgement(applicantId, application.application_number);
}

// Admissions staff updates decision
async function updateAdmissionDecision(applicationId, decision, offeredProgramme, conditions) {
  var validDecisions = ['offered', 'waitlisted', 'rejected'];
  if (!validDecisions.includes(decision)) throw new Error('Invalid decision');

  await db.applications.update(applicationId, {
    status: decision, offered_programme: offeredProgramme, conditions, decided_at: new Date(),
  });

  var application = await db.applications.findById(applicationId);
  if (decision === 'offered') {
    await sendOfferLetter(application.applicant_id, offeredProgramme, conditions);
  } else {
    await sendDecisionEmail(application.applicant_id, decision);
  }
}

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 gym membership app for a related membership and access management pattern after enrollment fees are paid.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Generate the application number only in the charge.success webhook — never before payment.
  • ✓Gate document upload and final submission behind payment confirmation status.
  • ✓Support multiple programmes per application: one fee covers multiple course choices.
  • ✓Send email with application number and document checklist immediately after payment.
  • ✓Build an admin portal for admissions staff to review applications and update decision status.

Frequently Asked Questions

Should I refund the application fee if the student is rejected?
Application fees are almost universally non-refundable — they cover the cost of processing the application, which occurs regardless of the outcome. State this clearly on the application page before the student pays. The exception is if the application was rejected due to a system error or if the intake was cancelled by the institution before the decision was made.
How do I handle international students who cannot pay in NGN or KES?
Paystack supports USD for Nigerian merchants and GBP/USD for some account types. For international applicants, initialize the transaction with currency: "USD" and let Paystack handle the conversion. Alternatively, create a separate Stripe payment link for international applicants. Track currency paid in the application record for reconciliation.
Can I support fee waivers for students who cannot afford the application fee?
Yes. Add a waiver_code field to your intake_periods table. When a student enters a valid waiver code on the application form, skip the Paystack payment and set the application status directly to "payment_confirmed" with payment_method: "waiver". Log all waivers with the student ID and approving officer for audit purposes.

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

Keep reading

Things You Can Build with Paystack: Project Guides

A complete map of real-world projects you can build using the Paystack API. From online store checkouts and subscription platforms to savings apps and solar pay-as-you-go systems, each category links to step-by-step build guides with working code.

Read more

Build a Parking Payment App

A build guide for a parking payment app where drivers scan a QR code on entry, pay for their session via Paystack (card or M-Pesa), and the system validates payment before the barrier lifts for exit. Covers time-based pricing, session management, and overstay handling.

Read more

Build a Referral Payout System

A build guide for a referral payout system where users share referral links, earn commissions when referrals convert, and withdraw their balance via Paystack Transfers to their bank account or M-Pesa wallet.

Read more

Build a Veterinary Clinic Booking and Payment System

A build guide for a veterinary clinic booking and payment system where pet owners book appointments online, pay a deposit via Paystack to confirm the slot, and pay the balance at the clinic after the consultation. Includes pet health records, vaccination schedules, and automated reminders.

Read more

Apple Pay for Kenyan Merchants on Paystack

How to set up and accept Apple Pay through Paystack as a Kenyan merchant. Covers domain verification, which devices and browsers support Apple Pay, the customer experience, how tokenized card payments work under the hood, when Apple Pay matters in the Kenyan market, and the integration code.

Read more

Subaccounts on Paystack: Complete Guide

Everything you need to know about Paystack subaccounts: creating them via the API, validating bank details before creation, setting default split ratios, updating bank details when vendors change banks, listing and filtering subaccounts, and managing them at scale in a production marketplace.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.