The subscribable events
Seven events are available to customer webhooks today. Subscribing to any other event name is refused at registration rather than accepted and then silently never delivered.
| Event | Means | Entity | Version |
|---|---|---|---|
cash.payment.received | Money arrived and a payment record exists for it. | payment | 1 |
cash.allocation.reversed | An applied allocation was undone, releasing the obligation it settled. | allocation | 1 |
billing.invoice.issued | An invoice was issued to its payer and is now collectable. | invoice | 1 |
billing.invoice.voided | An issued invoice was voided — an accounting act, not a deletion. | invoice | 1 |
billing.penalty.assessed | A late-payment penalty was charged against a lease. | penalty | 1 |
tenancy.tps_allocation.received | The housing partner allocated a unit to a purchaser, and a pending tenancy now exists for it. | tps_allocation | 1 |
tenancy.tps_contract.activated | A tenant-purchase went live: its financing contract exists and its instalments are scheduled. | tps_contract | 1 |
Internal event families — ledger.*, sync.*, platform.* and the audit
events — are not subscribable and cannot be opened up on request. Exposing an
event is a one-way door: once a customer can subscribe to it, its shape becomes an
external contract. That decision is taken deliberately per event, not as a side
effect of adding one.
The delivery envelope
Every delivery has the same outer shape, whatever the event.
eventstringrequiredThe event name, e.g. cash.payment.received. This is the signed copy — trust
it rather than the X-Webhook-Event header.
timestampstring (ISO 8601)requiredWhen this attempt was built. It changes between retries of the same event, so do not use it as an identity or as the time the thing happened.
executionIdstringrequiredIdentifies the event. It is stable across every retry of the same event, so
it is the key to deduplicate on. For a real delivery it is the platform's event
id; for a test fire it is a test-… value.
dataobjectrequiredThe event's context. Its fields are described below.
webhookobjectrequiredWhich of your registered endpoints this was sent to.
webhook.uuidstringThe endpoint's uuid — the same one you use on the management routes.
webhook.namestringThe name you gave it.
webhook.idnumberInternal numeric id. Key on uuid instead.
Inside data
data.event_idstringThe event id — the same value as the envelope's executionId.
data.event_typestringThe event name, repeated inside the context.
data.org_idnumber | nullThe organisation the event belongs to.
data.branch_idnumber | nullThe branch, when the event has one.
data.entity_typestringWhat kind of thing the event is about — payment, invoice, and so on.
data.entity_idnumberThe id of that thing. This is the reference you read back through the partner API when you need the full record.
data.attemptnumberWhich delivery attempt this is, starting at 1. A value above 1 means an earlier
attempt did not succeed — possibly because your 2xx never got back to the
platform, so treat the event as potentially already seen.
data.dataobjectThe event's own declared payload — the per-event fields in the sections below.
Common payload fields
The four money-bearing events share a base shape:
orgIdnumberrequiredThe organisation.
branchIdnumber | nullrequiredThe branch, or null for an organisation-wide fact.
amountstringrequiredA decimal as a string, to avoid floating-point rounding. Parse it with a decimal type, not a float.
currencystring (3 letters)requiredISO 4217 currency code, e.g. KES.
The two tenant-purchase events are not money-bearing: they carry orgId and
branchId, and no amount or currency.
cash.payment.received
Money arrived and a payment record exists for it.
paymentIdnumberrequiredpayMethodstringrequiredmpesa, bank_transfer.referencestring | nullrequired{
"event": "cash.payment.received",
"timestamp": "2026-09-30T08:41:02.119Z",
"executionId": "0b9d4c2e-7a51-4e6c-9f33-2c8d1e5b4a07",
"data": {
"event_id": "0b9d4c2e-7a51-4e6c-9f33-2c8d1e5b4a07",
"event_type": "cash.payment.received",
"org_id": 1042,
"branch_id": 2187,
"entity_type": "payment",
"entity_id": 884512,
"attempt": 1,
"data": {
"orgId": 1042,
"branchId": 2187,
"amount": "45000.00",
"currency": "KES",
"paymentId": 884512,
"payMethod": "mpesa",
"reference": "SJ41K9PQ2M"
}
},
"webhook": {
"id": 7,
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"name": "Ops notifier"
}
}
cash.allocation.reversed
An applied allocation was undone, releasing the obligation it settled. Reversing a posted financial effect always names a person and a reason internally; the webhook tells you the reversal happened.
allocationIdnumberrequiredobligationIdnumberrequired{
"event": "cash.allocation.reversed",
"timestamp": "2026-09-30T11:07:55.004Z",
"executionId": "5c7f1a93-02be-41d7-8a60-9d3e71c4b5f8",
"data": {
"event_id": "5c7f1a93-02be-41d7-8a60-9d3e71c4b5f8",
"event_type": "cash.allocation.reversed",
"org_id": 1042,
"branch_id": 2187,
"entity_type": "allocation",
"entity_id": 311904,
"attempt": 1,
"data": {
"orgId": 1042,
"branchId": 2187,
"amount": "12500.00",
"currency": "KES",
"allocationId": 311904,
"obligationId": 772310
}
},
"webhook": {
"id": 7,
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"name": "Ops notifier"
}
}
billing.invoice.issued
An invoice was issued to its payer and is now collectable. This is the issue, not the draft — an invoice still in draft has not fired this event.
invoiceIdnumberrequiredinvoiceNumberstringrequireddueDatestringrequired{
"event": "billing.invoice.issued",
"timestamp": "2026-09-30T06:00:12.845Z",
"executionId": "a81e4f60-5d2c-4b19-91fa-3e7c82d05b64",
"data": {
"event_id": "a81e4f60-5d2c-4b19-91fa-3e7c82d05b64",
"event_type": "billing.invoice.issued",
"org_id": 1042,
"branch_id": 2187,
"entity_type": "invoice",
"entity_id": 540118,
"attempt": 1,
"data": {
"orgId": 1042,
"branchId": 2187,
"amount": "45000.00",
"currency": "KES",
"invoiceId": 540118,
"invoiceNumber": "INV-2026-09-00412",
"dueDate": "2026-10-05"
}
},
"webhook": {
"id": 7,
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"name": "Ops notifier"
}
}
billing.invoice.voided
An issued invoice was voided. This is an accounting act, not a deletion — the invoice still exists and is still readable through the partner API; it is no longer collectable.
invoiceIdnumberrequired{
"event": "billing.invoice.voided",
"timestamp": "2026-09-30T14:22:31.560Z",
"executionId": "c40b7d18-9e63-4a52-bb77-1f0a6c93d2e5",
"data": {
"event_id": "c40b7d18-9e63-4a52-bb77-1f0a6c93d2e5",
"event_type": "billing.invoice.voided",
"org_id": 1042,
"branch_id": 2187,
"entity_type": "invoice",
"entity_id": 540118,
"attempt": 1,
"data": {
"orgId": 1042,
"branchId": 2187,
"amount": "45000.00",
"currency": "KES",
"invoiceId": 540118
}
},
"webhook": {
"id": 7,
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"name": "Ops notifier"
}
}
billing.penalty.assessed
A late-payment penalty was charged against a lease.
leaseIdnumberrequired{
"event": "billing.penalty.assessed",
"timestamp": "2026-09-30T02:15:08.277Z",
"executionId": "e2f9a305-6c84-4d7b-82e1-5b40c7961df3",
"data": {
"event_id": "e2f9a305-6c84-4d7b-82e1-5b40c7961df3",
"event_type": "billing.penalty.assessed",
"org_id": 1042,
"branch_id": 2187,
"entity_type": "penalty",
"entity_id": 90446,
"attempt": 1,
"data": {
"orgId": 1042,
"branchId": 2187,
"amount": "2250.00",
"currency": "KES",
"leaseId": 66120
}
},
"webhook": {
"id": 7,
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"name": "Ops notifier"
}
}
Tenant purchase (TPS)
Two events follow a tenant-purchase from the housing partner's allocation to the
purchase going live. Both carry the same leaseId — join on it to follow one
purchase end to end. An allocation is not always followed by an activation: a
pending tenancy can wait, or be withdrawn, and only the allocation is announced
until the purchase goes live.
Neither event carries the purchaser's name or identity number. Read the purchaser
through the API, using the identifiers in data.
tenancy.tps_allocation.received
The housing partner (Boma Yangu) allocated a unit to a purchaser, and a pending
tenancy now exists for it. The envelope's entity_type is lease and its
entity_id is that tenancy — the same value as leaseId.
leaseIdnumberrequiredunitIdnumberrequiredunitNumberstringrequiredaccountNumberstring | nullrequiredThe partner's tenant-purchase account number, or null when the partner sent
none. The tenancy is created either way.
{
"event": "tenancy.tps_allocation.received",
"timestamp": "2026-10-01T09:12:44.018Z",
"executionId": "6b0d4f2e-91a7-4c3b-8e55-0f7a2c19d834",
"data": {
"event_id": "6b0d4f2e-91a7-4c3b-8e55-0f7a2c19d834",
"event_type": "tenancy.tps_allocation.received",
"org_id": 1042,
"branch_id": 2187,
"entity_type": "lease",
"entity_id": 310552,
"attempt": 1,
"data": {
"orgId": 1042,
"branchId": 2187,
"leaseId": 310552,
"unitId": 48810,
"unitNumber": "AH005",
"accountNumber": "TPS-0042117"
}
},
"webhook": {
"id": 7,
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"name": "Ops notifier"
}
}
tenancy.tps_contract.activated
A tenant-purchase went live: its financing contract exists and its instalments
are scheduled. Fires once per contract. The envelope's entity_type is
tps_contract and its entity_id is the contract.
contractIdnumberrequiredcontractNumberstringrequiredleaseIdnumberrequiredleaseId its allocation carried.caseIdnumberrequired{
"event": "tenancy.tps_contract.activated",
"timestamp": "2026-10-14T11:40:02.551Z",
"executionId": "c4a81e60-2f3d-4b9a-9d17-7e2b05f6a3c1",
"data": {
"event_id": "c4a81e60-2f3d-4b9a-9d17-7e2b05f6a3c1",
"event_type": "tenancy.tps_contract.activated",
"org_id": 1042,
"branch_id": 2187,
"entity_type": "tps_contract",
"entity_id": 3318,
"attempt": 1,
"data": {
"orgId": 1042,
"branchId": 2187,
"contractId": 3318,
"contractNumber": "TPS-0042117",
"leaseId": 310552,
"caseId": 2291
}
},
"webhook": {
"id": 7,
"uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
"name": "Ops notifier"
}
}
Versioning
Each event carries a version in the catalogue, 1 for all seven today. A
breaking change to an event's payload arrives as a new version rather than a
silent reshape of the existing one. Write your handler so that unknown extra
fields are ignored, and key on event rather than on payload shape.
Subscription limits
eventTypesstring[]requiredAt least 1 and at most 50 event names per endpoint. Duplicates are collapsed. Every name must be on the subscribable list above.
To change which events an endpoint receives, register a new endpoint with the set
you want — PATCH /v3/webhooks/{uuid} changes the name, description, URL and
enabled flag, but not the subscribed event list.