Skip to main content

Handling Kuda Business API Webhooks

Set up webhook notifications for transactions and bills, receive events, and process them safely.

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

  1. Sign in to the Kuda Business dashboard.

  2. Go to Business API, then open the Settings tab.

  3. Select Webhooks.

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

  5. Enter your public notification URL. Private and local addresses are not accepted, including addresses that start with 10., 127., 192.168. or 169.254.. If your URL is rejected, you will see "Input a valid url".

  6. Enter a webhook username and password. The password can be up to 25 characters.

  7. 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 OK within 10 seconds

  • process 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 plaintext

  • password: your configured webhook password, Base64-encoded (only the password is encoded, not a username:password pair)

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

eventType

Event type, such as Transaction.Notification.

amount

Amount in kobo.

transactionReference

Kuda transaction reference.

accountNumber

Account affected by the transaction.

transactionType

Credit, Debit, or Reversal.

instrumentNumber

Internal transaction instrument/reference.

clientRequestRef

Optional client reference.

transactionScope

Direction or context, such as inward or outward.

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:

  • eventType

  • transactionReference

  • instrumentNumber

  • BillRequestRef

  • BillResponseReference

  • clientRequestRef

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

TRANSACTION_STATUS_QUERY

Bulk payment

BULK_PAYMENT_TSQ

Bill payment

BILL_TSQ

Dynamic account collections

DYNAMIC_COLLECTION_ACCOUNT_TSQ

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 instrumentNumber as a bank account number.

  • Depending only on webhooks without a TSQ fallback.

  • Saving only one webhook type when you need both Transactions and Bills.

Did this answer your question?