Developers

Connect your systems to Mali Yangu.

Read the applications sent to you, move them through your own process, and get a signed message the moment something happens. Plain JSON over https.

Quick start

Three steps

  1. Upgrade to Premium or Enterprise, then open Developers in your portal.
  2. Create an API key and keep it on your server. It is shown once.
  3. Send it on every request as Authorization: Bearer YOUR_KEY.
curl "https://api.example.com/api/v1/applications?status=submitted&limit=10" \ -H "Authorization: Bearer my_live_xxxxxxxx_xxxxxxxxxxxxxxxx"

Keys have permissions: applications:read and applications:write. Give each system only what it needs, and revoke a key at any time.

Reference

Endpoints

Base address: https://api.example.com/api/v1

RequestWhat it doesKey needs
GET /meYour company, plan and this month's usageany key
GET /applicationsYour applications, newest first. Filter with status. Page with limit (up to 100) and cursorapplications:read
GET /applications/{id}One applicationapplications:read
POST /applications/{id}/statusMove an application on. The customer is told, exactly as when you do it in the portalapplications:write

Changing status

curl -X POST "https://api.example.com/api/v1/applications/APPLICATION_ID/status" \ -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \ -d '{ "status": "offer_sent", "notes": "Offer valid 7 days", "offer": { "approvedAmount": 3000000, "approvedRate": 2.8, "approvedTenureMonths": 12 } }'

Statuses: submitted, under_review, offer_sent, approved, rejected, disbursed, cancelled. Applications move forward only. A move that is not allowed returns a clear error.

An application

{ "id": "6ad7204111ce1767ae9291a0", "status": "under_review", "loanAmount": 3500000, "currency": "UGX", "tenureMonths": 12, "interestRate": 2.8, "monthlyPayment": 336000, "asset": { "name": "Motorcycle", "category": "motorcycle" }, "customer": { "firstName": "Amina", "lastName": "N.", "phoneNumber": "+256771234567", "kycScore": 78, "assessment": "passed", "creditRating": "fair" }, "createdAt": "2026-10-20T09:12:00.000Z", "updatedAt": "2026-10-20T10:01:00.000Z" }

National ID numbers and photos are never sent through the API.

Webhooks

Get told, don't poll

Add an https address in your portal and we post to it when something happens. Each delivery is JSON with an event id, a name and the data.

{ "id": "evt_9f2c1b7a4d3e8a10", "event": "application.created", "createdAt": "2026-10-20T09:12:00.000Z", "data": { ...an application, as above... } }
EventSent when
application.createdA customer applies to you
application.updatedAn application changes status
usage.near_limitYou reach 80% of your monthly allowance
usage.limit_reachedYou use your whole monthly allowance
subscription.activatedA plan payment is confirmed
subscription.expiredA paid plan ends

Check every delivery is really from us

Each request carries an X-MaliYangu-Signature header such as t=1760951520,v1=ab12.... Compute an HMAC-SHA256 of t + "." + rawBody with your endpoint's signing secret and compare. Reject anything older than five minutes.

import crypto from 'node:crypto'; // rawBody must be the exact bytes received, before any JSON parsing. export function isFromMaliYangu(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // too old const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex'); const given = Buffer.from(parts.v1 || '', 'hex'); const wanted = Buffer.from(expected, 'hex'); return given.length === wanted.length && crypto.timingSafeEqual(given, wanted); }
  • Answer with any 2xx within 8 seconds. Do slow work after replying.
  • If you do not, we try again twice, a few seconds apart, then record the delivery as failed. After 10 failures in a row the endpoint is paused and you are shown why.
  • The same event can arrive more than once. Use id to ignore repeats.
  • Deliveries are best effort. Call GET /applications now and then to catch up on anything missed.
  • Webhook addresses must be public https addresses.
Errors and limits

When something goes wrong

{ "error": { "code": "invalid_api_key", "message": "That API key is not valid, or it has been revoked." } }
StatuscodeMeaning
401invalid_api_keyMissing, wrong or revoked key
403missing_scopeThe key lacks the permission for this call
403plan_does_not_include_apiYour plan no longer includes the API
400invalid_status, invalid_transition, offer_requiredThe request could not be applied
404not_foundNo such application on your account
429rate_limitedToo many requests. Premium allows 60 a minute per key, Enterprise 300

Ready to connect?

The API is included on Premium and Enterprise. Need higher limits or a custom integration? Talk to us.