K Kyanda API docs
Postman Sign in

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.

EnvironmentBase URL
Sandboxhttp://sandbox.kyanda.io:3030
LiveFind the Live API Endpoint in the merchant portal. Paths stay the same as sandbox.

Quickstart

  1. Sign in at business.kyanda.app and copy MerchantID plus API key (security key).
  2. Build the HMAC string for the endpoint. Order is strict. Do not insert separators.
  3. POST JSON with Content-Type: application/json and apiKey: {your API key}.
  4. 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.

HeaderValue
Content-Typeapplication/json
apiKeyYour 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.

1. Fields
amount = 100, phone = 0715330000, telco = SAFARICOM, initiatorPhone = 0715330000, MerchantID = kyanda
2. Concat
1000715330000SAFARICOM0715330000kyanda
3. HMAC
HMAC-SHA256(concat, apiKey) → lowercase hex → signature
Never include 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.

Use sandbox credentials only. Do not paste a live API key, live MerchantID, or production endpoint here.

POST /billing/v1/account-balance

Concatenated string: kyanda
Pick an endpoint, add a sandbox key, then generate.

Account

Account balance

Returns wallet and earnings balances.

POST /billing/v1/account-balance
HMAC: MerchantID
FieldDescription
MerchantIDYour merchant identifier.
signatureHMAC-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.

POST /billing/v1/transaction-check
HMAC: MerchantID + transactionRef
FieldDescription
MerchantIDYour merchant identifier.
transactionRefmerchant_reference or transactionId from the create response.
signatureHMAC 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.

POST /billing/v1/prefix-check
HMAC: 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.

POST /billing/v3/mobile-payout/create
HMAC: amount + destinationPhone + initiatorName + initiatorCountry + channel + MerchantID
Example: 15000715330000John DoeGermanyMPESAkyanda
FieldDescription
source.initiatorNameFull name, for example John Doe.
source.initiatorCountryCountry, for example Germany.
destination.destinationPhone10 digits starting with 07.
destination.amountWhole shillings.
destination.channelMPESA, 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.

POST /billing/v3/bank-payout/create
HMAC: 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;
BankCodeBankCode
Absa Bank101Co-operative Bank102
Equity Bank (K)103Family Bank104
KCB Bank Kenya105Standard Chartered (K)106
ABC Bank107I&M Bank (K)108
HF Group109Ecobank (K)110
Habib Bank A.G. Zurich111NCBA Bank (K)112
Stanbic Bank113Sidian Bank114
Rafiki Microfinance Bank115Spire Bank116
First Community Bank117Access Bank (K)118
Diamond Trust Bank (K)119DIB Kenya Bank120
Development Bank121Citibank N.A.122
Bank of Baroda (K)123Bank of Africa (K)124
Faulu Micro-Finance Bank125Credit Bank126
Choice Microfinance Bank127Consolidated Bank128
Caritas Microfinance Bank129Kenya Women Microfinance Bank130
Mayfair Bank131Kingdom Bank132
National Bank of Kenya133Middle East Bank (K)134
Paramount Universal Bank135Prime Bank136
Postbank137SBM Bank (K)138
Victoria Commercial Bank139UBA Kenya Bank140
Salaam Microfinance Bank141M Oriental Bank (K)142
Gulf African Bank143Guaranty Trust Bank (K)144
Guardian Bank145Bank of India (K)146

Mobile checkout

Prompt the customer. They approve on the handset. You get the IPN when it lands.

POST /billing/v1/checkout/create
HMAC: amount + phoneNumber + channel + MerchantID
Example: 15000715330000MPESAkyanda
FieldDescription
phoneNumber10 digits starting with 07.
amountWhole shillings to collect.
channelMPESA, AIRTEL, or EQUITEL.
metadataOptional 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.

POST /billing/v1/airtime/create
HMAC: amount + phone + telco + initiatorPhone + MerchantID
Example: 1000715330000SAFARICOM0715330000kyanda
Do not put productCode in the HMAC.
FieldDescription
phoneRecipient, 10 digits starting with 07.
amountWhole shillings. Greater than 2, less than 7000.
telcoSAFARICOM, AIRTEL, TELKOM, EQUITEL, FAIBA, or FAIBA_B.
initiatorPhoneInitiator number for records.
productCodeRequired 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.

POST /billing/v1/airtime/create
HMAC stays 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.

BundleValidityKESproductCodeAuto-renew
Daily 500MB1 day20DAILY_500MBDAILY_AUTO_500MB
Fisi 3 hour3 hours50fisihour3
Daily 1.5GB1 day50Daily_1.5GB
Gumzo Weekly 507 days75Gumzo_Weekly_50
Fisi 5 hour5 hours80fisihour5
3GB 3 day3 days1003GB3DAY
Fisi 6 hour6 hours120fisihour6
Weekly 10GB7 days300WEEKLY_DATA_10GBWEEKLY_DATA_AUTO_10GB
Gumzo Monthly 25030 days300Gumzo_Monthly_250
Monthly 15GB30 days500Monthly_15GBMonthly_15GB_Auto
Gumzo Monthly 50030 days500Gumzo_Monthly_500
All in One 530 days500All_inOne_5
Monthly 40GB30 days1000MONTHLY_DATA_40GBMONTHLY_DATA_AUTO_40GB
All in One 1030 days1000All_inOne_10
Monthly 120GB30 days2000Monthly_120GBMonthly_120GB_Auto
All in One 2030 days2000All_inOne_20
Family Basic Plus 150 mins30 days2000Family_Basic_Plus_150Mins
Family Plus Plus 300 mins30 days3500Family_Plus_Plus_300Mins
Family Max Plus 600 mins30 days6000Family_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.

POST /billing/v1/callback-url/create
HMAC: 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.

CodeMeaning
0000Processed successfully.
1100Accepted, still processing.
1101Invalid Merchant ID.
1102Authentication failed.
1103Forbidden.
1106Service unavailable.
1109Blank required field.
1201Invalid bank code.
3101Invalid telco prefix (3 digits).
4000Insufficient funds.
8002 / 9001Invalid phone format.
8003Invalid telco.
8004 / 9003Invalid amount format.
8005 / 9004Amount limit exceeded.
8006 / 9005Duplicate transmission.
9002Invalid channel, or data-bundle productCode / amount mismatch.