API version: training-1
Protocol: HTTP inside the IremboHub cluster (mock service)
Data format: JSON
Used by: IremboHub Integration Manager Certification (reference objects VICTOR_KEZA_*)
The KEZA Training API is a mocked copy of KEZA. It is served by the IremboHub Mocks feature (service KEZA, category victor-course). The IremboHub Integration Manager Certification uses it so that learners can build, run and troubleshoot real integrations without a live partner.
Each operation is one mock. Each IremboHub endpoint named VICTOR_KEZA_* calls one of these operations. The mocks deliberately use a different authentication pattern per operation, so that one partner covers every auth type the engine supports.
Not the production contract. The production KEZA contract is the KEZA API Reference: OAuth 2.0 only,
/v1/eligibility,/v1/enrollmentswith idempotency keys. Do not build a production integration from this page. The paths, payloads and credentials below exist only in the training mocks.
MOCK_BASE_URL = http://mock-service-engine.apps:8000
All paths below start with /victor/keza. For example:
MOCK_BASE_URL + /victor/keza/v1/learners/1199880012345678
Requirements:
/integration/v1/fetch/sync or /integration/v1/push/sync) with the VICTOR_KEZA_* endpoint code, not directly from a laptop.MOCK mode sends traffic to its mock URL. The auth block of the mapping still runs in MOCK mode, so the login and token mocks must exist too.| Persona | National ID | Behaviour |
|---|---|---|
| Aline Mukamana | 1199880012345678 |
Known learner, eligible for DATA_ANALYTICS, awarded a scholarship, holds certificate KZ-CERT-2026-0001; a new enrollment succeeds. |
| Eric Habimana | 1199770098765432 |
Known learner, NOT_ELIGIBLE (AGE_LIMIT); already enrolled (ENR-2026-0042). |
| Unknown learner | 1190000000000009 |
Well-formed ID with no record: 404 LEARNER_NOT_FOUND. |
| Malformed ID | for example 12345 |
Learner profile returns 400 INVALID_NATIONAL_ID. |
| Pattern | Engine auth.type |
Operations | Credential (training only) |
|---|---|---|---|
| Username/password login → bearer token | USER_CREDENTIALS |
learner, eligibility, enrollments, guardians, documents | POST /victor/keza/v1/auth/login with irembo-integration / Keza@2026; token at data.token |
| OAuth 2.0 client credentials | CLIENT_CREDENTIALS |
scholarships | POST /victor/keza/oauth/token?grant_type=client_credentials&client_id=irembo-hub&client_secret=keza-secret-2026; token at access_token |
| Static API key header | TOKEN |
programs | X-API-Key: keza-api-key-victor-2026 |
| HTTP Basic | BASIC |
certificates | user victor-certs, password Cert@2026 |
| Required channel header, no auth | none | centers | X-Channel: IREMBO |
| None | none | districts | — |
These credentials are published on purpose: they exist only in the training mocks. In a real integration, keep every secret in an environment variable defined by the platform team, and reference it in the endpoint configuration. It never appears in a course, a ticket or a wiki page.
Environment variable format. The whole JSON string value must be exactly
"${NAME}", for example"password": "${KEZA_PASSWORD}","token": "${KEZA_API_KEY}","client_secret": "${KEZA_CLIENT_SECRET}". Partial strings ("Bearer ${KEZA_API_KEY}"),"$NAME","{{NAME}}"and unquoted$NAMEare not resolved. A placeholder in the endpoint URL is rejected when you save. If the variable is not defined, the call fails before any HTTP request with500 MISSING_ENVIRONMENT_VARIABLE.
| Method | Path | Mock code | IremboHub endpoint(s) | Auth |
|---|---|---|---|---|
POST |
/victor/keza/v1/auth/login |
VICTOR_KEZA_LOGIN |
used by the USER_CREDENTIALS auth block |
none |
POST |
/victor/keza/oauth/token |
VICTOR_KEZA_OAUTH_TOKEN |
used by the CLIENT_CREDENTIALS auth block |
client credentials in the query |
GET |
/victor/keza/v1/learners/{nationalId} |
VICTOR_KEZA_LEARNER |
VICTOR_KEZA_LEARNER_FETCH |
bearer (login) |
GET |
/victor/keza/v1/eligibility |
VICTOR_KEZA_ELIGIBILITY |
VICTOR_KEZA_ELIGIBILITY_FETCH, VICTOR_KEZA_ELIGIBILITY_DECISION |
bearer (login) |
GET |
/victor/keza/v1/programs |
VICTOR_KEZA_PROGRAMS |
VICTOR_KEZA_PROGRAMS_FETCH, VICTOR_KEZA_PROGRAM_OPTIONS_FETCH |
API key |
GET |
/victor/keza/v1/centers |
VICTOR_KEZA_CENTERS |
VICTOR_KEZA_CENTERS_FETCH |
X-Channel header |
GET |
/victor/keza/v1/districts |
VICTOR_KEZA_DISTRICTS |
VICTOR_KEZA_DISTRICTS_FETCH |
none |
GET |
/victor/keza/v1/certificates/{certificateNumber} |
VICTOR_KEZA_CERTIFICATES |
VICTOR_KEZA_CERTIFICATE_FETCH |
Basic |
GET |
/victor/keza/v1/scholarships |
VICTOR_KEZA_SCHOLARSHIPS |
VICTOR_KEZA_SCHOLARSHIP_FETCH |
OAuth bearer |
POST |
/victor/keza/v1/enrollments |
VICTOR_KEZA_ENROLLMENTS |
VICTOR_KEZA_ENROLLMENT_PUSH, VICTOR_KEZA_ENROLLMENT_SAFE_PUSH |
bearer (login) |
POST |
/victor/keza/v1/enrollments/{enrollmentId}/guardians |
VICTOR_KEZA_GUARDIANS |
VICTOR_KEZA_GUARDIANS_PUSH |
bearer (login) |
POST |
/victor/keza/v1/documents |
VICTOR_KEZA_DOCUMENTS |
VICTOR_KEZA_DOCUMENT_PUSH |
bearer (login) |
GET |
/victor/keza/v1/exam-sessions |
VICTOR_KEZA_EXAM_SESSIONS |
VICTOR_KEZA_EXAM_SESSIONS_FETCH (scheduled job fetch leg) |
X-Channel header |
POST |
/victor/irembo/v1/schedules/bulk-create |
VICTOR_IREMBO_EXAM_SCHEDULE |
VICTOR_IREMBO_EXAM_SCHEDULE_PUSH (scheduled job push leg; Irembo-side receiver stand-in) |
none |
Rules in each mock are evaluated in order. The first rule whose conditions all match wins. The rule marked default answers everything else.
POST /victor/keza/v1/auth/login
Issues the bearer token used by the USER_CREDENTIALS operations.
{ "username": "irembo-integration", "password": "Keza@2026" }
| Status | Rule | Body |
|---|---|---|
200 OK |
Valid Irembo credentials | {"status":"SUCCESS","data":{"token":"keza-victor-token-7f3a","tokenType":"Bearer","expiresIn":3600}} |
401 Unauthorized |
Otherwise | {"status":"FAILED","code":"INVALID_CREDENTIALS","message":"Username or password is incorrect."} |
"auth": {
"type": "USER_CREDENTIALS",
"baseUrl": "http://mock-service-engine.apps:8000",
"parameters": {
"authPath": "/victor/keza/v1/auth/login",
"bodyTemplate": { "username": "irembo-integration", "password": "Keza@2026" }
},
"tokenConfig": {
"storageKey": "VICTOR_KEZA_TOKEN",
"headerName": "Authorization",
"tokenPrefix": "Bearer",
"expiryBuffer": 300,
"extractPath": "data.token"
}
}
The engine caches the token under storageKey and renews it expiryBuffer seconds before expiresIn runs out.
POST /victor/keza/oauth/token?grant_type=client_credentials&client_id=irembo-hub&client_secret=keza-secret-2026
| Status | Rule | Body |
|---|---|---|
200 OK |
Valid client | {"access_token":"keza-oauth-9b21","token_type":"Bearer","expires_in":3600,"scope":"scholarships.read"} |
401 Unauthorized |
Otherwise | {"error":"invalid_client","error_description":"Client authentication failed."} |
The training mock reads the client credentials from the query string. The production KEZA token endpoint uses HTTP Basic and a form body. See Create an access token.
GET /victor/keza/v1/learners/{nationalId}
| Location | Name | Type | Required | Constraints |
|---|---|---|---|---|
| Path | nationalId |
string | Yes | 16 digits starting with 1 |
| Status | Rule | Body |
|---|---|---|
200 OK |
Known learner (Aline) | Profile below |
404 Not Found |
Well-formed but unknown ID (^1[0-9]{15}$) |
{"code":"LEARNER_NOT_FOUND","message":"No learner matches the supplied National ID."} |
400 Bad Request |
Malformed ID | {"code":"INVALID_NATIONAL_ID","message":"nationalId must be 16 digits starting with 1."} |
401 Unauthorized |
Otherwise (no or wrong token) | {"code":"UNAUTHORIZED","message":"Missing or invalid bearer token."} |
{
"nationalId": "1199880012345678",
"firstName": "Aline",
"lastName": "Mukamana",
"dateOfBirth": "1998-04-12",
"gender": "F",
"phone": "0788000111",
"district": "Gasabo",
"enrollments": [
{ "programCode": "WEB_BASICS", "status": "COMPLETED", "year": 2025 }
]
}
GET /victor/keza/v1/eligibility?nationalId={nationalId}&program={programCode}
| Status | Rule | Notes |
|---|---|---|
200 OK |
Eligible learner (Aline + DATA_ANALYTICS) |
eligibility.status = "ELIGIBLE" |
200 OK |
Not eligible learner (Eric) | eligibility.status = "NOT_ELIGIBLE", reasonCode = "AGE_LIMIT" |
404 Not Found |
Authenticated but unknown learner | {"found":false,"code":"LEARNER_NOT_FOUND",...} |
401 Unauthorized |
Otherwise | UNAUTHORIZED |
{
"found": true,
"learner": { "nationalId": "1199880012345678", "firstName": "Aline", "lastName": "Mukamana", "district": "Gasabo" },
"eligibility": { "status": "ELIGIBLE", "programCode": "DATA_ANALYTICS", "reasonCode": null, "evaluatedAt": "2026-09-26T08:00:00Z" }
}
A NOT_ELIGIBLE answer is still HTTP 200. VICTOR_KEZA_ELIGIBILITY_DECISION shows how to turn it into a business error with response.validation.failureCondition.
GET /victor/keza/v1/programs
X-API-Key: keza-api-key-victor-2026
| Status | Rule | Body |
|---|---|---|
200 OK |
Valid API key | {"programs":[...]}: DATA_ANALYTICS (free, 12 weeks), WEB_BASICS (15 000 RWF, 8 weeks), CLOUD_OPS (inactive) |
401 Unauthorized |
Otherwise | {"code":"INVALID_API_KEY","message":"X-API-Key header is missing or wrong."} |
Each program has code, title, durationWeeks, fee.amount, fee.currency, minAge, maxAge and active. VICTOR_KEZA_PROGRAM_OPTIONS_FETCH reshapes this list into form options with response.transform.
GET /victor/keza/v1/centers?district={DISTRICT}&program={programCode}
X-Channel: IREMBO
| Status | Rule | Body |
|---|---|---|
200 OK |
Gasabo + DATA_ANALYTICS |
Two centres: KZ-GSB-01 Kacyiru Digital Hub (12 seats, starts 2026-10-05) and KZ-GSB-02 Kimironko Tech Lab (0 seats) |
200 OK |
Any other district | {"district":"<district>","program":"<program>","centers":[]} (echoed with response templating) |
400 Bad Request |
Otherwise (missing X-Channel) |
{"code":"MISSING_CHANNEL","message":"Header X-Channel is required."} |
The district must be upper case. The course mapping sends $uppercase($trim(districtName)).
GET /victor/keza/v1/districts
Always returns 200 with five districts (Gasabo, Kicukiro, Nyarugenge, Huye, Musanze) and generatedAt. The mock waits 2.5 seconds on purpose. VICTOR_KEZA_DISTRICTS_FETCH enables response.cache (ttl: 300), so only the first call in five minutes pays that delay.
GET /victor/keza/v1/certificates/{certificateNumber}
Authorization: Basic base64(victor-certs:Cert@2026)
| Status | Rule | Body |
|---|---|---|
200 OK |
Valid certificate KZ-CERT-2026-0001 |
{"certificateNumber":"KZ-CERT-2026-0001","holder":{"nationalId":"1199880012345678","fullName":"Aline Mukamana"},"programCode":"WEB_BASICS","issuedOn":"2025-12-15","status":"VALID"} |
404 Not Found |
Unknown certificate | {"code":"CERTIFICATE_NOT_FOUND","message":"No certificate with this number."} |
401 Unauthorized |
Otherwise | {"code":"UNAUTHORIZED","message":"Basic credentials missing or wrong."} |
GET /victor/keza/v1/scholarships?nationalId={nationalId}
Authorization: Bearer <access_token from the OAuth token operation>
| Status | Rule | Body |
|---|---|---|
200 OK |
Aline has a scholarship | {"nationalId":"1199880012345678","scholarships":[{"scholarshipCode":"KZ-SCH-DA-2026","programCode":"DATA_ANALYTICS","coverage":"FULL","status":"AWARDED"}]} |
200 OK |
No scholarship | {"nationalId":"<id>","scholarships":[]} |
401 Unauthorized |
Otherwise | {"error":"invalid_token","error_description":"Access token missing or expired."} |
POST /victor/keza/v1/enrollments
Authorization: Bearer keza-victor-token-7f3a
| Property | Type | Required | Description |
|---|---|---|---|
applicationNumber |
string | Yes | IremboHub application number. |
programCode |
string | Yes | Program code. |
centerCode |
string | No | Chosen centre. |
learner.nationalId |
string | Yes | 16 digits. |
learner.fullName |
string | No | First and last name. |
learner.phone |
string | No | Contact phone. |
| Status | Rule | Body |
|---|---|---|
409 Conflict |
Already enrolled (Eric) | {"code":"ALREADY_ENROLLED","message":"Learner already has an active enrollment for this program.","enrollmentId":"ENR-2026-0042","status":"CONFIRMED"} |
201 Created |
New enrollment (Aline + DATA_ANALYTICS + applicationNumber present) |
{"enrollmentId":"ENR-2026-0107","status":"CONFIRMED","applicationNumber":"<echoed>","centerCode":"<echoed>","startDate":"2026-10-05"} |
422 Unprocessable Entity |
Invalid body | {"code":"VALIDATION_ERROR","message":"applicationNumber, programCode and learner.nationalId are required."} |
401 Unauthorized |
Otherwise | UNAUTHORIZED |
VICTOR_KEZA_ENROLLMENT_SAFE_PUSH makes a retried push harmless. It treats ALREADY_ENROLLED as a success with response.validation.successCondition and successTransform.
POST /victor/keza/v1/enrollments/{enrollmentId}/guardians
Authorization: Bearer keza-victor-token-7f3a
Registers one guardian per call. VICTOR_KEZA_GUARDIANS_PUSH uses iterateOver: "guardians", so one IremboHub push makes one call per repeater row. The results are collected under guardianResults.
| Status | Rule | Body |
|---|---|---|
201 Created |
Body has fullName |
{"guardianId":"GRD-<5 digits>","enrollmentId":"<path>","fullName":"<echoed>","relationship":"<echoed>"} |
422 Unprocessable Entity |
Invalid guardian | {"code":"VALIDATION_ERROR","message":"fullName is required."} |
401 Unauthorized |
Otherwise | UNAUTHORIZED |
POST /victor/keza/v1/documents
Authorization: Bearer keza-victor-token-7f3a
{
"applicationNumber": "APP-2026-000123",
"document": { "type": "PREVIOUS_CERTIFICATE", "name": "certificate.pdf", "url": "<secure download link>" }
}
| Status | Rule | Body |
|---|---|---|
202 Accepted |
Body has document.url |
{"documentId":"DOC-<6 digits>","status":"RECEIVED","applicationNumber":"<echoed>"} |
422 Unprocessable Entity |
Missing document | {"code":"DOCUMENT_REQUIRED","message":"document.url is required."} |
401 Unauthorized |
Otherwise | UNAUTHORIZED |
The mapping builds the link with generateAttachmentURL(CERTIFICATE). KEZA receives a time-limited URL, not the file bytes.
GET /victor/keza/v1/exam-sessions?centerCode={centerCode}&from={yyyy-MM-dd}&to={yyyy-MM-dd}
X-Channel: IREMBO
| Status | Rule | Body |
|---|---|---|
200 OK |
X-Channel: IREMBO + centerCode=KZ-GSB-01 |
Kacyiru Digital Hub with 3 sessions: KZX-GSB01-AM and KZX-GSB01-PM on from (Room A, 20 and 12 seats), KZX-GSB01-LAST on to (Room B, 0 seats). from/to are echoed with response templating |
200 OK |
X-Channel: IREMBO, any other centre |
{"centerCode":"<centre>","from":"…","to":"…","sessions":[]} |
400 Bad Request |
Otherwise (missing X-Channel) |
{"code":"MISSING_CHANNEL","message":"Header X-Channel is required."} |
This operation is not called by a form. It is pulled every morning by the scheduled job VICTOR_KEZA_EXAM_SCHEDULE_SYNC (below).
POST /victor/irembo/v1/schedules/bulk-create?scheduleCode={scheduleCode}
Content-Type: application/json
[ { "groupType": {"code","name"}, "group": {"code","name"}, "from": "<ISO date-time>", "to": "<ISO date-time>", "capacity": 20 }, … ]
| Status | Rule | Body |
|---|---|---|
200 OK |
scheduleCode=VICTOR_KEZA_EXAMS |
{"responseCode":"SCHEDULE_BULK_CREATED","message":"Bulk-create completed","data":{"scheduleCode":"…","rowsReceived":3,"groupTypesUpserted":1,"groupsUpserted":2,"timeRangesUpserted":3,"slotsUpserted":3}} |
404 Not Found |
Otherwise | {"responseCode":"SCHEDULE_NOT_FOUND","message":"No schedule with this code."} |
It imitates the service engine's internal POST …/service/api/v1/internal/schedules/bulk-create?scheduleCode=…. The real route is only reachable engine to engine (the gateway answers 403), accepts at most 1000 rows per call, and returns 404 until the organisation has created the schedule (Org Portal → Slots and booking manager). The summary is nested under data in both.
The course converts the real MINIJUST notary-availability job (MINIJUST_FETCH_MINIJUST_AVAILABILITY: MINIJUST_NOTARY_AVAIL_FETCH → MINIJUST_NOTARY_AVAIL_PUSH, weekly) into a KEZA example. The portal calls it a Scheduled Endpoint (/dashboard/jobs; needs the JOB permission).
| Field | Value |
|---|---|
| Schedule code | VICTOR_KEZA_EXAM_SCHEDULE_SYNC (at most 50 characters) |
| Fetch endpoint | VICTOR_KEZA_EXAM_SESSIONS_FETCH (FMAPPING, URL MOCK_BASE_URL/victor/keza/v1/exam-sessions) |
| Push endpoint | VICTOR_IREMBO_EXAM_SCHEDULE_PUSH (PMAPPING, URL MOCK_BASE_URL/victor/irembo/v1/schedules/bulk-create) |
| Schedule | Daily at 04:00 → cron 0 0 4 * * * (6 fields, UTC) = 06:00 Kigali. Next run and run times are displayed in the browser's time zone |
| Inputs | {"centerCode":"KZ-GSB-01","scheduleCode":"VICTOR_KEZA_EXAMS","from":"today","to":"today + 14 days"} |
| Pagination | off (one fetch per run). If you turn it on, forward page and size in the FETCH mapping query, or every page fetches the same rows and the run ends CAPPED |
FETCH mapping:
{
"method": "GET",
"headers": { "X-Channel": "IREMBO" },
"query": { "centerCode": centerCode, "from": from, "to": to },
"response": {
"transform": {
"data": [sessions.{
"groupType": { "code": $$.centerCode, "name": $$.centerName },
"group": { "code": room, "name": roomName },
"from": examDate & "T" & startTime & ":00+02:00",
"to": examDate & "T" & endTime & ":00+02:00",
"capacity": seatsLeft
}]
}
}
}
PUSH mapping (the job sends {"data": <canonical fetch response>, "scheduleCode": …, "idempotencyKey": …}):
{
"method": "POST",
"query": { "scheduleCode": scheduleCode },
"body": data.data,
"response": { "transform": data }
}
How a run behaves (verified on staging, 30 Sep 2026):
| Situation | Run result |
|---|---|
| First run of the day | SUCCESS · Pages 1 · Rows 3 / 0 / 3 · ok (slotsUpserted counts as updated) |
| Second run the same UTC day | SUCCESS · 3 / 0 / 0: same idempotency key <jobCode>\|<sorted inputs>\|p0\|P, the cached push is returned |
| Other centre (no sessions) | SUCCESS · Pages 0 · 0 / 0 / 0: nothing is pushed |
X-Channel removed from the FETCH mapping |
FAILED · PARTNER_ERROR_FETCH-VICTOR_KEZA_EXAM_SESSIONS_FETCH; Next run = now + 5 min (backoff 5, 10, 20, 40 min); disabled after 5 consecutive failures. After the fix, one SUCCESS resets Consecutive failures to 0 |
| Partner slower than 60 s | FAILED after about 61 s; Kibana: Read timed out → Error occurred while processing fetch request with Id … |
On staging the scheduler switch (scheduled.jobs.enabled) is off: the cron never fires, Run now always works. In Requests, the PUSH row has Application No VICTOR_KEZA_EXAM_SCHEDULE_SYNC|p0; the FETCH row has none. Kibana: search VICTOR_KEZA_EXAM_SCHEDULE_SYNC and service:"integration-mgmt-engine" (threads scheduled-job-N).
The course lesson is 6.5 Scheduled jobs: pull the KEZA exam schedule regularly.
Fetch operations go through the integration gateway. The partner's answer comes back as a JSON string in data.response:
curl -s -X POST "https://integration-gateway.staging.iremboinc.com/integration/v1/fetch/sync" \
-H "Authorization: Bearer $TOKEN" \
-H "portal: integration-management-portal" \
-H "Content-Type: application/json" \
-d '{
"endpointCode": "VICTOR_KEZA_LEARNER_FETCH",
"callerId": "victor-course",
"requester": "victor-course",
"payload": { "nationalId": "1199880012345678" }
}' | jq '.data.response | fromjson'
Push operations go through the admin gateway. They also need applicationNumber, and the partner's answer comes back as a JSON string in data.message:
curl -s -X POST "https://admin-gateway.staging.iremboinc.com/integration/v1/push/sync" \
-H "Authorization: Bearer $TOKEN" \
-H "portal: administration-management-portal" \
-H "Content-Type: application/json" \
-d '{
"endpointCode": "VICTOR_KEZA_ENROLLMENT_PUSH",
"callerId": "victor-course",
"requester": "victor-course",
"applicationNumber": "C260926081500VICTR1",
"payload": {
"applicationNumber": "C260926081500VICTR1",
"programCode": "DATA_ANALYTICS",
"centerCode": "KZ-GSB-01",
"nationalId": "1199880012345678",
"firstName": "Aline",
"lastName": "Mukamana",
"phoneNumber": "0788000111"
}
}' | jq '.data.message | fromjson'
Real answer from staging:
{
"enrollmentId": "ENR-2026-0107",
"status": "CONFIRMED",
"applicationNumber": "C260926081500VICTR1",
"centerCode": "KZ-GSB-01",
"startDate": "2026-10-05"
}
$TOKEN is a placeholder. Log in with your own training account ($USER_EMAIL / $USER_PASSWORD) to get one, and never paste a real token into shared material.
| Topic | Production (KEZA API Reference) | Training (this page) |
|---|---|---|
| Authentication | OAuth 2.0 client credentials only, with scopes | Six patterns: login, OAuth, API key, Basic, channel header, none |
| Eligibility status | Eligible / Ineligible |
ELIGIBLE / NOT_ELIGIBLE |
| Unknown learner | 200 with found: false |
404 with found: false |
| Enrollment key | applicationReference + Idempotency-Key header |
applicationNumber; duplicates answer 409 ALREADY_ENROLLED |
| Errors | application/problem+json (ProblemDetails) |
Simple {code, message} bodies |
| Extra operations | — | learners, programs, centers, districts, certificates, scholarships, guardians, documents, exam sessions (+ an Irembo schedule receiver) |
The differences are deliberate. An integration manager must read each partner's contract and map it, not assume one shape.