Events reference

The ten events a webhook may subscribe to, and what each delivery looks like.

The subscribable events

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

EventMeansEntityVersion
cash.payment.receivedMoney arrived and a payment record exists for it.payment1
cash.allocation.reversedAn applied allocation was undone, releasing the obligation it settled.allocation1
billing.invoice.issuedAn invoice was issued to its payer and is now collectable.invoice1
billing.invoice.voidedAn issued invoice was voided — an accounting act, not a deletion.invoice1
billing.penalty.assessedA late-payment penalty was charged against a lease.penalty1
tenancy.tps_allocation.receivedThe housing partner allocated a unit to a purchaser, and a pending tenancy now exists for it.tps_allocation1
tenancy.tps_case.submittedA tenant-purchase case was submitted for approval: a decision is now pending on it.tps_case1
tenancy.tps_case.approvedThe approval decision on a tenant-purchase case was approved.tps_case1
tenancy.tps_case.rejectedThe approval decision on a tenant-purchase case was rejected — the case returns to its submitter, it is not closed.tps_case1
tenancy.tps_contract.activatedA tenant-purchase went live: its financing contract exists and its instalments are scheduled.tps_contract1

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.

eventstringrequired

The event name, e.g. cash.payment.received. This is the signed copy — trust it rather than the X-Webhook-Event header.

timestampstring (ISO 8601)required

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

executionIdstringrequired

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

dataobjectrequired

The event's context. Its fields are described below.

webhookobjectrequired

Which of your registered endpoints this was sent to.

webhook.uuidstring

The endpoint's uuid — the same one you use on the management routes.

webhook.namestring

The name you gave it.

webhook.idnumber

Internal numeric id. Key on uuid instead.

Inside data

data.event_idstring

The event id — the same value as the envelope's executionId.

data.event_typestring

The event name, repeated inside the context.

data.org_idnumber | null

The organisation the event belongs to.

data.branch_idnumber | null

The branch, when the event has one.

data.entity_typestring

What kind of thing the event is about — payment, invoice, and so on.

data.entity_idnumber

The id of that thing. This is the reference you read back through the partner API when you need the full record.

data.attemptnumber

Which 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.dataobject

The event's own declared payload — the per-event fields in the sections below.

Common payload fields

The five money-bearing events share a base shape:

orgIdnumberrequired

The organisation.

branchIdnumber | nullrequired

The branch, or null for an organisation-wide fact.

amountstringrequired

A decimal as a string, to avoid floating-point rounding. Parse it with a decimal type, not a float.

currencystring (3 letters)required

ISO 4217 currency code, e.g. KES.

The five 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.

paymentIdnumberrequired
The payment record's id.
payMethodstringrequired
How the money came in, e.g. mpesa, bank_transfer.
referencestring | nullrequired
The payer's or channel's reference, when there is one.
{
  "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.

allocationIdnumberrequired
The allocation that was undone.
obligationIdnumberrequired
The obligation it had settled, now released.
{
  "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.

invoiceIdnumberrequired
The invoice's id.
invoiceNumberstringrequired
The human-facing invoice number.
dueDatestringrequired
When the invoice falls due.
{
  "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
The invoice that was voided.
{
  "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
The lease the penalty was charged against.
{
  "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)

Five events follow a tenant-purchase: the housing partner's allocation, the three decisions taken on its approval case, and the purchase going live. They carry the same leaseId — join on it to follow one purchase end to end.

Treat them as a series of facts rather than as a path to a final state. An allocation is not always followed by an activation: a pending tenancy can wait, or be withdrawn. A case that is rejected is not closed, and can be submitted again.

The three tps_case events are decided in the approvals database and cross into the delivery rail from there, so they arrive a short time after the decision — a minute or two in practice, not instantly. Their leaseId can be null; the allocation's and the activation's never is.

No tenant-purchase 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.

leaseIdnumberrequired
The pending tenancy created for the allocation.
unitIdnumberrequired
The physical unit (flat) that was allocated.
unitNumberstringrequired
The unit's door number, as the partner and the purchaser know it.
accountNumberstring | nullrequired

The 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_case.submitted

A tenant-purchase case was submitted for approval, and a decision is now pending on it. The envelope's entity_type is tps_case and its entity_id is the case — the same value as caseId.

This is the submission for approval, not the applicant first filing the case. A case that is sent back for rework and submitted again raises this a second time, with a later approvalRequestId.

caseIdnumberrequired
The tenant-purchase case.
approvalRequestIdnumberrequired
The approval request the decision will be recorded on.
leaseIdnumber | nullrequired

The tenancy the case is about — the same leaseId its allocation and its activation carry, which is how you join all five events for one purchase. null when the case exists before its allocation has been linked to it; we announce those rather than stay silent about them.

{
  "event": "tenancy.tps_case.submitted",
  "timestamp": "2026-10-03T07:21:10.442Z",
  "executionId": "2d7f91ac-5b4e-4a18-9c62-81e3d0f7b5a9",
  "data": {
    "event_id": "2d7f91ac-5b4e-4a18-9c62-81e3d0f7b5a9",
    "event_type": "tenancy.tps_case.submitted",
    "org_id": 1042,
    "branch_id": 2187,
    "entity_type": "tps_case",
    "entity_id": 2291,
    "attempt": 1,
    "data": {
      "orgId": 1042,
      "branchId": 2187,
      "caseId": 2291,
      "leaseId": 310552,
      "approvalRequestId": 8814
    }
  },
  "webhook": {
    "id": 7,
    "uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
    "name": "Ops notifier"
  }
}

tenancy.tps_case.approved

The approval decision on a tenant-purchase case was approved. The envelope's entity_type is tps_case and its entity_id is the case.

This is the decision, not a stage change. An approved case does not necessarily move anywhere: a case already waiting on activation stays where it is. Do not infer a case's current state from this event — read the case back through the API.

Where several approvers are required, this fires once, on the final one.

caseIdnumberrequired
The tenant-purchase case.
approvalRequestIdnumberrequired
The approval request the decision was recorded on.
leaseIdnumber | nullrequired

The tenancy the case is about, or null when no allocation is linked yet.

{
  "event": "tenancy.tps_case.approved",
  "timestamp": "2026-10-08T14:05:33.907Z",
  "executionId": "b81e4c27-6a9f-4d53-8b10-3f7c2e95d460",
  "data": {
    "event_id": "b81e4c27-6a9f-4d53-8b10-3f7c2e95d460",
    "event_type": "tenancy.tps_case.approved",
    "org_id": 1042,
    "branch_id": 2187,
    "entity_type": "tps_case",
    "entity_id": 2291,
    "attempt": 1,
    "data": {
      "orgId": 1042,
      "branchId": 2187,
      "caseId": 2291,
      "leaseId": 310552,
      "approvalRequestId": 8814
    }
  },
  "webhook": {
    "id": 7,
    "uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
    "name": "Ops notifier"
  }
}

tenancy.tps_case.rejected

The approval decision on a tenant-purchase case was rejected. The envelope's entity_type is tps_case and its entity_id is the case.

A rejected decision does not close the case. It goes back to the person who submitted it, and the case's own status does not move, so the same case may raise tenancy.tps_case.submitted again and reach a different decision. Treat the sequence for one case as a series of facts, not as a path to a final state.

The reason the approver gave is not delivered. It is recorded on the platform side; read it through the API if you need it.

caseIdnumberrequired
The tenant-purchase case.
approvalRequestIdnumberrequired
The approval request the decision was recorded on.
leaseIdnumber | nullrequired

The tenancy the case is about, or null when no allocation is linked yet.

{
  "event": "tenancy.tps_case.rejected",
  "timestamp": "2026-10-08T14:05:33.907Z",
  "executionId": "7e3a0b94-1c58-4f26-9d71-4a8b5c0e2f13",
  "data": {
    "event_id": "7e3a0b94-1c58-4f26-9d71-4a8b5c0e2f13",
    "event_type": "tenancy.tps_case.rejected",
    "org_id": 1042,
    "branch_id": 2187,
    "entity_type": "tps_case",
    "entity_id": 2291,
    "attempt": 1,
    "data": {
      "orgId": 1042,
      "branchId": 2187,
      "caseId": 2291,
      "leaseId": 310552,
      "approvalRequestId": 8814
    }
  },
  "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.

contractIdnumberrequired
The financing contract.
contractNumberstringrequired
The contract's reference number.
leaseIdnumberrequired
The tenancy the contract finances — the same leaseId its allocation carried.
caseIdnumberrequired
The tenant-purchase case that was activated.
{
  "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 ten 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[]required

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