One GET.
One answer.
Ask whether an NSID belongs to a currently enrolled student. Get status, institution, programme and level back inside 250ms — or a 409 carrying the link that asks the student for permission.
curl https://api.bona.ng/v1/verify/NS-TEST-0001-AAAM \ -H "Authorization: Bearer bona_test_7f2a…c91d"
{
"verified": true,
"status": "active",
"statusLabel": "Currently enrolled",
"institution": "University of Lagos",
"programme": "B.Eng Electrical & Electronic Engineering",
"level": 400,
"expectedCompletion": "2027-07-31",
"requestId": "req_9f2c41a08b3d"
}Three tiers. Ask for the least.
The tier is not a plan you sit on — it is chosen per call, by what you ask for, and you are billed for what comes back. Attestation is the cheapest thing on the rate card on purpose: the fewer fields you request, the less you pay and the less there is to lose.
Is this NSID a currently enrolled student, and where? Yes or no, plus the institution. Enough to open a student account or price a data bundle.
institution
What a bank, a telco or NELFUND needs to assess eligibility — including the expected completion date, so you know when to ask again instead of finding out years late.
programme · level
expectedCompletion
Adds identity fields for licensed institutions with a lawful basis to hold them. The NIN is matched, never returned — you get ninMatch: true, not a number.
name · dateOfBirth
ninMatch
A consent_required response means the record exists and your key is fine — the student simply has not authorised you for those fields. Send them to the consentUrl in the body and call again when they have. Treating it as a failure is the one integration bug that produces no alert and no complaint until a student asks at a counter why they were turned away.
HTTP/1.1 409 Conflict
{
"error": {
"code": "CONSENT_REQUIRED",
"message": "This student has not granted you these fields yet.",
"consentUrl": "https://app.bona.ng/requests/cr_8f21b4",
"requestId": "req_9f2c41a08b3e"
}
}Every state you will meet in production exists in sandbox, including the ones that ruin naive integrations. These NSIDs carry real check characters, so they pass the same validation as production values.
Sandbox keys on signup, a copy-pasteable call with a real NSID already in it, and a request inspector for when it goes wrong at 2am.