Merchant API
Kyanda API referenceUpdated 3 Sep 2026
Move money, vend airtime, and sell data bundles with one JSON contract. Every request uses the same header and the same HMAC field order.
Get your keys
MerchantID and API key live in the merchant portal. The API key is also the security key.
Sign the string
Concatenate fields in the documented order. No commas, no JSON. HMAC-SHA256 with the API key. Put the hex digest in signature.
POST, then listen
You get a reference immediately. Final state arrives on your callback. Poll transaction status if you need a second look.
| Environment | Base URL |
|---|---|
| Sandbox | http://sandbox.kyanda.io:3030 |
| Live | Find the Live API Endpoint in the merchant portal. Paths stay the same as sandbox. |
Quickstart
- Sign in at business.kyanda.app and copy MerchantID plus API key (security key).
- Build the HMAC string for the endpoint. Order is strict. Do not insert separators.
- POST JSON with
Content-Type: application/jsonandapiKey: {your API key}. - Store
merchant_reference/transactionId. Wait for the callback, or check status.
# Account balance signs only MerchantID
# echo -n "kyanda" | openssl dgst -sha256 -hmac "YOUR_API_KEY"
curl -X POST "http://sandbox.kyanda.io:3030/billing/v1/account-balance" \
-H "Content-Type: application/json" \
-H "apiKey: YOUR_API_KEY" \
-d '{
"MerchantID": "kyanda",
"signature": "HEX_HMAC_OF_MerchantID"
}'const crypto = require("crypto");
function sign(payload, apiKey) {
return crypto.createHmac("sha256", apiKey).update(payload).digest("hex");
}
const merchantId = "kyanda";
const apiKey = process.env.KYANDA_API_KEY;
const signature = sign(merchantId, apiKey);
await fetch("http://sandbox.kyanda.io:3030/billing/v1/account-balance", {
method: "POST",
headers: { "Content-Type": "application/json", apiKey },
body: JSON.stringify({ MerchantID: merchantId, signature }),
});import hashlib, hmac, json, os, urllib.request
api_key = os.environ["KYANDA_API_KEY"]
merchant_id = "kyanda"
signature = hmac.new(api_key.encode(), merchant_id.encode(), hashlib.sha256).hexdigest()
req = urllib.request.Request(
"http://sandbox.kyanda.io:3030/billing/v1/account-balance",
data=json.dumps({"MerchantID": merchant_id, "signature": signature}).encode(),
headers={"Content-Type": "application/json", "apiKey": api_key},
method="POST",
)
print(urllib.request.urlopen(req).read().decode())<?php
$apiKey = getenv("KYANDA_API_KEY");
$merchantId = "kyanda";
$signature = hash_hmac("sha256", $merchantId, $apiKey);
$ch = curl_init("http://sandbox.kyanda.io:3030/billing/v1/account-balance");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "apiKey: {$apiKey}"],
CURLOPT_POSTFIELDS => json_encode(["MerchantID" => $merchantId, "signature" => $signature]),
CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);String apiKey = System.getenv("KYANDA_API_KEY");
String merchantId = "kyanda";
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String hex = java.util.HexFormat.of().formatHex(
mac.doFinal(merchantId.getBytes(StandardCharsets.UTF_8)));Authentication
There is one secret. In the portal it may be labelled API key or security key — same value. Send it as the apiKey header and use it as the HMAC secret.
| Header | Value |
|---|---|
Content-Type | application/json |
apiKey | Your API key / security key. |
How the signature is built
We do not sign the JSON object. We sign a flat string of selected field values, glued in a fixed order. Spaces inside a name stay. Nothing else is inserted.
signature in the string you sign. For data bundles, productCode is in the JSON body and is not in the HMAC. Amounts are whole Kenya Shillings — no cents, no commas.Signature lab
Generate a real HMAC with the same field order the API uses. Optionally fire the request at sandbox.
Generator and sandbox tester
Runs in your browser. Keys are not stored.
POST /billing/v1/account-balance
kyandaAccount
Account balance
Returns wallet and earnings balances.
MerchantID| Field | Description |
|---|---|
| MerchantID | Your merchant identifier. |
| signature | HMAC-SHA256 of MerchantID. |
{
"MerchantID": "kyanda",
"signature": "HMAC(MerchantID)"
}const signature = sign(merchantId, apiKey);signature = hmac.new(api_key.encode(), merchant_id.encode(), hashlib.sha256).hexdigest()$signature = hash_hmac("sha256", $merchantId, $apiKey);String signature = hmacSha256(merchantId, apiKey);{
"Account_Bal": 399930,
"Earnings_Bal": 1586.6
}Transaction status
Look up a request by the reference we returned on create.
MerchantID + transactionRef| Field | Description |
|---|---|
| MerchantID | Your merchant identifier. |
| transactionRef | merchant_reference or transactionId from the create response. |
| signature | HMAC of MerchantID then transactionRef. |
{
"MerchantID": "kyanda",
"transactionRef": "kyanda-API1089772",
"signature": "HMAC(MerchantID + transactionRef)"
}const signature = sign(merchantId + transactionRef, apiKey);signature = hmac.new(api_key.encode(), f"{merchant_id}{transaction_ref}".encode(), hashlib.sha256).hexdigest()$signature = hash_hmac("sha256", $merchantId . $transactionRef, $apiKey);String signature = hmacSha256(merchantId + transactionRef, apiKey);Telco prefix
Resolve a 3-digit prefix such as 722.
MerchantID only. prefix is not signed.{
"MerchantID": "kyanda",
"prefix": "722",
"signature": "HMAC(MerchantID)"
}const signature = sign(merchantId, apiKey);signature = sign(merchant_id)$signature = hash_hmac("sha256", $merchantId, $apiKey);String signature = hmacSha256(merchantId, apiKey);Payments
Send to mobile wallets or banks. Collect with checkout (STK).
Mobile wallet payout
MPESA, Airtel Money, or Equitel. Nested source and destination stay as shown.
amount + destinationPhone + initiatorName + initiatorCountry + channel + MerchantIDExample:
15000715330000John DoeGermanyMPESAkyanda| Field | Description |
|---|---|
| source.initiatorName | Full name, for example John Doe. |
| source.initiatorCountry | Country, for example Germany. |
| destination.destinationPhone | 10 digits starting with 07. |
| destination.amount | Whole shillings. |
| destination.channel | MPESA, AIRTEL, or EQUITEL. |
{
"MerchantID": "kyanda",
"source": { "initiatorName": "John Doe", "initiatorCountry": "Germany" },
"destination": { "destinationPhone": "0715330000", "amount": "1500", "channel": "MPESA" },
"signature": "HMAC(amount+destinationPhone+initiatorName+initiatorCountry+channel+MerchantID)"
}const payload = "1500" + "0715330000" + "John Doe" + "Germany" + "MPESA" + merchantId;payload = "1500" + "0715330000" + "John Doe" + "Germany" + "MPESA" + merchant_id$payload = "1500" . "0715330000" . "John Doe" . "Germany" . "MPESA" . $merchantId;String payload = "1500" + "0715330000" + "John Doe" + "Germany" + "MPESA" + merchantId;Bank payout
Use the Kyanda 3-digit bank code, not a CBK sort code. 101 is Absa.
amount + accountNumber + phoneNumber + bankCode + initiatorName + initiatorCountry + MerchantID{
"MerchantID": "kyanda",
"source": { "initiatorName": "John Doe", "initiatorCountry": "Germany" },
"destination": {
"name": "Harry Kessy",
"accountNumber": "2042581154",
"phoneNumber": "0715330000",
"amount": "1500",
"bankCode": "101"
},
"signature": "HMAC(amount+accountNumber+phoneNumber+bankCode+initiatorName+initiatorCountry+MerchantID)"
}const payload = "1500" + "2042581154" + "0715330000" + "101" + "John Doe" + "Germany" + merchantId;payload = "1500" + "2042581154" + "0715330000" + "101" + "John Doe" + "Germany" + merchant_id$payload = "1500" . "2042581154" . "0715330000" . "101" . "John Doe" . "Germany" . $merchantId;String payload = "1500" + "2042581154" + "0715330000" + "101" + "John Doe" + "Germany" + merchantId;| Bank | Code | Bank | Code |
|---|---|---|---|
| Absa Bank | 101 | Co-operative Bank | 102 |
| Equity Bank (K) | 103 | Family Bank | 104 |
| KCB Bank Kenya | 105 | Standard Chartered (K) | 106 |
| ABC Bank | 107 | I&M Bank (K) | 108 |
| HF Group | 109 | Ecobank (K) | 110 |
| Habib Bank A.G. Zurich | 111 | NCBA Bank (K) | 112 |
| Stanbic Bank | 113 | Sidian Bank | 114 |
| Rafiki Microfinance Bank | 115 | Spire Bank | 116 |
| First Community Bank | 117 | Access Bank (K) | 118 |
| Diamond Trust Bank (K) | 119 | DIB Kenya Bank | 120 |
| Development Bank | 121 | Citibank N.A. | 122 |
| Bank of Baroda (K) | 123 | Bank of Africa (K) | 124 |
| Faulu Micro-Finance Bank | 125 | Credit Bank | 126 |
| Choice Microfinance Bank | 127 | Consolidated Bank | 128 |
| Caritas Microfinance Bank | 129 | Kenya Women Microfinance Bank | 130 |
| Mayfair Bank | 131 | Kingdom Bank | 132 |
| National Bank of Kenya | 133 | Middle East Bank (K) | 134 |
| Paramount Universal Bank | 135 | Prime Bank | 136 |
| Postbank | 137 | SBM Bank (K) | 138 |
| Victoria Commercial Bank | 139 | UBA Kenya Bank | 140 |
| Salaam Microfinance Bank | 141 | M Oriental Bank (K) | 142 |
| Gulf African Bank | 143 | Guaranty Trust Bank (K) | 144 |
| Guardian Bank | 145 | Bank of India (K) | 146 |
Mobile checkout
Prompt the customer. They approve on the handset. You get the IPN when it lands.
amount + phoneNumber + channel + MerchantIDExample:
15000715330000MPESAkyanda| Field | Description |
|---|---|
| phoneNumber | 10 digits starting with 07. |
| amount | Whole shillings to collect. |
| channel | MPESA, AIRTEL, or EQUITEL. |
| metadata | Optional object echoed on the IPN. |
{
"MerchantID": "kyanda",
"phoneNumber": "0715330000",
"amount": "1500",
"channel": "MPESA",
"metadata": { "orderId": "SO-1001" },
"signature": "HMAC(amount+phoneNumber+channel+MerchantID)"
}const signature = sign("1500" + "0715330000" + "MPESA" + merchantId, apiKey);signature = sign("1500" + "0715330000" + "MPESA" + merchant_id)$signature = hash_hmac("sha256", "1500"."0715330000"."MPESA".$merchantId, $apiKey);String signature = hmacSha256("1500" + "0715330000" + "MPESA" + merchantId, apiKey);Airtime
Pinless airtime
Networks: SAFARICOM, AIRTEL, TELKOM, EQUITEL, FAIBA. Data bundles use FAIBA_B on the same path.
amount + phone + telco + initiatorPhone + MerchantIDExample:
1000715330000SAFARICOM0715330000kyandaDo not put
productCode in the HMAC.| Field | Description |
|---|---|
| phone | Recipient, 10 digits starting with 07. |
| amount | Whole shillings. Greater than 2, less than 7000. |
| telco | SAFARICOM, AIRTEL, TELKOM, EQUITEL, FAIBA, or FAIBA_B. |
| initiatorPhone | Initiator number for records. |
| productCode | Required only for FAIBA_B. Not signed. |
{
"MerchantID": "kyanda",
"phone": "0715330000",
"amount": "100",
"telco": "SAFARICOM",
"initiatorPhone": "0715330000",
"signature": "HMAC(amount+phone+telco+initiatorPhone+MerchantID)"
}const signature = sign("100" + "0715330000" + "SAFARICOM" + "0715330000" + merchantId, apiKey);payload = "100" + "0715330000" + "SAFARICOM" + "0715330000" + merchant_id$payload = "100" . "0715330000" . "SAFARICOM" . "0715330000" . $merchantId;String payload = "100" + "0715330000" + "SAFARICOM" + "0715330000" + merchantId;Data Bundles
Same airtime endpoint. Set telco to FAIBA_B and send a catalog productCode. Amount must match the table. API only.
amount + phone + telco + initiatorPhone + MerchantID with telco FAIBA_B. Example: 5000715330000FAIBA_B0715330000kyanda. productCode is in the body only.{
"MerchantID": "kyanda",
"phone": "0715330000",
"amount": "500",
"telco": "FAIBA_B",
"initiatorPhone": "0715330000",
"productCode": "Monthly_15GB",
"signature": "HMAC(5000715330000FAIBA_B0715330000kyanda)"
}const signature = sign("500" + "0715330000" + "FAIBA_B" + "0715330000" + merchantId, apiKey);payload = "500" + "0715330000" + "FAIBA_B" + "0715330000" + merchant_id$payload = "500" . "0715330000" . "FAIBA_B" . "0715330000" . $merchantId;String payload = "500" + "0715330000" + "FAIBA_B" + "0715330000" + merchantId;Send productCode exactly as listed. Auto-renew codes are optional at the same price.
| Bundle | Validity | KES | productCode | Auto-renew |
|---|---|---|---|---|
| Daily 500MB | 1 day | 20 | DAILY_500MB | DAILY_AUTO_500MB |
| Fisi 3 hour | 3 hours | 50 | fisihour3 | — |
| Daily 1.5GB | 1 day | 50 | Daily_1.5GB | — |
| Gumzo Weekly 50 | 7 days | 75 | Gumzo_Weekly_50 | — |
| Fisi 5 hour | 5 hours | 80 | fisihour5 | — |
| 3GB 3 day | 3 days | 100 | 3GB3DAY | — |
| Fisi 6 hour | 6 hours | 120 | fisihour6 | — |
| Weekly 10GB | 7 days | 300 | WEEKLY_DATA_10GB | WEEKLY_DATA_AUTO_10GB |
| Gumzo Monthly 250 | 30 days | 300 | Gumzo_Monthly_250 | — |
| Monthly 15GB | 30 days | 500 | Monthly_15GB | Monthly_15GB_Auto |
| Gumzo Monthly 500 | 30 days | 500 | Gumzo_Monthly_500 | — |
| All in One 5 | 30 days | 500 | All_inOne_5 | — |
| Monthly 40GB | 30 days | 1000 | MONTHLY_DATA_40GB | MONTHLY_DATA_AUTO_40GB |
| All in One 10 | 30 days | 1000 | All_inOne_10 | — |
| Monthly 120GB | 30 days | 2000 | Monthly_120GB | Monthly_120GB_Auto |
| All in One 20 | 30 days | 2000 | All_inOne_20 | — |
| Family Basic Plus 150 mins | 30 days | 2000 | Family_Basic_Plus_150Mins | — |
| Family Plus Plus 300 mins | 30 days | 3500 | Family_Plus_Plus_300Mins | — |
| Family Max Plus 600 mins | 30 days | 6000 | Family_Max_Plus_600Mins | — |
Callbacks
When a request finishes we POST JSON to your HTTPS URL. Register it in the portal or here. After the first API registration, further changes go through support.
MerchantID only. callbackURL is not signed.{
"MerchantID": "kyanda",
"callbackURL": "https://website.com/callback",
"signature": "HMAC(MerchantID)"
}const signature = sign(merchantId, apiKey);signature = sign(merchant_id)$signature = hash_hmac("sha256", $merchantId, $apiKey);String signature = hmacSha256(merchantId, apiKey);Reply to every IPN with HTTP 200 and:
{
"status": "success"
}Errors
Failures return status, status_code, and transactiontxt.
| Code | Meaning |
|---|---|
| 0000 | Processed successfully. |
| 1100 | Accepted, still processing. |
| 1101 | Invalid Merchant ID. |
| 1102 | Authentication failed. |
| 1103 | Forbidden. |
| 1106 | Service unavailable. |
| 1109 | Blank required field. |
| 1201 | Invalid bank code. |
| 3101 | Invalid telco prefix (3 digits). |
| 4000 | Insufficient funds. |
| 8002 / 9001 | Invalid phone format. |
| 8003 | Invalid telco. |
| 8004 / 9003 | Invalid amount format. |
| 8005 / 9004 | Amount limit exceeded. |
| 8006 / 9005 | Duplicate transmission. |
| 9002 | Invalid channel, or data-bundle productCode / amount mismatch. |