Events reference

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

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.

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_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 four 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 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.

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)

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.

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_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 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[]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.