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).
Responses:
- 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 from 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 }
Why a hosted link, not direct STK-Push-via-API: the brief asked us to confirm this choice. A hosted page — the same pattern used by Stripe Checkout and similar products — means your team never has to build M-Pesa STK Push integration, phone-number capture UI, or handle Safaricom's callback format at all; you just redirect or link your parent-facing UI to the URL we give you, and we handle the entire payment interaction on a page we host and maintain. If your integration specifically needs to trigger STK Push directly from your own backend without redirecting a user anywhere, tell MoJamoza — that's a reasonable v2 addition, not built in v1.
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: ..."