API version: 1.0
Protocol: HTTPS
Data format: JSON
Authentication: OAuth 2.0 Client Credentials
KEZA is a course-management platform that evaluates learner eligibility and receives approved enrollments from authorized partner systems.
This reference defines the KEZA HTTP API contract. It is independent of any client application, integration platform, programming language, or training environment.
All paths in this reference are relative to:
BASE_URL
KEZA provides a separate BASE_URL and credentials for each environment during partner onboarding.
Example construction:
BASE_URL + /v1/eligibility
Requirements:
BASE_URL.BASE_URL in application source code.Business endpoints accept and return JSON.
Accept: application/json
Content-Type: application/json
Error responses use:
Content-Type: application/problem+json
The OAuth token endpoint accepts form-encoded data and returns JSON.
Request and response content uses UTF-8.
YYYY-MM-DD.2026-09-03T08:30:00Z.Identifiers are strings even when they contain only digits.
KEZA-REG-{number}.Clients must ignore response properties they do not recognize. KEZA may add optional response properties without publishing a new major API version.
KEZA will publish a new major version before it:
All /v1 operations require an OAuth 2.0 bearer access token obtained with the Client Credentials grant.
| Scope | Permission |
|---|---|
eligibility:read |
Check a learner's eligibility. |
enrollments:write |
Create an enrollment. |
enrollments:read |
Retrieve an enrollment for reconciliation. |
Request only the scopes required by the consuming application.
POST /oauth2/token
Creates a short-lived access token for a registered KEZA client.
Operation ID: createAccessToken
Authentication: HTTP Basic with client ID and client secret
| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Basic base64(client_id:client_secret) |
Content-Type |
Yes | application/x-www-form-urlencoded |
Accept |
Recommended | application/json |
X-Correlation-Id |
Recommended | A UUID supplied by the client. |
Use an HTTP client library to construct the Basic header. Do not store or manually paste the Base64 value.
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
grant_type |
string | Yes | Must be client_credentials |
OAuth grant used by system clients. |
scope |
string | Yes | Space-separated supported scopes | Permissions requested for the token. |
| Status | Meaning | Response |
|---|---|---|
200 OK |
Token issued | AccessTokenResponse |
400 Bad Request |
Invalid grant, request, or scope | OAuthError |
401 Unauthorized |
Invalid client credentials | OAuthError |
429 Too Many Requests |
Token request limit exceeded | OAuthError |
503 Service Unavailable |
Authorization service unavailable | OAuthError |
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json
{
"access_token": "<access-token>",
"token_type": "Bearer",
"expires_in": 300,
"scope": "eligibility:read enrollments:write"
}
{
"error": "invalid_client",
"error_description": "The client credentials are invalid."
}
Supported OAuth error values:
| Error | Status | Resolution |
|---|---|---|
invalid_request |
400 |
Supply each required field exactly once. |
unsupported_grant_type |
400 |
Use client_credentials. |
invalid_scope |
400 |
Request only scopes assigned to the client. |
invalid_client |
401 |
Correct or rotate the client credentials. |
temporarily_unavailable |
429 or 503 |
Wait as instructed and retry with backoff. |
A 401 response also includes:
WWW-Authenticate: Basic realm="keza-token"
expires_in period ends.401 INVALID_TOKEN.curl --request POST \
--url "$BASE_URL/oauth2/token" \
--user "$KEZA_CLIENT_ID:$KEZA_CLIENT_SECRET" \
--header "Accept: application/json" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "scope=eligibility:read"
| Header | Required | Applies to | Description |
|---|---|---|---|
Authorization |
Yes | All /v1 operations |
Bearer {access_token} |
Accept |
Yes | All operations | application/json |
Content-Type |
Yes | Requests with a body | application/json |
X-Correlation-Id |
Recommended | All operations | Client-generated UUID for tracing. |
Idempotency-Key |
Yes | POST /v1/enrollments |
Stable key for one logical enrollment. |
| Header | Description |
|---|---|
X-Correlation-Id |
Echoes the valid client value or contains a KEZA-generated UUID. |
X-KEZA-Request-Id |
Unique identifier assigned to this HTTP request by KEZA. |
Retry-After |
Seconds to wait before retrying a 429 or temporary 503 response. |
Location |
URI of a newly created enrollment after 201 Created. |
A retry is a new HTTP request and should use a new X-Correlation-Id. A retry of the same enrollment must keep the original Idempotency-Key.
GET /v1/eligibility
Checks whether KEZA recognizes the learner and whether the learner meets the requirements for a program.
Operation ID: getEligibility
Required scope: eligibility:read
Idempotent: Yes
| Location | Name | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
| Query | nationalId |
string | Yes | Exactly 16 digits | Learner identifier. |
| Query | program |
string | Yes | DIGITAL_SKILLS or DATA_FOUNDATIONS |
Program to evaluate. |
| Status | Meaning | Response |
|---|---|---|
200 OK |
Evaluation completed, including learner-not-found | EligibilityResponse |
400 Bad Request |
Query validation failed | ProblemDetails |
401 Unauthorized |
Token missing, expired, revoked, or invalid | ProblemDetails |
403 Forbidden |
Token lacks eligibility:read |
ProblemDetails |
429 Too Many Requests |
Rate limit exceeded | ProblemDetails |
500 Internal Server Error |
Unexpected KEZA failure | ProblemDetails |
502 Bad Gateway |
Invalid response from a KEZA dependency | ProblemDetails |
503 Service Unavailable |
KEZA or a dependency is unavailable | ProblemDetails |
504 Gateway Timeout |
A KEZA dependency timed out | ProblemDetails |
{
"found": true,
"student": {
"nationalId": "1000000000000001",
"name": "Keza Uwase"
},
"eligibility": {
"status": "Eligible",
"reasonCode": null,
"reason": null,
"programCode": "DIGITAL_SKILLS",
"evaluatedAt": "2026-09-03T08:30:00Z"
}
}
Ineligibility is a completed business evaluation, not an API error.
{
"found": true,
"student": {
"nationalId": "1000000000000002",
"name": "Mugisha Amani"
},
"eligibility": {
"status": "Ineligible",
"reasonCode": "PREREQUISITE_NOT_MET",
"reason": "The learner has not completed the required foundation module.",
"programCode": "DIGITAL_SKILLS",
"evaluatedAt": "2026-09-03T08:31:00Z"
}
}
A missing learner is a valid lookup result and is represented by found: false.
{
"found": false,
"code": "LEARNER_NOT_FOUND",
"message": "No learner was found for the supplied National ID and program."
}
{
"type": "https://developer.keza.example/problems/validation-error",
"title": "Request validation failed",
"status": 400,
"detail": "One or more request parameters are invalid.",
"instance": "urn:keza:request:kza-req-01J7A3TX30V6A9M5P2C4T8M9K1",
"code": "INVALID_NATIONAL_ID",
"correlationId": "acb33439-a673-416e-9b5c-45838637d6b4",
"errors": [
{
"location": "query",
"parameter": "nationalId",
"code": "INVALID_FORMAT",
"detail": "nationalId must contain exactly 16 digits."
}
]
}
curl --request GET \
--url "$BASE_URL/v1/eligibility?nationalId=1000000000000001&program=DIGITAL_SKILLS" \
--header "Accept: application/json" \
--header "Authorization: Bearer $KEZA_ACCESS_TOKEN" \
--header "X-Correlation-Id: acb33439-a673-416e-9b5c-45838637d6b4"
POST /v1/enrollments
Registers an approved learner application in KEZA.
Operation ID: createEnrollment
Required scope: enrollments:write
Idempotent: When the same valid Idempotency-Key is reused for the same logical request
In addition to the common headers:
| Header | Required | Constraints | Description |
|---|---|---|---|
Idempotency-Key |
Yes | 1–50 letters, digits, hyphens, or underscores | Must equal applicationReference. |
Schema: EnrollmentRequest
{
"applicationReference": "APP-2026-0001",
"learner": {
"nationalId": "1000000000000001",
"name": "Keza Uwase",
"email": "keza.uwase@example.test",
"phoneNumber": "+250780000001"
},
"programCode": "DIGITAL_SKILLS",
"decision": "APPROVED",
"approvedAt": "2026-09-03T08:45:00Z"
}
| Property | Type | Required | Constraints | Description |
|---|---|---|---|---|
applicationReference |
string | Yes | 1–50 allowed characters | Stable client reference for one application. |
learner |
object | Yes | See Learner |
Learner being enrolled. |
programCode |
string | Yes | Supported program code | Program receiving the enrollment. |
decision |
string | Yes | Must be APPROVED |
Final application decision. |
approvedAt |
string | Yes | RFC 3339 UTC; not future; no more than 30 days old | Approval time. |
At least one of learner.email or learner.phoneNumber is required.
| Status | Meaning | Response |
|---|---|---|
201 Created |
A new enrollment was recorded | EnrollmentResponse |
200 OK |
The same idempotent request was already recorded | EnrollmentResponse |
400 Bad Request |
Body or idempotency header validation failed | ProblemDetails |
401 Unauthorized |
Token missing, expired, revoked, or invalid | ProblemDetails |
403 Forbidden |
Token lacks enrollments:write |
ProblemDetails |
409 Conflict |
Idempotency key was reused with different business data | ProblemDetails |
415 Unsupported Media Type |
Request body is not JSON | ProblemDetails |
422 Unprocessable Content |
A business rule rejected the enrollment | ProblemDetails |
429 Too Many Requests |
Rate limit exceeded | ProblemDetails |
500 Internal Server Error |
Unexpected KEZA failure | ProblemDetails |
503 Service Unavailable |
KEZA is temporarily unavailable | ProblemDetails |
{
"accepted": true,
"applicationReference": "APP-2026-0001",
"partnerReference": "KEZA-REG-0042",
"status": "RECEIVED",
"receivedAt": "2026-09-03T08:45:01Z"
}
The response includes:
Location: /v1/enrollments/KEZA-REG-0042
RECEIVED means KEZA safely recorded the request. It does not mean every later course-administration activity is complete.
When the same idempotency key and same effective request body are sent again, KEZA returns the original resource.
{
"accepted": true,
"applicationReference": "APP-2026-0001",
"partnerReference": "KEZA-REG-0042",
"status": "ALREADY_RECEIVED",
"receivedAt": "2026-09-03T08:45:01Z"
}
JSON property order and insignificant whitespace do not affect idempotency comparison.
KEZA returns a conflict when the key was already used with different business data.
{
"type": "https://developer.keza.example/problems/idempotency-conflict",
"title": "Idempotency conflict",
"status": 409,
"detail": "The idempotency key was already used with different enrollment data.",
"instance": "urn:keza:request:kza-req-01J7A6HVV9P7A8N2P1T7H0Y4Z6",
"code": "IDEMPOTENCY_CONFLICT",
"correlationId": "742f4fca-8d85-4daa-b583-4abdfdb593ad",
"errors": []
}
Do not generate a new key to bypass this response.
{
"type": "https://developer.keza.example/problems/enrollment-rejected",
"title": "Enrollment rejected",
"status": 422,
"detail": "The application cannot be registered.",
"instance": "urn:keza:request:kza-req-01J7A6Q7W3D6X7K9M0R2F5S8T1",
"code": "LEARNER_NOT_ELIGIBLE",
"correlationId": "68da5bdf-1af7-4596-a56b-2478df35a5ea",
"errors": [
{
"pointer": "#/learner/nationalId",
"code": "LEARNER_NOT_ELIGIBLE",
"detail": "The learner is not eligible for DIGITAL_SKILLS."
}
]
}
Documented business rejection codes:
LEARNER_NOT_FOUNDLEARNER_NOT_ELIGIBLEINVALID_DECISIONAPPROVAL_TOO_OLDPROGRAM_CLOSEDcurl --request POST \
--url "$BASE_URL/v1/enrollments" \
--header "Accept: application/json" \
--header "Authorization: Bearer $KEZA_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: APP-2026-0001" \
--header "X-Correlation-Id: 68da5bdf-1af7-4596-a56b-2478df35a5ea" \
--data '{
"applicationReference": "APP-2026-0001",
"learner": {
"nationalId": "1000000000000001",
"name": "Keza Uwase",
"email": "keza.uwase@example.test",
"phoneNumber": "+250780000001"
},
"programCode": "DIGITAL_SKILLS",
"decision": "APPROVED",
"approvedAt": "2026-09-03T08:45:00Z"
}'
GET /v1/enrollments/by-application-reference/{applicationReference}
Retrieves the enrollment created for a client application reference. This operation can reconcile an uncertain result after a create request times out.
Operation ID: getEnrollmentByApplicationReference
Required scope: enrollments:read
Idempotent: Yes
| Name | Type | Required | Constraints | Description |
|---|---|---|---|---|
applicationReference |
string | Yes | 1–50 allowed characters | Reference used in the original create request. |
| Status | Meaning | Response |
|---|---|---|
200 OK |
Enrollment found | EnrollmentLookupResponse |
400 Bad Request |
Application reference is malformed | ProblemDetails |
401 Unauthorized |
Token missing, expired, revoked, or invalid | ProblemDetails |
403 Forbidden |
Token lacks enrollments:read |
ProblemDetails |
404 Not Found |
No enrollment exists for the reference | ProblemDetails |
429 Too Many Requests |
Rate limit exceeded | ProblemDetails |
500 Internal Server Error |
Unexpected KEZA failure | ProblemDetails |
503 Service Unavailable |
KEZA is temporarily unavailable | ProblemDetails |
{
"found": true,
"applicationReference": "APP-2026-0001",
"partnerReference": "KEZA-REG-0042",
"status": "RECEIVED",
"receivedAt": "2026-09-03T08:45:01Z"
}
{
"type": "https://developer.keza.example/problems/enrollment-not-found",
"title": "Enrollment not found",
"status": 404,
"detail": "No enrollment exists for application reference APP-2026-0001.",
"instance": "urn:keza:request:kza-req-01J7A7A4F1B3D5G8H2J4K6M9N0",
"code": "ENROLLMENT_NOT_FOUND",
"correlationId": "1042f1c7-e8f0-4a27-9830-2995b92a7af7",
"errors": []
}
curl --request GET \
--url "$BASE_URL/v1/enrollments/by-application-reference/APP-2026-0001" \
--header "Accept: application/json" \
--header "Authorization: Bearer $KEZA_ACCESS_TOKEN" \
--header "X-Correlation-Id: 1042f1c7-e8f0-4a27-9830-2995b92a7af7"
| Property | Type | Required | Description |
|---|---|---|---|
access_token |
string | Yes | Opaque bearer token. |
token_type |
string | Yes | Always Bearer. |
expires_in |
integer | Yes | Lifetime in seconds. |
scope |
string | Yes | Space-separated granted scopes. |
| Property | Type | Required | Description |
|---|---|---|---|
error |
string | Yes | Stable OAuth error code. |
error_description |
string | No | Human-readable diagnostic guidance. |
| Property | Type | Required | Constraints |
|---|---|---|---|
nationalId |
string | Yes | Exactly 16 digits. |
name |
string | Yes | 1–100 characters. |
email |
string | Conditional | Valid email; maximum 254 characters. |
phoneNumber |
string | Conditional | E.164 format. |
At least one contact property is required when Learner appears in an enrollment request.
The response uses a discriminating found property.
When found is true:
| Property | Type | Required | Description |
|---|---|---|---|
found |
boolean | Yes | true |
student |
object | Yes | Recognized learner identity. |
eligibility |
object | Yes | Evaluation outcome. |
eligibility.status |
string | Yes | Eligible or Ineligible |
eligibility.reasonCode |
string or null | Yes | Stable reason code when ineligible. |
eligibility.reason |
string or null | Yes | Human-readable reason. |
eligibility.programCode |
string | Yes | Evaluated program. |
eligibility.evaluatedAt |
date-time | Yes | Evaluation time. |
When found is false:
| Property | Type | Required | Description |
|---|---|---|---|
found |
boolean | Yes | false |
code |
string | Yes | LEARNER_NOT_FOUND |
message |
string | Yes | Human-readable result. |
| Property | Type | Required | Constraints |
|---|---|---|---|
applicationReference |
string | Yes | Pattern ^[A-Za-z0-9_-]{1,50}$ |
learner |
Learner |
Yes | Email or phone required. |
programCode |
string | Yes | Supported program. |
decision |
string | Yes | APPROVED |
approvedAt |
date-time | Yes | UTC; not future; maximum age 30 days. |
| Property | Type | Required | Description |
|---|---|---|---|
accepted |
boolean | Yes | Always true for this response. |
applicationReference |
string | Yes | Client application reference. |
partnerReference |
string | Yes | KEZA enrollment reference. |
status |
string | Yes | RECEIVED or ALREADY_RECEIVED |
receivedAt |
date-time | Yes | Time KEZA first recorded the enrollment. |
| Property | Type | Required | Description |
|---|---|---|---|
found |
boolean | Yes | Always true for a 200 response. |
applicationReference |
string | Yes | Client application reference. |
partnerReference |
string | Yes | KEZA enrollment reference. |
status |
string | Yes | Current KEZA status. |
receivedAt |
date-time | Yes | Time KEZA recorded the enrollment. |
KEZA business endpoint errors use the Problem Details format.
| Property | Type | Required | Description |
|---|---|---|---|
type |
URI | Yes | Stable URI identifying the problem category. |
title |
string | Yes | Stable human-readable summary. |
status |
integer | Yes | Same status as the HTTP response. |
detail |
string | Yes | Guidance specific to this occurrence. |
instance |
URI reference | Yes | Identifier for this occurrence. |
code |
string | Yes | Stable KEZA machine-readable code. |
correlationId |
UUID | Yes | Correlation identifier for support. |
errors |
array | Yes | Field or parameter errors; empty when not applicable. |
Clients must branch on the HTTP status and code. Do not parse title or detail for business logic.
| HTTP status | Code | Applies to | Meaning |
|---|---|---|---|
400 |
INVALID_NATIONAL_ID |
Eligibility | National ID missing or malformed. |
400 |
INVALID_PROGRAM |
Eligibility | Program missing or unsupported. |
400 |
VALIDATION_ERROR |
Enrollment | One or more body fields are invalid. |
400 |
IDEMPOTENCY_KEY_MISSING |
Enrollment | Required header is absent. |
400 |
IDEMPOTENCY_KEY_MISMATCH |
Enrollment | Header differs from applicationReference. |
401 |
INVALID_TOKEN |
All /v1 operations |
Bearer token is unusable. |
403 |
INSUFFICIENT_SCOPE |
All /v1 operations |
Token lacks the required scope. |
404 |
ENROLLMENT_NOT_FOUND |
Enrollment lookup | No enrollment exists for the reference. |
409 |
IDEMPOTENCY_CONFLICT |
Enrollment | Key was used with different business data. |
415 |
UNSUPPORTED_MEDIA_TYPE |
Enrollment | Request is not JSON. |
422 |
LEARNER_NOT_FOUND |
Enrollment | Learner cannot be enrolled. |
422 |
LEARNER_NOT_ELIGIBLE |
Enrollment | Learner failed eligibility rules. |
422 |
INVALID_DECISION |
Enrollment | Decision is not APPROVED. |
422 |
APPROVAL_TOO_OLD |
Enrollment | Approval is older than 30 days. |
422 |
PROGRAM_CLOSED |
Enrollment | Program no longer accepts enrollments. |
429 |
RATE_LIMIT_EXCEEDED |
All operations | Client exceeded its assigned limit. |
500 |
INTERNAL_ERROR |
All operations | Unexpected KEZA error. |
502 |
INVALID_UPSTREAM_RESPONSE |
Eligibility | A dependency returned an invalid response. |
503 |
SERVICE_UNAVAILABLE |
All operations | Service is temporarily unavailable. |
504 |
UPSTREAM_TIMEOUT |
Eligibility | A dependency timed out. |
A 401 response contains:
WWW-Authenticate: Bearer realm="keza", error="invalid_token"
A 403 caused by missing permission contains:
WWW-Authenticate: Bearer realm="keza", error="insufficient_scope", scope="enrollments:write"
KEZA assigns limits per client and environment. The onboarding record defines the numeric limits.
When a limit is exceeded:
429 Too Many Requests;ProblemDetails with RATE_LIMIT_EXCEEDED;Retry-After specifies the minimum number of seconds to wait.Clients must not retry before Retry-After has elapsed.
Recommended client timeouts:
| Timeout | Recommended value |
|---|---|
| Connection | 3 seconds |
| Total request | 10 seconds |
A timeout means the client did not receive a usable response. It does not prove that KEZA did not process the request.
A client may retry:
429 or 503;429, 502, 503, or 504;Respect Retry-After when present. Otherwise use exponential backoff with jitter and no more than three attempts in one automated sequence.
Do not automatically retry:
400 validation failures;403 insufficient scope;404 resource not found;409 idempotency conflicts;415 unsupported media type;422 business rejections;401 after token renewal.After POST /v1/enrollments times out:
GET /v1/enrollments/by-application-reference/{applicationReference}.The IremboHub Integration Manager Certification uses a mocked KEZA with extra operations and one authentication pattern per operation. It is not part of this contract. See KEZA Training API (Victor sandbox).
| Operation | Training path | Auth pattern |
|---|---|---|
| Learner profile | GET /victor/keza/v1/learners/{nationalId} |
Login → bearer |
| Eligibility | GET /victor/keza/v1/eligibility |
Login → bearer |
| Program catalogue | GET /victor/keza/v1/programs |
API key (X-API-Key) |
| Training centres | GET /victor/keza/v1/centers |
Channel header |
| Districts (slow, cached) | GET /victor/keza/v1/districts |
None |
| Certificate verification | GET /victor/keza/v1/certificates/{certificateNumber} |
HTTP Basic |
| Scholarships | GET /victor/keza/v1/scholarships |
OAuth 2.0 client credentials |
| Enrollment | POST /victor/keza/v1/enrollments |
Login → bearer |
| Guardians (one per call) | POST /victor/keza/v1/enrollments/{enrollmentId}/guardians |
Login → bearer |
| Supporting document | POST /victor/keza/v1/documents |
Login → bearer |