๐ŸŽ“ Solvo

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).

ResponseMeaning
401 UnauthorizedMissing or invalid credentials.
429 Too Many RequestsYou'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
}
FieldRequiredNotes
student_referenceYesYour own identifier for this student. ElimuPay has no students table row for you โ€” this string is the only link back to your system.
total_amountYesKES, the full amount being financed.
number_of_instalmentsYesInteger, at least 1.
term_start, term_endNoYYYY-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_idNoIf 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: a hosted page โ€” the same pattern Stripe Checkout and similar products use โ€” 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:

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: ..."