ElimuPay Standalone API โ Developer Reference
Version v1 ยท Base URL {APP_BASE_URL}/api/elimupay/v1/
ElimuPay's fee-instalment financing engine, available as a standalone API for schools with no other relationship to the MoJamoza platform. You keep your own school management system; ElimuPay handles generating instalment schedules and collecting M-Pesa payments against them.
Who this is for
Your school's own backend system. You supply your own identifier for each student
(student_reference) โ ElimuPay has no record of your students beyond that
reference string.
Authentication
Every request (except the hosted payment page โ see below) requires two headers, issued when your API client was onboarded:
X-ElimuPay-Key: ep_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-ElimuPay-Secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Both are shown to you once at onboarding and never recoverable afterward โ if lost, ask MoJamoza to issue new credentials (the old ones will need revoking).
| Response | Meaning |
|---|---|
401 Unauthorized | Missing or invalid credentials. |
429 Too Many Requests | You've exceeded your assigned rate limit (requests/minute, set at onboarding; ask MoJamoza if you need it raised). |
Endpoints
Create an instalment plan
POST /api/elimupay/v1/instalment-plans/create.php
Content-Type: application/json
{
"student_reference": "YOUR-SYSTEM-STUDENT-ID-123",
"total_amount": 45000.00,
"number_of_instalments": 3,
"term_start": "2026-09-01",
"term_end": "2026-12-12",
"financing_partner_id": null
}
| Field | Required | Notes |
|---|---|---|
student_reference | Yes | Your own identifier for this student. ElimuPay has no students table row for you โ this string is the only link back to your system. |
total_amount | Yes | KES, the full amount being financed. |
number_of_instalments | Yes | Integer, at least 1. |
term_start, term_end | No | YYYY-MM-DD. ElimuPay has no academic calendar for a school it doesn't operate โ supply your own term dates so instalment due dates align with your actual term. Omit both to fall back to a fixed 30-day-per-instalment interval starting today. |
financing_partner_id | No | If you know which licensed financing partner is backing this plan. Omit for a self-serve (no third-party financing) plan. |
Response 201 Created:
{
"instalment_plan_id": 42,
"schedule": [
{ "instalment_number": 1, "due_date": "2026-10-04", "amount": 15000.00 },
{ "instalment_number": 2, "due_date": "2026-11-08", "amount": 15000.00 },
{ "instalment_number": 3, "due_date": "2026-12-12", "amount": 15000.00 }
]
}
A student reference can only have one pending/active plan at a
time โ attempting to create a second while one exists returns 422 with an
explanatory error. A cancelled or defaulted plan doesn't block creating a fresh one.
Response 422 Unprocessable Entity on validation failure (e.g.
missing student_reference, a duplicate plan) โ { "error": "..." }.
Retrieve a plan
GET /api/elimupay/v1/instalment-plans/get.php?id=42
Returns the plan's current status and full schedule โ useful for polling as an alternative
to relying solely on webhooks. Scoped to your own client: requesting a plan ID that isn't yours
returns 404, not another client's data.
{
"instalment_plan_id": 42,
"student_reference": "YOUR-SYSTEM-STUDENT-ID-123",
"total_amount": 45000.00,
"number_of_instalments": 3,
"status": "active",
"schedule": [
{ "instalment_number": 1, "due_date": "2026-10-04", "amount_due": 15000.00, "amount_paid": 15000.00, "status": "paid" },
{ "instalment_number": 2, "due_date": "2026-11-08", "amount_due": 15000.00, "amount_paid": 0.00, "status": "pending" },
{ "instalment_number": 3, "due_date": "2026-12-12", "amount_due": 15000.00, "amount_paid": 0.00, "status": "pending" }
]
}
Plan status values: pending (awaiting financing-partner
disbursement confirmation, only relevant if financing_partner_id was set),
active, completed, defaulted, cancelled.
Instalment status values: pending, paid,
overdue, waived.
Generate a payment link
POST /api/elimupay/v1/instalment-plans/payment-link.php
Content-Type: application/json
{ "instalment_schedule_id": 101 }
Response 200 OK:
{ "payment_url": "https://your-mojamoza-domain.example.com/elimupay-pay/index.php?token=...", "expires_in_seconds": 259200 }
Redirect or link your parent-facing UI to payment_url. The link expires after
expires_in_seconds (3 days by default) โ generate a fresh one if it lapses. The
link itself is the authorization for that one instalment; no further API call is needed once
you've handed it to your parent.
Webhooks
If you provided a webhook_url at onboarding, ElimuPay POSTs to it on:
instalment.payment_receivedโ fired every time a payment lands against an instalment (even a partial one).instalment_plan.completedโ fired once every instalment in a plan is fully paid.
Request body:
{
"event": "instalment.payment_received",
"data": { "instalment_plan_id": 42, "instalment_schedule_id": 101, "amount_paid": 15000.00, "status": "paid" },
"sent_at": "2026-08-12T14:03:00+00:00"
}
Verifying the signature โ every webhook request carries:
X-ElimuPay-Signature: sha256=<hex-encoded HMAC-SHA256 of the raw request body>
Computed using the webhook_secret you were given at onboarding (shown once,
alongside your API credentials). Verify it before trusting the payload:
$expected = 'sha256=' . hash_hmac('sha256', $rawRequestBody, $yourWebhookSecret);
if (!hash_equals($expected, $_SERVER['HTTP_X_ELIMUPAY_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
Webhook delivery is best-effort with no retry queue yet โ if your endpoint
is down when an event fires, that specific notification is lost (though the underlying data is
always retrievable via GET instalment-plans/get.php). Poll periodically if
reliability matters more than real-time notification for your integration.
Usage & billing
Every API call โ successful or not โ is logged for usage-based billing. There is no self-service invoicing UI yet; MoJamoza can pull your usage on request.
Errors
All error responses share the shape { "error": "human-readable message" }, with
an appropriate HTTP status code (400 malformed request, 401 auth
failure, 404 not found / not yours, 422 validation failure,
429 rate limited, 500 internal error โ retry with backoff).
Example: full flow with curl
# 1. Create a plan
curl -X POST https://YOUR_DOMAIN/api/elimupay/v1/instalment-plans/create.php \
-H "X-ElimuPay-Key: ep_..." -H "X-ElimuPay-Secret: ..." \
-H "Content-Type: application/json" \
-d '{"student_reference":"STU-001","total_amount":30000,"number_of_instalments":3}'
# 2. Generate a payment link for the first instalment
curl -X POST https://YOUR_DOMAIN/api/elimupay/v1/instalment-plans/payment-link.php \
-H "X-ElimuPay-Key: ep_..." -H "X-ElimuPay-Secret: ..." \
-H "Content-Type: application/json" \
-d '{"instalment_schedule_id":101}'
# 3. Hand payment_url to your parent โ they complete payment on ElimuPay's hosted page.
# 4. Poll for status, or wait for your configured webhook.
curl https://YOUR_DOMAIN/api/elimupay/v1/instalment-plans/get.php?id=42 \
-H "X-ElimuPay-Key: ep_..." -H "X-ElimuPay-Secret: ..."