Developers
Payment numbers
The number
- 10 digits, numbers only, never starting with 0. Works on a phone keypad. Shown grouped as
2047 1185 30; typed with or without spaces or dashes. - The last digit is a Luhn check digit, so a mistyped number is refused before anything is looked up.
- Made by Wellth, random and unique across Wellth. It never holds a name, a date of birth or an admission number.
- One per person per organisation, for as long as they are with you. Never reused, even after you retire it.
To check a number in your own screens (say, before printing a fee slip):
// "2047 1185 30", "2047118530" or "2047-1185-30" → true; one wrong digit → false.
function isWellthPayNumber(input) {
const s = String(input).replace(/[\s-]/g, "");
if (!/^[1-9]\d{9}$/.test(s)) return false;
let sum = 0;
for (let i = 0; i < 10; i++) {
let d = Number(s[9 - i]);
if (i % 2 === 1) {
d *= 2;
if (d > 9) d -= 9;
}
sum += d;
}
return sum % 10 === 0;
}Who keeps the balance
PUT /learners/:externalRef, or PUT /payers/:externalRef with balanceMinor.PUT /payers/:externalRef without balanceMinor, then send bills.A payer never changes kind: sending balanceMinor for a BILLS payer, or leaving it out for a REPORTED one, is a 409.
Create or update a learner
PUT/api/org/v1/learners/:externalRef
Also at /api/school/v1/learners/:externalRef. Creates the learner with a new payment number (201), or updates it (200). Idempotent by itself: no Idempotency-Key needed.
:externalRef is your own opaque id for the learner, URL-encoded: 1 to 120 printable characters, no spaces. Never a name or an admission number.
N. K., S.2. 1 to 40 characters.UGX in Uganda, KES in Kenya. Anything else is a 400."350000". "0" when nothing is owed; null when unknown (the payer chooses the amount). At most 2,000,000,000.Term 3, 2026. A school may send any text; any other organisation sends null or one of its descriptions.wellthRefs (WP-8F3K2Q7A) of this learner's Wellth payments that your balanceMinor already counts. At most 50, newest first. See below.curl -X PUT https://wellth.example/api/org/v1/learners/oca_stu_ckx123 \
-H "Authorization: Bearer $WELLTH_KEY" \
-H "Content-Type: application/json" \
-d '{
"learnerLabel": "N. K., S.2",
"currency": "UGX",
"balanceMinor": "350000",
"termLabel": "Term 3, 2026",
"includesWellthRefs": []
}'{
"externalRef": "oca_stu_ckx123",
"payNumber": "2047118530",
"payNumberDisplay": "2047 1185 30",
"payUrl": "https://wellth.example/pay/n/2047118530",
"learnerLabel": "N. K., S.2",
"balanceMinor": "350000",
"reportedBalanceMinor": "350000",
"currency": "UGX",
"termLabel": "Term 3, 2026",
"balanceUpdatedAt": "2026-10-27T15:20:00.000Z",
"status": "ACTIVE"
}/pay/n/<payNumber>. Put its QR on fee slips.ACTIVE or RETIRED.Keeping the balance right: includesWellthRefs
When a payment lands, Wellth lowers the balance at once, so the next payer sees the right figure, and tells your system by the learner.payment_succeeded webhook. But your system may send a balance worked out before it booked that webhook. Without help, that would bring back money already paid, and a second parent could pay it again.
So with every PUT, list the wellthRefs your figure already counts. Wellth keeps balance = max(0, balanceMinor − every settled Wellth payment on this learner you have never listed). Once listed, a payment counts for good, so listing your newest 50 each time is enough. Unknown references are ignored.
An example
- You send
"350000". Wellth shows 350,000. - A parent pays 150,000 on Wellth:
WP-8F3K2Q7A. Wellth shows 200,000 and sends the webhook. - Before your system books it, a fee change makes it send
"350000"again, listing nothing. Wellth still shows 200,000:WP-8F3K2Q7Aisn't counted in your figure yet. - Your system books the webhook and sends
"200000"with["WP-8F3K2Q7A"]. Wellth shows 200,000, andreportedBalanceMinoris 200,000 too.
Read or retire a learner
GET/api/org/v1/learners/:externalRef
The same shape as the PUT answer. 404 if Wellth doesn't know the learner.
DELETE/api/org/v1/learners/:externalRef
The learner left: status RETIRED. The number is never given to anyone else. A payment already on its way still lands and is reported. Retiring twice is fine. A later PUT for the same externalRef makes the learner ACTIVE again with the same number (200).
Payers whose balance comes from bills
PUT/api/org/v1/payers/:externalRef
Without balanceMinor: a BILLS payer, made with its number (201) or its label and site updated (200). With balanceMinor: exactly the learner PUT above (send learnerLabel then).
S. A.. No phone, file or ID numbers (5 or more digits in a row is refused). learnerLabel is accepted too.UGX or KES: your organisation's.curl -X PUT https://wellth.example/api/org/v1/payers/pt_4471 \
-H "Authorization: Bearer $WELLTH_KEY" \
-H "Content-Type: application/json" \
-d '{ "payerLabel": "S. A.", "currency": "UGX", "siteCode": "kyanja" }'{
"externalRef": "pt_4471",
"payNumber": "4027186354",
"payNumberDisplay": "4027 1863 54",
"payUrl": "https://wellth.example/pay/n/4027186354",
"learnerLabel": "S. A.",
"balanceMinor": "0",
"reportedBalanceMinor": null,
"currency": "UGX",
"termLabel": null,
"balanceUpdatedAt": "2026-11-01T09:02:11.000Z",
"status": "ACTIVE",
"payerRef": "pt_4471",
"billing": "BILLS",
"siteCode": "kyanja"
}Every payer answer adds payerRef (your externalRef again), billing and siteCode to the learner shape. A BILLS payer's balanceMinor is the sum of what is open on its bills, and its termLabel is the one bill's description, or “2 bills”.
GET/api/org/v1/payers/:externalRef
Any payer, any kind. 404 if unknown.
DELETE/api/org/v1/payers/:externalRef
Retires a learner or a BILLS payer, as for learners. A rider is ended in the portal instead (409).
Riders on a remittance plan (read-only)
A boda boda or taxi fleet keeps each rider's plan in the Wellth portal. GET /api/org/v1/payers/:externalRef for a rider adds remittance, and balanceMinor is what the rider's number takes today. Amounts are strings; a field not set is null.
{
"remittance": {
"dailyAmountMinor": "15000",
"arrearsMinor": "30000",
"aheadMinor": "0",
"todayMinor": "15000",
"ownershipTargetMinor": "6500000",
"paidTowardTargetMinor": "2310000",
"status": "ACTIVE"
}
}A rider can't be changed or retired through the API: PUT and DELETE answer 409.
How people pay a number
- The direct link
/pay/n/<payNumber>(thepayUrl; a QR on the slip), your poster page/s/<handle>, or School fees in the Wellth app. Payers sign in first. - They see your name with its tick, the label, what is owed and as of when. They pay all of it or part: any whole amount up to what is owed, less what is already on its way.
- Balance
null: any whole amount. Balance"0": refused with “Nothing is owed right now”. - Mobile money, bank transfer, from abroad, or a Wellth balance. The payer's fee is their own line, on top: you receive the full amount, and
amountMinorin the webhook never includes it. - A payment on its way holds the balance (15 minutes by phone, 30 from abroad), so two payers can't both pay the same money.