Webhooks
Events & payload
What LokiPay sends to your app, and when.
LokiPay sends an HTTP POST to your webhook URL (Settings → Webhook) whenever a payment reaches an outcome. Webhooks are written in the same database transaction as the status change (outbox pattern), so an event is never lost or sent for a change that didn’t happen.
Events#
| type | When | Do this |
|---|---|---|
payment.verified | A MoMo SMS verified the payment, or an admin approved it. | Fulfil the order. Use receivedAmountPesewas. |
payment.review | The payment needs an admin decision (see reviewReason). | Show the customer “we’re checking your payment”. |
payment.expired | The reference window ended without payment. | Optional: remind the customer. It can still be verified by a late SMS or claim. |
payment.rejected | An admin rejected the payment in review. | Tell the customer; do not fulfil. |
Request#
http
POST /api/payments/lokipay/webhook HTTP/1.1
Content-Type: application/json
User-Agent: LokiPay-Webhook/1.0
X-LokiPay-Event: payment.verified
X-LokiPay-Event-Id: evt_6f1c0a9e2b8d4f7a9c3e5b1d2f4a6c8e
X-LokiPay-Signature: t=1762077672,v1=5f2b…c91ePayload#
json
{
"id": "evt_6f1c0a9e2b8d4f7a9c3e5b1d2f4a6c8e",
"type": "payment.verified",
"created": "2026-11-02T10:01:12.000Z",
"data": {
"intentId": "cmvymj4a1000621twmsz5y6i5",
"shortRef": "Q9N2LPU7",
"purpose": "payment",
"reference": "ORDER-1001",
"description": "Order #1001",
"customer": { "ref": "cus_42", "name": "Ama" },
"metadata": { "cart": "c_93" },
"paymentLinkId": null,
"payer": null,
"amountPolicy": "AT_LEAST",
"status": "VERIFIED",
"expectedAmountPesewas": 15000,
"receivedAmountPesewas": 15000,
"claimedTransactionId": "92481234567",
"reviewReason": null,
"verifiedAt": "2026-11-02T10:01:12.000Z",
"transaction": {
"transactionId": "92481234567",
"provider": "MTN",
"amountPesewas": 15000,
"reference": "Q9N2LPU7",
"payerName": "AMA SERWAA",
"payerPhone": "0241112233",
"receivedAt": "2026-11-02T10:01:10.000Z"
}
}
}For payment-link payments, purpose is payment_link, paymentLinkId is set and payer holds the name/phone collected on the page.
Responding#
- Verify the signature first — see verify signatures.
- Return any
2xxwithin 10 seconds. Do slow work (emails, stock updates) after responding. - Be idempotent: store
id(ordata.intentId+type) and ignore repeats. LokiPay may deliver an event more than once.
