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