Webhooks let your app receive updates about transactions and bill payments without polling continuously.
Use webhooks together with transaction status queries. A webhook is a notification. Your app should still be able to confirm important outcomes with the relevant TSQ service.
Configure your webhook URL
Sign in to the Kuda Business dashboard.
Go to Business API, then open the Settings tab.
Select Webhooks.
Choose the type of webhook: Transactions or Bills. The page opens on Bills. Each type has its own URL, so set up each one you need.
Enter your public notification URL. Private and local addresses are not accepted, including addresses that start with
10.,127.,192.168.or169.254.. If your URL is rejected, you will see "Input a valid url".Enter a webhook username and password. The password can be up to 25 characters.
Select Save. Save stays off until the URL, username and password are all filled in.
When the save works, you will see "You've saved your webhook details."
Notes:
The URL you saved for each type is shown when you return to the page. The username and password are not shown again. Enter them again each time you save.
Do not use your Kuda Business login password as the webhook password.
Webhook handler rules
Your webhook endpoint should:
accept JSON requests
authenticate the request using your configured webhook credentials (see below)
store the payload immediately
respond with HTTP
200 OKwithin 10 secondsprocess business logic asynchronously where possible
be safe to receive duplicate notifications
If your server does not respond successfully, Kuda may queue and retry delivery.
How Kuda sends your credentials
Kuda does not use HTTP Basic Auth. There is no Authorization header on the callback. Instead, each request carries two custom headers:
username: your configured webhook username, in plaintextpassword: your configured webhook password, Base64-encoded (only the password is encoded, not ausername:passwordpair)
To validate a request, compare the username header directly, and Base64-decode the password header before comparing. For example, with configured credentials my-webhook-user / my-webhook-pass, Kuda sends:
username: my-webhook-user password: bXktd2ViaG9vay1wYXNz (base64 of "my-webhook-pass")
If your framework's Basic Auth middleware guards the webhook route, Kuda's callbacks will be rejected. Validate these custom headers yourself instead.
Live payload differences
Verified against live production callbacks: the session field arrives as sessionId (not sessionID as some documentation shows), and transaction notifications include extra undocumented fields such as originalRecipientAccount, originalSenderAccount, senderAccountName, originalAmount, and originalSenderName. Parse payloads leniently and do not reject unknown fields.
Example webhook handler
import express from "express";const router = express.Router();function validateKudaCredentials(req, res, next) {
const username = req.headers["username"];
const encodedPassword = req.headers["password"] ?? "";
const password = Buffer.from(String(encodedPassword), "base64").toString("utf8"); if (
username !== process.env.KUDA_WEBHOOK_USERNAME ||
password !== process.env.KUDA_WEBHOOK_PASSWORD
) {
return res.sendStatus(401);
} next();
}router.post("/webhooks/kuda", validateKudaCredentials, async (req, res) => {
const payload = req.body; await saveWebhookPayload({
eventType: payload.eventType,
payload,
receivedAt: new Date().toISOString()
}); res.sendStatus(200);
queueWebhookProcessing(payload);
});export default router;
Transaction notification
Transaction notifications can be sent for incoming and outgoing transactions.
Example:
{
"eventType": "Transaction.Notification",
"amount": 36000,
"transactionDate": "2023-12-08T00:00:00",
"transactionReference": "231208609730",
"accountName": "Sample Business",
"accountNumber": "2050000000",
"narrations": "Incoming transfer",
"transactionType": "Credit",
"senderName": "Sample Sender",
"senderAccountNumber": "3000000000",
"recipientName": "Sample Business",
"instrumentNumber": "ITR-123",
"sessionID": null,
"clientRequestRef": "client-ref-001",
"transactionScope": "inward"
}
Important fields:
Field | Meaning |
| Event type, such as |
| Amount in kobo. |
| Kuda transaction reference. |
| Account affected by the transaction. |
|
|
| Internal transaction instrument/reference. |
| Optional client reference. |
| Direction or context, such as |
Bill payment notification
Bill notifications can include bill request and response references plus PIN or token details.
{
"eventType": "Bill.Transaction",
"BillRequestRef": "BILL20260603001",
"BillResponseReference": "mtn12345",
"Pin": {
"Number": "123456789012",
"Serial": "SERIAL123456",
"Instructions": "Use this token on the biller platform."
},
"BillerName": "MTN NG VTU",
"KudaAccountNumber": "2000000000",
"TransactionAmount": "10000",
"CustomerIdentifier": "07030000000",
"BillType": "airtime"
}
Bill payment webhooks can depend on external biller or aggregator services. If a bill webhook is delayed, use BILL_TSQ to confirm the final status.
Idempotency
Your webhook processor should handle duplicates safely. Use stable identifiers such as:
eventTypetransactionReferenceinstrumentNumberBillRequestRefBillResponseReferenceclientRequestRef
Store processed event identifiers so duplicate deliveries do not double-credit orders or trigger duplicate fulfilment.
Webhooks and TSQ
Use the matching status-query service when a webhook is missing, delayed, or unclear:
Flow | Status-query service |
Single transfer |
|
Bulk payment |
|
Bill payment |
|
Dynamic account collections |
|
Common mistakes
Doing heavy processing before sending
200 OK.Not storing the raw webhook payload.
Assuming webhook delivery will be instant.
Not handling duplicate notifications.
Treating
instrumentNumberas a bank account number.Depending only on webhooks without a TSQ fallback.
Saving only one webhook type when you need both Transactions and Bills.
