Realtime notifications

Lipafy does not send outbound webhooks to integrators today. Instead, use the server-sent events stream at /v1/notifications/stream to subscribe to real-time events for your account.

Payment-provider callbacks are private Lipafy operator infrastructure. Application developers should treat POST /v1/topups/initiate, GET /v1/topups/:id, and the SSE stream as the supported public top-up contract.

What changes after a successful top-up

[payment confirmation arrives]
|-- verify callback
|-- look up topup by reference
|-- if not found -> ignore
|-- if already processed -> ignore
|-- if status='success':
| |-- topup.status = 'completed'
| |-- topup.credits_issued = amount
| |-- wallet_transactions insert (type='topup', status='confirmed')
| |-- wallet.balance += amount, wallet.available += amount
| `-- emit notification 'topup.completed' to /v1/notifications/stream
|-- if status='failed':
| |-- topup.status = 'failed'
| `-- emit notification 'topup.failed'
`-- return success to the payment rail

SSE notification stream

Subscribe with any authenticated credential:

1GET /v1/notifications/stream
2Authorization: Bearer <credential>
3Accept: text/event-stream

The connection stays open; the server pushes lines like:

event: topup.completed
data: {"topup_id":"...","amount":"1000","wallet_balance":"4500"}
event: approval.requested
data: {"approval_id":"...","capability_slug":"...","amount":"5000"}
event: approval.decided
data: {"approval_id":"...","decision":"approve"}
: ping

Events emitted today:

EventWhen
topup.completedM-Pesa confirmation credited the wallet
topup.failedM-Pesa returned failure, cancellation, or timeout
approval.requestedA gateway call triggered escalation; user needs to decide
approval.decidedThe approval was approved or denied
approval.expiredThe approval TTL passed without a decision
capability_request.completedA gateway call succeeded
capability_request.failedA gateway call failed

A : ping comment is sent every 25 seconds to keep proxies from closing the connection. EventSource clients ignore comment lines automatically.

Why SSE instead of outbound webhooks

Outbound webhooksSSE notifications
You expose a public HTTPS endpointLipafy holds the connection
You verify signatures, handle replaysAuth happens at connection open
Lipafy retries on failureReconnect with the standard SSE Last-Event-ID header
Survives client offlineMisses events while disconnected (queue is short)

SSE is simpler for the integrator. The downside - missing events during disconnects - is mitigated for state-bearing events because the underlying resource is always queryable via REST. The SSE stream is the fast path; the REST endpoint is the source of truth.

If you have a real need for outbound webhooks, open an issue so the use case can be evaluated. SSE plus REST polling is the supported path today.