Reference

BitnovaPay API

REST API for Direct Pay (non-custodial STK orchestration) and Smart Gateway (custodial M-Pesa, cards, PayPal, crypto). JSON in, JSON out. HTTPS only.

Overview

The API has two namespaces - personal (individual user keys) and business (scoped to a business).

Personal API v1

Bearer token: pk_live_-

https://pay.bitnova.co.ke/api/v1/
Business API v2

Bearer token: bsk_live_-

https://pay.bitnova.co.ke/api/v2/
All requests must be made over HTTPS. HTTP is rejected.

Authentication

Pass your API key as a Bearer token in the Authorization header.

# Direct Pay - personal key
curl -X GET https://pay.bitnova.co.ke/api/v1/balance \
  -H "Authorization: Bearer pk_live_xxxxxxxxxxxx"

# Smart Gateway - business key
curl -X POST https://pay.bitnova.co.ke/api/v2/payment \
  -H "Authorization: Bearer bsk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"method":"mpesa","amount":1000,"phone":"0712345678"}'
Never expose your API key in client-side JavaScript or public repositories.

Quick Start

Direct Pay - Non-Custodial STK

Money goes straight to your paybill. Bitnova deducts a flat fee from your service wallet.

// 1. Create a channel (your paybill or till)
curl -X POST https://pay.bitnova.co.ke/api/v1/channels \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"label":"Equity Bank","type":"paybill","shortcode":"247247"}'

// 2. Fire STK push directly to that channel
curl -X POST https://pay.bitnova.co.ke/api/v1/stk-push \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"channel_id":1,"amount":500,"phone":"0712345678"}'

Smart Gateway - Custodial M-Pesa

Funds collected by Bitnova, credited to your wallet after fees.

curl -X POST https://pay.bitnova.co.ke/api/v1/charges \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "mpesa",
    "amount": 1500,
    "phone":  "0712345678",
    "description": "Order #1234"
  }'

Error Handling

All errors return a JSON body with an error field. HTTP status follows REST conventions.

{
  "error": "Phone number required for M-Pesa.",
  "code":  400
}
HTTP StatusMeaning
400Bad request - missing or invalid parameter
401Unauthorized - invalid or missing API key
403Forbidden - insufficient permissions
404Resource not found
429Rate limit exceeded
500Server error - try again

Channels - Direct Pay

Channels are your own paybill, till, or bank shortcodes. STK pushes go directly to these destinations.

GET /api/v1/channels List all your channels
{ "data": [{ "id": 1, "label": "Equity Bank", "type": "paybill", "shortcode": "247247", "is_active": true }], "count": 1 }
POST /api/v1/channels Create a channel
ParameterTypeDescription
labelstringrequirede.g. "Equity Bank"
typestringrequiredpaybill | till | bank
shortcodestringrequiredM-Pesa paybill or till number
account_referencestringoptionalPrefix on M-Pesa prompt

STK Push - Direct Pay

Money goes directly to your paybill. Bitnova never holds transaction funds.
POST /api/v1/stk-push Non-custodial STK to your channel
ParameterTypeDescription
channel_idintegerrequiredID from GET /channels — this is the bank/till shortcode (PartyB)
amountfloatrequiredAmount in KES (min 1)
phonestringrequired07XXXXXXXX or 2547XXXXXXXX — customer's M-Pesa (PartyA)
descriptionstringoptionalMax 20 chars (shown on M-Pesa prompt)
payer_namestringoptionalCustomer name for receipt
payer_emailstringoptionalSend receipt to this address

Response

{ "status":"pending","module":"A","reference":"MDA-XXXXXXXXXX","fee":5.00,"message":"STK sent to 0712345678. Complete on your phone." }

Create Payment - Smart Gateway

Initiate a custodial payment. Funds collected by Bitnova, credited to your wallet after fees.

POST /api/v1/charges Custodial payment - all methods
ParameterTypeDescription
methodstringrequiredmpesa | card | paypal | crypto
amountfloatrequiredAmount in KES
phonestringif mpesa07XXXXXXXX or 2547XXXXXXXX
descriptionstringoptionalOrder description
payer_emailstringoptionalReceipt email

PayPal / Card response

{ "gateway":"paypal","redirect":"https://www.paypal.com/checkoutnow?token=xxx","reference":"BNP-XXXXXXXXXX" }

Crypto response (NOWPayments)

{ "gateway":"nowpayments","redirect":"https://nowpayments.io/payment/?iid=xxxx","invoice_id":"xxxx" }

Payment Status

GET /api/v1/charges/{reference} Poll for completion
{ "reference":"BNP-XXXXXXXXXX","status":"completed","amount":1500,"fee":15,"net_amount":1485,"method":"mpesa" }
Poll every 4 seconds for M-Pesa, 3 seconds for cards. Max 5 minutes then show timeout. Or use webhooks for server-to-server notification.

Transactions

GET /api/v1/transactions List with filters
Query paramTypeDefaultDescription
statusstringallpending | completed | failed
modulestringallA (Direct Pay) | B (Smart Gateway)
methodstringallmpesa | card | crypto
pageinteger1Pagination
per_pageinteger15Max 100

Wallet Balance

GET /api/v1/balance Current wallet balance
{ "balance":12500.00,"locked_balance":15.00,"available":12485.00,"currency":"KES" }
locked_balance is reserved for pending Direct Pay service fees. Released automatically on completion or failure.

Withdrawal

Withdrawals are processed manually within 1-2 business days.
POST /api/v1/withdraw Request a withdrawal
ParameterTypeDescription
amountfloatrequiredMin KES 100
methodstringrequiredmpesa | bank | paypal
accountstringrequiredPhone, account number, or PayPal email

Register Webhook

Webhooks can be registered via the API — useful for plugins and integrations that auto-configure on install. Registrations are idempotent: registering the same URL twice returns the existing secret.

POST /api/v1/webhooks Register a webhook endpoint
ParameterTypeDescription
urlstringrequiredHTTPS URL to receive events
eventsarrayrequiredpayment.success | payment.failed | withdrawal.success | withdrawal.failed
sourcestringoptionalapi | woocommerce | shopify

Response

{ "id": 1, "secret": "whsec_xxxxxxxxxxxxxxxx", "url": "https://yoursite.com/webhooks", "events": ["payment.success","payment.failed"] }
Store the secret securely — it is only returned once. Use it to verify incoming webhook signatures.
GET /api/v1/webhooks List registered webhooks
{ "data": [{ "id": 1, "url": "https://...", "events": ["payment.success"], "is_active": true }], "count": 1 }
DELETE /api/v1/webhooks/{id} Remove a webhook
{ "deleted": true, "id": 1 }

Webhook Events

Configure endpoints in your dashboard under Settings → Webhooks. Bitnova sends signed JSON to your URL on every payment event.

payment.success
Direct Pay (Module A) or Smart Gateway (Module B) payment completed
payment.failed
STK cancelled, card declined, or expired
payment.refunded
Card or PayPal refund processed
payout.completed
Bulk disbursement sent to recipient
withdrawal.completed
Admin marked withdrawal as sent
invoice.paid
Invoice fully paid

Payload structure

{
  "event":     "payment.success",
  "timestamp": 1735689600,
  "data": {
    "reference": "MDA-XXXXXXXXXX",
    "module":    "A",
    "amount":    2500,
    "fee":       5,
    "method":    "mpesa",
    "status":    "completed"
  }
}

Signature Verification

Every webhook includes an X-BitnovaPay-Signature header formatted as sha256=HMAC. Always verify before processing.

PHP

$payload  = file_get_contents('php://input');
$sig      = $_SERVER['HTTP_X_BITNOVAPAY_SIGNATURE'];
$expected = 'sha256=' . hash_hmac('sha256', $payload, 'your_secret');

if (!hash_equals($expected, $sig)) {
    http_response_code(401); die('Unauthorized');
}
$event = json_decode($payload, true);

Node.js

const sig      = req.headers['x-bitnovapay-signature'];
const expected = 'sha256=' + require('crypto')
  .createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(req.body, 'utf8').digest('hex');
if (!require('crypto').timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
  return res.status(401).send('Unauthorized');