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

http
POST /hooks/biller HTTP/1.1
Content-Type: application/json
User-Agent: LakiPay-Biller

Signature verification

Idempotency matters — the same payment.id and event can arrive again.

Payload

Example delivery — bill:paid

json
{
  "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

EventWhenWhat you should do
bill:createdNew invoice, INITIATED. Advance near period start; arrears at period end.Store against unique_account_id. Show as due. Do not mark paid.
bill:processedCheckout started. Status PENDING.Keep a single open attempt for that payment id.
bill:regeneratedNew payment replaced a failed one.Point the invoice at the new payment.id.
bill:paidStatus COMPLETE — wallet, card, or approved receipt.Mark paid and keep the package active. Finance posts here.
bill:failedAttempt failed.Leave unpaid. Wait for bill:regenerated or new checkout.
bill:cancelledAttempt cancelled.Leave unpaid unless you also cancel the debt.
bill:overdueDue date passed. Status OVERDUE.Run dunning. Use penalty_amount — do not add a second fee.

Products and subscriptions

EventWhenWhat you should do
product:approvedAdmin activated the product.You may offer it to new customers.
product:declinedAdmin deactivated the product.Stop selling it. Cancel subscriptions if the service is withdrawn.
subscription:createdSubscribe succeeded.Open the account. Store product_user.id and unique_account_id.
subscription:cancelledYou cancelled the contract.Stop expecting new bills. Keep history.
subscription:pausedFixed term reached the day after ends_at.Stop expecting new bills. Account still exists.
subscription:resumedResubscribe succeeded.Bill again from the new subscribed_at.

Manual payments

EventWhenWhat you should do
manual_payment:submittedA receipt is waiting.Queue for bank-statement review. Do not mark the bill paid.
manual_payment:approvedApprove or confirm succeeded. Bill is paid.Treat like bill:paid if that invoice is not posted yet.
manual_payment:rejectedReviewer rejected the receipt.Keep the bill unpaid and ask for another receipt.

Retries and handling

  1. 1

    Read the keys

    event, payment.id when present, and product_user.unique_account_id.

  2. 2

    Store under an idempotency key

    Event + payment id, or event + product user id for subscription events.

  3. 3

    Deduplicate

    If that key exists, return HTTP 200 and stop.

  4. 4

    Acknowledge fast

    Return 200 before slow work.

  5. 5

    Apply business logic

    Use the event tables above.

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