/organizationThe organisation this credential belongs to
curl https://ubunya.com/api/v1/organization \ -H "Authorization: Bearer $UBUNYA_KEY"
Connect your community to a CRM, an M-Pesa till, a spreadsheet or an AI assistant. Everything here works on one community at a time and with the permissions of the person who connected it.
The API lives at https://ubunya.com/api/v1. Make an API key under Manage → Integrations in your community, then:
curl https://ubunya.com/api/v1/organization \ -H "Authorization: Bearer ubk_…"
You get back your community and the person the key acts as. The same calls power the MCP server and the CLI, so whatever works here works there.
API keys (start with ubk_) are for your own scripts and servers. A key is read only or read and write, and it can never do more than the person who made it: if their role changes, so does the key. Revoke a key at any time on the Integrations page.
OAuth 2.1 is for apps other people use. Discovery is at https://ubunya.com/.well-known/oauth-authorization-server. We support the authorization code flow with PKCE (S256, required), refresh tokens that rotate on every use, and dynamic client registration at /api/oauth/register. Scopes are read and write. On the consent screen the person picks one of the communities where they're staff. Access tokens last an hour; refresh tokens 30 days.
# 1. Send people to https://ubunya.com/api/oauth/authorize?response_type=code&client_id=…&redirect_uri=… &code_challenge=…&code_challenge_method=S256&scope=read%20write&state=… # 2. Swap the code they come back with curl -X POST https://ubunya.com/api/oauth/token \ -d grant_type=authorization_code -d client_id=… -d code=… \ -d redirect_uri=… -d code_verifier=…
Lists return {"data": […], "next": 123}. Pass next back as after for the following page; it's null on the last one. limit goes up to 200. Dates are ISO 8601, amounts are strings in your community's currency.
Errors come as {"detail": "…"} with a plain sentence you can show people: 400 when a rule stops the action (an intake is full, a payment is more than the balance), 401 for a missing or revoked credential, 403 when the scope or the person's role doesn't allow it, 404 when it isn't in your community, and 429 above 600 requests a minute.
Add a URL under Manage → Integrations and we POST each event to it as JSON as it happens, retrying for about a day if your end is down. Every request carries Ubunya-Event, Ubunya-Delivery (unique, so you can ignore repeats) and Ubunya-Signature.
| member.joined | Someone joined the community | |
| application.submitted | Someone applied to an intake | |
| application.decided | An application was accepted, waitlisted or turned down | |
| enrollment.created | Someone joined an intake | |
| enrollment.completed | Someone finished an intake | |
| enrollment.withdrawn | Someone left an intake | |
| lesson.completed | Someone finished a lesson | |
| attendance.marked | Attendance was marked for a session | |
| session.scheduled | A session was added to the calendar | |
| payment.recorded | A fee payment was recorded | |
| payment.voided | A fee payment was voided | |
| certificate.issued | A certificate was issued |
Check the signature before trusting a request: HMAC-SHA256 of <t>.<raw body> with your signing secret must equal v1 in Ubunya-Signature: t=…,v1=…. Reject old t values to stop replays.
# Python
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = f"{parts['t']}.".encode() + body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < tolerance
return fresh and hmac.compare_digest(expected, parts["v1"])// Node.js
import crypto from "node:crypto";
export function verify(secret, header, rawBody, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < tolerance;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}The quickest way: in Zapier or Make, add a Catch hook step, copy its URL, and add it as a webhook in Ubunya with the events you want. For a full integration, use REST hooks with an API key or OAuth: POST /hooks with {"target_url", "event"} to subscribe, DELETE /hooks/{id} to unsubscribe, and GET /hooks/samples/{event} for sample data while someone builds their Zap.
Let an assistant answer questions like “who still owes for Cohort 5?” or “accept the waitlisted applicants”. Add a custom connector with this URL; the app signs you in with Ubunya and asks which community to use:
https://ubunya.com/api/mcp
In Claude: Settings → Connectors → Add custom connector. Tools that change something are marked, so the assistant asks before using them. Clients that can't do OAuth can send an API key as Authorization: Bearer ubk_….
| get_organization | reads | The community this connection is for, and who it acts as. |
| list_members | reads | People in the community, oldest first. |
| get_member | reads | One person and the intakes they're in. |
| list_programmes | reads | The community's programmes. |
| list_intakes | reads | Intakes (cohorts), optionally for one programme. |
| list_enrollments | reads | Who is in an intake. |
| add_to_intake | changes | Add someone to an intake by email. Members are enrolled straight away; anyone else gets an email invitation. |
| set_enrollment_status | changes | Mark an enrolment completed, withdrawn or active again. |
| list_applications | reads | Applications to intakes. |
| decide_application | changes | Accept (enrols them), waitlist or turn down an application. The applicant is emailed. |
| list_sessions | reads | Sessions on the calendar. |
| get_attendance | reads | Attendance marks for one session. |
| list_payments | reads | Fee payments recorded. |
| list_balances | reads | What each learner owes (fee, paid, balance and what's due now). |
| record_payment | changes | Record a fee payment a learner made (sends them a receipt). |
| list_squads | reads | Squad standings for an intake: rank, score (including team bonuses), members and captain. |
| split_into_squads | changes | Split an intake into balanced squads. The first call returns a review of who would go where and a confirm_token; show it to the person, and call again with confirm_token only if they agree. |
| message_squads | changes | Send an announcement to some or all squads in an intake, or only to their captains. The first call returns a review (who gets it and how) and a confirm_token; send again with confirm_token once the person agrees. |
| add_many_to_intake | changes | Add many people to an intake by email (members are enrolled, others invited). The first call returns a review and a confirm_token; call again with confirm_token once the person agrees. |
| list_certificates | reads | Certificates issued, with links anyone can use to check them. |
pipx install ubunya # or: uv tool install ubunya ubunya login # opens your browser; pick the community ubunya members ubunya applications --status submitted ubunya decide 42 accept --message "Welcome!" ubunya balances --owing ubunya pay 812 2500 --method mpesa --ref SGT7H2XYZ ubunya api GET /sessions # any endpoint, JSON out
Add --json to any command. Use ubunya login --key ubk_… on servers without a browser.
Generated from the live OpenAPI schema: openapi.json. Import it into Postman, Insomnia or a code generator.
/organizationThe organisation this credential belongs to
curl https://ubunya.com/api/v1/organization \ -H "Authorization: Bearer $UBUNYA_KEY"
/membersPeople in the community
| after | integer · query | |
| limit | integer · query | |
| joined_since | string (date-time) · query |
curl https://ubunya.com/api/v1/members \ -H "Authorization: Bearer $UBUNYA_KEY"
/members/{user_id}One person, with their intakes
| user_id* | integer · path |
curl https://ubunya.com/api/v1/members/1 \ -H "Authorization: Bearer $UBUNYA_KEY"
/programmesProgrammes
curl https://ubunya.com/api/v1/programmes \ -H "Authorization: Bearer $UBUNYA_KEY"
/intakesIntakes, optionally for one programme
| programme_id | integer · query |
curl https://ubunya.com/api/v1/intakes \ -H "Authorization: Bearer $UBUNYA_KEY"
/intakes/{intake_id}/enrollmentsWho is in an intake
| intake_id* | integer · path | |
| after | integer · query | |
| limit | integer · query | |
| status | string · query |
curl https://ubunya.com/api/v1/intakes/1/enrollments \ -H "Authorization: Bearer $UBUNYA_KEY"
/intakes/{intake_id}/enrollmentsAdd someone to an intake (enrols members; invites anyone else by email)
| intake_id* | integer · path |
| email* | string | |
| slot_id | integer |
curl -X POST https://ubunya.com/api/v1/intakes/1/enrollments \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"…"}'/enrollments/{enrollment_id}Mark an enrolment completed, withdrawn or active
| enrollment_id* | integer · path |
| status* | string |
curl -X PATCH https://ubunya.com/api/v1/enrollments/1 \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"…"}'/intakes/{intake_id}/enrollments/importAdd many people to an intake by email (review first)
| intake_id* | integer · path |
| rows* | ImportRow[] | |
| confirm_token | string |
curl -X POST https://ubunya.com/api/v1/intakes/1/enrollments/import \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"rows":"…"}'/applicationsApplications
| after | integer · query | |
| limit | integer · query | |
| status | string · query | |
| intake_id | integer · query |
curl https://ubunya.com/api/v1/applications \ -H "Authorization: Bearer $UBUNYA_KEY"
/applications/{application_id}/decisionAccept, waitlist or turn down an application
| application_id* | integer · path |
| decision* | string | accept, waitlist or reject |
| message | string | Sent to the applicant. |
curl -X POST https://ubunya.com/api/v1/applications/1/decision \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"decision":"…"}'/sessionsSessions on the calendar
| after | integer · query | |
| limit | integer · query | |
| starts_after | string (date-time) · query | |
| starts_before | string (date-time) · query | |
| intake_id | integer · query |
curl https://ubunya.com/api/v1/sessions \ -H "Authorization: Bearer $UBUNYA_KEY"
/sessions/{session_id}/attendanceAttendance marks for a session
| session_id* | integer · path |
curl https://ubunya.com/api/v1/sessions/1/attendance \ -H "Authorization: Bearer $UBUNYA_KEY"
/paymentsFee payments
| after | integer · query | |
| limit | integer · query | |
| paid_since | string (date) · query |
curl https://ubunya.com/api/v1/payments \ -H "Authorization: Bearer $UBUNYA_KEY"
/paymentsRecord a payment (for example from your M-Pesa till)
JSON body| enrollment_id* | integer | |
| amount* | string | e.g. "15000" or "15000.50" |
| method* | string | mpesa, bank, cash, cheque, card or other |
| reference | string | |
| paid_on* | string (date) |
curl -X POST https://ubunya.com/api/v1/payments \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"enrollment_id":1,"amount":"…","method":"…","paid_on":"2026-11-09"}'/payments/balancesWhat each learner owes
| after | integer · query | |
| limit | integer · query | |
| owing | boolean · query |
curl https://ubunya.com/api/v1/payments/balances \ -H "Authorization: Bearer $UBUNYA_KEY"
/certificatesCertificates issued
| after | integer · query | |
| limit | integer · query |
curl https://ubunya.com/api/v1/certificates \ -H "Authorization: Bearer $UBUNYA_KEY"
/hooks/eventsEvents you can subscribe to
curl https://ubunya.com/api/v1/hooks/events \ -H "Authorization: Bearer $UBUNYA_KEY"
/hooksSubscribe a URL to one event (Zapier REST hooks)
JSON body| target_url* | string | |
| event* | string |
curl -X POST https://ubunya.com/api/v1/hooks \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"target_url":"…","event":"…"}'/hooks/{hook_id}Unsubscribe
| hook_id* | integer · path |
curl -X DELETE https://ubunya.com/api/v1/hooks/1 \ -H "Authorization: Bearer $UBUNYA_KEY"
/hooks/samples/{event}Example payloads for an event (recent real ones if any)
| event* | string · path |
curl https://ubunya.com/api/v1/hooks/samples/1 \ -H "Authorization: Bearer $UBUNYA_KEY"
/intakes/{intake_id}/squadsSquad standings for an intake
| intake_id* | integer · path |
curl https://ubunya.com/api/v1/intakes/1/squads \ -H "Authorization: Bearer $UBUNYA_KEY"
/intakes/{intake_id}/squads/splitSplit an intake into balanced squads (review first)
| intake_id* | integer · path |
| count* | integer | |
| confirm_token | string |
curl -X POST https://ubunya.com/api/v1/intakes/1/squads/split \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"count":1}'/intakes/{intake_id}/squads/messageMessage some or all squads, or only captains (review first)
| intake_id* | integer · path |
| body* | string | |
| squad_ids | integer[] | |
| captains_only | boolean | |
| confirm_token | string |
curl -X POST https://ubunya.com/api/v1/intakes/1/squads/message \
-H "Authorization: Bearer $UBUNYA_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"…"}'