Webhooks
Callback delivery, payload shape, bill/product/subscription/manual-payment events.
LakiPay POSTs JSON to your callback when a bill, product, subscription, or manual payment changes.
Bill events use the payment callback_url from initiate when set, otherwise the product callback. Product and subscription events use the product callback only. Empty URL → not delivered.
Delivery request
POST /hooks/biller HTTP/1.1
Content-Type: application/json
User-Agent: LakiPay-BillerSignature verification
Idempotency matters — the same payment.id and event can arrive again.
Payload
Example delivery — bill:paid
{
"event": "bill:paid",
"timestamp": "2026-10-22T10:30:00Z",
"status": "COMPLETE",
"payment": {
"id": "8f1c...",
"product_user_id": "2ab4...",
"product_id": "91de...",
"amount": "1499",
"penalty_amount": "0",
"status": "COMPLETE",
"payment_reference": "BILL-10042",
"transaction_id": "txn_...",
"payment_method": "TELEBIRR",
"due_date": "2026-10-22T00:00:00Z",
"billing_period_start": "2026-10-01T00:00:00Z",
"billing_period_end": "2026-11-01T00:00:00Z",
"paid_date": "2026-10-22T10:29:00Z"
},
"product": {
"id": "91de...",
"merchant_id": "44aa...",
"title": "Fiber Internet",
"is_active": true,
"category": "INTERNET"
},
"product_user": {
"id": "2ab4...",
"unique_account_id": "HUB-10042",
"phone_number": "251911000111",
"is_active": true
},
"previous_payment_id": null,
"checkout_token": null
}Amounts are strings. Irrelevant fields are omitted. Match product_user.unique_account_id to your subscriber id. contract:ended is reserved and not dispatched — fixed terms end as subscription:paused.
Bill events
| Event | When | What you should do |
|---|---|---|
bill:created | New invoice, INITIATED. Advance near period start; arrears at period end. | Store against unique_account_id. Show as due. Do not mark paid. |
bill:processed | Checkout started. Status PENDING. | Keep a single open attempt for that payment id. |
bill:regenerated | New payment replaced a failed one. | Point the invoice at the new payment.id. |
bill:paid | Status COMPLETE — wallet, card, or approved receipt. | Mark paid and keep the package active. Finance posts here. |
bill:failed | Attempt failed. | Leave unpaid. Wait for bill:regenerated or new checkout. |
bill:cancelled | Attempt cancelled. | Leave unpaid unless you also cancel the debt. |
bill:overdue | Due date passed. Status OVERDUE. | Run dunning. Use penalty_amount — do not add a second fee. |
Products and subscriptions
| Event | When | What you should do |
|---|---|---|
product:approved | Admin activated the product. | You may offer it to new customers. |
product:declined | Admin deactivated the product. | Stop selling it. Cancel subscriptions if the service is withdrawn. |
subscription:created | Subscribe succeeded. | Open the account. Store product_user.id and unique_account_id. |
subscription:cancelled | You cancelled the contract. | Stop expecting new bills. Keep history. |
subscription:paused | Fixed term reached the day after ends_at. | Stop expecting new bills. Account still exists. |
subscription:resumed | Resubscribe succeeded. | Bill again from the new subscribed_at. |
Manual payments
| Event | When | What you should do |
|---|---|---|
manual_payment:submitted | A receipt is waiting. | Queue for bank-statement review. Do not mark the bill paid. |
manual_payment:approved | Approve or confirm succeeded. Bill is paid. | Treat like bill:paid if that invoice is not posted yet. |
manual_payment:rejected | Reviewer rejected the receipt. | Keep the bill unpaid and ask for another receipt. |
Retries and handling
- 1
Read the keys
event,payment.idwhen present, andproduct_user.unique_account_id. - 2
Store under an idempotency key
Event + payment id, or event + product user id for subscription events.
- 3
Deduplicate
If that key exists, return HTTP 200 and stop.
- 4
Acknowledge fast
Return 200 before slow work.
- 5
Apply business logic
Use the event tables above.
- 6
Reconcile if needed
GET /api/v2/biller/payments/{payment_reference}when a callback looks incomplete.
LakiPay retries on connection/timeout failures only. A completed HTTP response (including your 4xx/5xx) is recorded as sent. Return 2xx for healthy delivery logs.