ubunyaHelp
← Help centre

Developers

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.

Getting started

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.

Authentication

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=…

Pages, errors and limits

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.

Webhooks

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.joinedSomeone joined the community
application.submittedSomeone applied to an intake
application.decidedAn application was accepted, waitlisted or turned down
enrollment.createdSomeone joined an intake
enrollment.completedSomeone finished an intake
enrollment.withdrawnSomeone left an intake
lesson.completedSomeone finished a lesson
attendance.markedAttendance was marked for a session
session.scheduledA session was added to the calendar
payment.recordedA fee payment was recorded
payment.voidedA fee payment was voided
certificate.issuedA 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));
}

Zapier and Make

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.

MCP server (Claude and other AI apps)

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_organizationreadsThe community this connection is for, and who it acts as.
list_membersreadsPeople in the community, oldest first.
get_memberreadsOne person and the intakes they're in.
list_programmesreadsThe community's programmes.
list_intakesreadsIntakes (cohorts), optionally for one programme.
list_enrollmentsreadsWho is in an intake.
add_to_intakechangesAdd someone to an intake by email. Members are enrolled straight away; anyone else gets an email invitation.
set_enrollment_statuschangesMark an enrolment completed, withdrawn or active again.
list_applicationsreadsApplications to intakes.
decide_applicationchangesAccept (enrols them), waitlist or turn down an application. The applicant is emailed.
list_sessionsreadsSessions on the calendar.
get_attendancereadsAttendance marks for one session.
list_paymentsreadsFee payments recorded.
list_balancesreadsWhat each learner owes (fee, paid, balance and what's due now).
record_paymentchangesRecord a fee payment a learner made (sends them a receipt).
list_squadsreadsSquad standings for an intake: rank, score (including team bonuses), members and captain.
split_into_squadschangesSplit 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_squadschangesSend 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_intakechangesAdd 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_certificatesreadsCertificates issued, with links anyone can use to check them.

Command line

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.

API reference

Generated from the live OpenAPI schema: openapi.json. Import it into Postman, Insomnia or a code generator.

Organisation

get/organization

The organisation this credential belongs to

curl https://ubunya.com/api/v1/organization \
  -H "Authorization: Bearer $UBUNYA_KEY"

Members

get/members

People in the community

afterinteger · query
limitinteger · query
joined_sincestring (date-time) · query
curl https://ubunya.com/api/v1/members \
  -H "Authorization: Bearer $UBUNYA_KEY"
get/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"

Programmes and intakes

get/programmes

Programmes

curl https://ubunya.com/api/v1/programmes \
  -H "Authorization: Bearer $UBUNYA_KEY"
get/intakes

Intakes, optionally for one programme

programme_idinteger · query
curl https://ubunya.com/api/v1/intakes \
  -H "Authorization: Bearer $UBUNYA_KEY"
get/intakes/{intake_id}/enrollments

Who is in an intake

intake_id*integer · path
afterinteger · query
limitinteger · query
statusstring · query
curl https://ubunya.com/api/v1/intakes/1/enrollments \
  -H "Authorization: Bearer $UBUNYA_KEY"
post/intakes/{intake_id}/enrollments

Add someone to an intake (enrols members; invites anyone else by email)

intake_id*integer · path
JSON body
email*string
slot_idinteger
curl -X POST https://ubunya.com/api/v1/intakes/1/enrollments \
  -H "Authorization: Bearer $UBUNYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"…"}'
patch/enrollments/{enrollment_id}

Mark an enrolment completed, withdrawn or active

enrollment_id*integer · path
JSON body
status*string
curl -X PATCH https://ubunya.com/api/v1/enrollments/1 \
  -H "Authorization: Bearer $UBUNYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"…"}'
post/intakes/{intake_id}/enrollments/import

Add many people to an intake by email (review first)

intake_id*integer · path
JSON body
rows*ImportRow[]
confirm_tokenstring
curl -X POST https://ubunya.com/api/v1/intakes/1/enrollments/import \
  -H "Authorization: Bearer $UBUNYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rows":"…"}'

Applications

get/applications

Applications

afterinteger · query
limitinteger · query
statusstring · query
intake_idinteger · query
curl https://ubunya.com/api/v1/applications \
  -H "Authorization: Bearer $UBUNYA_KEY"
post/applications/{application_id}/decision

Accept, waitlist or turn down an application

application_id*integer · path
JSON body
decision*stringaccept, waitlist or reject
messagestringSent 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":"…"}'

Sessions and attendance

get/sessions

Sessions on the calendar

afterinteger · query
limitinteger · query
starts_afterstring (date-time) · query
starts_beforestring (date-time) · query
intake_idinteger · query
curl https://ubunya.com/api/v1/sessions \
  -H "Authorization: Bearer $UBUNYA_KEY"
get/sessions/{session_id}/attendance

Attendance marks for a session

session_id*integer · path
curl https://ubunya.com/api/v1/sessions/1/attendance \
  -H "Authorization: Bearer $UBUNYA_KEY"

Fees and payments

get/payments

Fee payments

afterinteger · query
limitinteger · query
paid_sincestring (date) · query
curl https://ubunya.com/api/v1/payments \
  -H "Authorization: Bearer $UBUNYA_KEY"
post/payments

Record a payment (for example from your M-Pesa till)

JSON body
enrollment_id*integer
amount*stringe.g. "15000" or "15000.50"
method*stringmpesa, bank, cash, cheque, card or other
referencestring
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"}'
get/payments/balances

What each learner owes

afterinteger · query
limitinteger · query
owingboolean · query
curl https://ubunya.com/api/v1/payments/balances \
  -H "Authorization: Bearer $UBUNYA_KEY"

Certificates

get/certificates

Certificates issued

afterinteger · query
limitinteger · query
curl https://ubunya.com/api/v1/certificates \
  -H "Authorization: Bearer $UBUNYA_KEY"

Webhook subscriptions

get/hooks/events

Events you can subscribe to

curl https://ubunya.com/api/v1/hooks/events \
  -H "Authorization: Bearer $UBUNYA_KEY"
post/hooks

Subscribe 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":"…"}'
delete/hooks/{hook_id}

Unsubscribe

hook_id*integer · path
curl -X DELETE https://ubunya.com/api/v1/hooks/1 \
  -H "Authorization: Bearer $UBUNYA_KEY"
get/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"

Squads

get/intakes/{intake_id}/squads

Squad standings for an intake

intake_id*integer · path
curl https://ubunya.com/api/v1/intakes/1/squads \
  -H "Authorization: Bearer $UBUNYA_KEY"
post/intakes/{intake_id}/squads/split

Split an intake into balanced squads (review first)

intake_id*integer · path
JSON body
count*integer
confirm_tokenstring
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}'
post/intakes/{intake_id}/squads/message

Message some or all squads, or only captains (review first)

intake_id*integer · path
JSON body
body*string
squad_idsinteger[]
captains_onlyboolean
confirm_tokenstring
curl -X POST https://ubunya.com/api/v1/intakes/1/squads/message \
  -H "Authorization: Bearer $UBUNYA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"…"}'