Base URL: https://sawipayinc.com/api/v1
·
Drop-in JS: https://sawipayinc.com/assets/js/nexuspay.js
·
SDKs: Node · Python · PHP
SawiPay gives you four ways to accept card payments plus M-Pesa — and official SDKs for Node.js, Python, and PHP so you can integrate in minutes from any backend.
Pass your API key in the Authorization header on every request. Get your keys from Dashboard → API Keys.
Authorization: Bearer npk_test_YOUR_API_KEY
Content-Type: application/json
The X-API-Key header is also accepted as an alternative.
All errors return JSON with an error field plus an HTTP status code. Successful charges return 200; declined cards return 402.
200Success400Bad request — missing or malformed data401Unauthorized — invalid or missing API key402Payment declined — card refused, insufficient funds, etc.422Validation error — invalid card number, expired card, etc.500Server error — safe to retry with exponential backoffDrop a single file into your project — no package manager required for any SDK.
# No npm install — just copy the file
cp sdk/node/nexuspay.js your-project/lib/
import NexusPay from './lib/nexuspay.js';
const client = new NexusPay({
apiKey: 'npk_live_...',
secretKey: 'sk_live_...', // only needed for webhook verification
});
const charge = await client.charge({
amount: 100000, // KES 1,000.00 (smallest unit)
currency: 'KES',
description: 'Order #10042',
metadata: { order_id: '10042' },
card: {
number: '4242424242424242',
exp_month: '12',
exp_year: '2027',
cvv: '123',
name: 'Jane Wanjiku',
},
});
console.log(charge.reference); // TXN-20250601-A1B2C3D4
console.log(charge.auth_code); // F3A92C
const push = await client.mpesaPush({
phone: '0712345678',
amount: 500, // KES 500 (whole number)
reference: 'Order-10042',
});
// Block until customer enters PIN (up to 2 minutes)
const result = await client.mpesaWait(push.checkout_request_id, {
intervalMs: 3000,
onPoll: (s) => console.log('Status:', s.status),
});
if (result.status === 'completed') {
console.log('Paid! Receipt:', result.mpesa_receipt_number);
}
// Full refund
const refund = await client.refund('TXN-20250601-A1B2C3D4');
// Partial refund (KES 500)
const partial = await client.refund('TXN-20250601-A1B2C3D4', 50000);
import express from 'express';
const app = express();
app.post('/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['x-nexuspay-signature'];
const valid = await client.verifyWebhook(req.body, sig);
if (!valid) return res.status(401).json({ error: 'Invalid signature' });
const event = JSON.parse(req.body);
if (event.event === 'payment.success') {
// fulfil the order
}
res.json({ received: true });
});
// Live exchange rates
const fx = await client.getFxRates();
console.log('1 USD =', fx.rates.KES, 'KES');
// Convert
const conv = await client.convertCurrency(100, 'USD', 'KES');
console.log('$100 =', conv.converted, 'KES');
// Hosted payment link
const link = await client.createPaymentLink({
amount: 250000,
currency: 'KES',
description: 'Invoice #089',
expires_hours: 48,
});
console.log('Share:', link.url);
# No pip install needed — just copy the file
cp sdk/python/nexuspay.py your-project/
# Optional: pip install requests (SDK uses it automatically if present)
from nexuspay import NexusPay, NexuPayError
client = SawiPay(
api_key = 'npk_live_...',
secret_key = 'sk_live_...',
)
try:
charge = client.charge(
amount = 100000,
currency = 'KES',
description = 'Order #10042',
metadata = {'order_id': '10042'},
card = {
'number': '4242424242424242',
'exp_month': '12',
'exp_year': '2027',
'cvv': '123',
'name': 'Jane Wanjiku',
},
)
print(charge['reference']) # TXN-20250601-A1B2C3D4
except NexuPayError as e:
print(f'Error [{e.code}]: {e}')
push = client.mpesa_push(
phone = '0712345678',
amount = 500,
reference = 'Order-10042',
)
result = client.mpesa_wait(
push['checkout_request_id'],
interval_s = 3,
max_wait_s = 120,
on_poll = lambda s: print('.', end='', flush=True),
)
if result['status'] == 'completed':
print('Paid! Receipt:', result['mpesa_receipt_number'])
refund = client.refund('TXN-20250601-A1B2C3D4') # full
partial = client.refund('TXN-20250601-A1B2C3D4', 50000) # partial KES 500
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
sig = request.headers.get('X-NexusPay-Signature', '')
valid = client.verify_webhook(request.get_data(), sig)
if not valid:
return jsonify(error='Invalid signature'), 401
event = request.get_json()
if event['event'] == 'payment.success':
pass # fulfil order
return jsonify(received=True)
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def nexuspay_webhook(request):
if request.method != 'POST':
return JsonResponse({'error': 'Method not allowed'}, status=405)
sig = request.headers.get('X-Nexuspay-Signature', '')
valid = client.verify_webhook(request.body, sig)
if not valid:
return JsonResponse({'error': 'Invalid signature'}, status=401)
event = json.loads(request.body)
# handle event...
return JsonResponse({'received': True})
# No Composer needed — just copy the file
cp sdk/php/NexusPay.php your-project/lib/
<?php
require 'lib/NexusPay.php';
use SawiPay\Client;
use NexusPay\NexuPayException;
$client = new Client([
'api_key' => 'npk_live_...',
'secret_key' => 'sk_live_...',
]);
try {
$charge = $client->charge([
'amount' => 100000,
'currency' => 'KES',
'description' => 'Order #10042',
'card' => [
'number' => '4242424242424242',
'exp_month' => '12',
'exp_year' => '2027',
'cvv' => '123',
'name' => 'Jane Wanjiku',
],
]);
echo $charge['reference']; // TXN-20250601-A1B2C3D4
} catch (NexuPayException $e) {
echo "Error [{$e->errorCode}]: {$e->getMessage()}";
}
$push = $client->mpesaPush([
'phone' => '0712345678',
'amount' => 500,
]);
$result = $client->mpesaWait(
checkoutRequestId: $push['checkout_request_id'],
intervalSeconds: 3,
maxWaitSeconds: 120,
onPoll: function(array $s): void { echo '.'; }
);
if ($result['status'] === 'completed') {
echo "Paid! Receipt: {$result['mpesa_receipt_number']}";
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_NEXUSPAY_SIGNATURE'] ?? '';
if (!$client->verifyWebhook($rawBody, $signature)) {
http_response_code(401);
exit(json_encode(['error' => 'Invalid signature']));
}
$event = json_decode($rawBody, true);
match ($event['event']) {
'payment.success' => fulfillOrder($event['data']),
'refund.processed'=> handleRefund($event['data']),
default => null,
};
http_response_code(200);
echo json_encode(['received' => true]);
// config/services.php
'nexuspay' => [
'key' => env('NEXUSPAY_API_KEY'),
'secret' => env('NEXUSPAY_SECRET_KEY'),
],
// AppServiceProvider::register()
$this->app->singleton(\NexusPay\Client::class, fn() =>
new \NexusPay\Client([
'api_key' => config('services.nexuspay.key'),
'secret_key' => config('services.nexuspay.secret'),
])
);
// In a controller or job:
public function pay(Request $request, \NexusPay\Client $nexuspay)
{
$charge = $nexuspay->charge([
'amount' => $request->amount,
'currency' => 'KES',
'card' => $request->card,
]);
// ...
}
Already run a store on WooCommerce or Shopify? Drop in a SawiPay plugin and start accepting card & M-Pesa payments without writing integration code.
woocommerce-sawipay as a zip via Plugins → Add New → Upload Pluginshopify-sawipay Node app (Heroku, Railway, Fly.io, etc.)Full setup guides, troubleshooting, and file structure are in each plugin's README.
Paste two lines into any HTML page and get a fully styled popup checkout. No form to build, no CSS to write, no backend required.
<!-- 1. Include the widget before </body> -->
<script src="https://sawipayinc.com/assets/js/nexuspay.js"></script>
<!-- 2. Initialise -->
<script>
NexusPay.init({
apiKey: 'npk_test_YOUR_KEY',
amount: 250000, // KES 2,500.00 in cents
currency: 'KES',
description: 'Premium Course',
merchantName: 'My Academy',
buttonId: 'pay-btn', // auto-open on click
onSuccess: function(data) {
window.location.href = '/thanks?ref=' + data.reference;
},
onError: function(data) {
alert('Payment failed: ' + data.message);
}
});
</script>
<button id="pay-btn">Pay KES 2,500</button>
NexusPay.open() programmatically from any click handler if you don't want to use buttonId.Generate a hosted payment URL — share it anywhere, no website needed. The customer pays on SawiPay's hosted page.
const link = await client.createPaymentLink({
amount: 250000,
currency: 'KES',
description: 'Invoice #2025-089',
redirect_url: 'https://yourapp.com/complete',
expires_hours: 48,
});
console.log(link.url); // share this with your customer
link = client.create_payment_link(
amount = 250000,
currency = 'KES',
description = 'Invoice #2025-089',
redirect_url = 'https://yourapp.com/complete',
expires_hours = 48,
)
print(link['url'])
$link = $client->createPaymentLink([
'amount' => 250000,
'currency' => 'KES',
'description' => 'Invoice #2025-089',
'redirect_url' => 'https://yourapp.com/complete',
'expires_hours' => 48,
]);
echo $link['url'];
curl -X POST https://sawipayinc.com/api/v1/payment-links \
-H "Authorization: Bearer npk_test_..." \
-H "Content-Type: application/json" \
-d '{"amount":250000,"currency":"KES","description":"Invoice #089"}'
Build your own checkout form, send card data to the API via fetch(). Full design freedom, no backend required for basic charges.
async function submitPayment() {
const response = await fetch('https://sawipayinc.com/api/v1/charge', {
method: 'POST',
headers: {
'Authorization': 'Bearer npk_test_YOUR_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 250000,
currency: 'KES',
card: {
number: document.getElementById('card-number').value.replace(/\s/g, ''),
exp_month: document.getElementById('exp-month').value,
exp_year: document.getElementById('exp-year').value,
cvv: document.getElementById('cvv').value,
name: document.getElementById('card-name').value,
},
}),
});
const data = await response.json();
if (data.success) {
window.location.href = '/thanks?ref=' + data.reference;
} else {
showError(data.message);
}
}
Process payments entirely on your backend — best for WooCommerce, Laravel, Django, Express, and any server that needs to keep the API key off the browser.
// Express route
app.post('/checkout', async (req, res) => {
try {
const charge = await client.charge({
amount: req.body.amount,
currency: 'KES',
card: req.body.card,
metadata: { user_id: req.session.userId },
});
await db.orders.update(req.body.orderId, {
status: 'paid',
txn_ref: charge.reference,
});
res.json({ success: true, reference: charge.reference });
} catch (err) {
res.status(402).json({ error: err.message });
}
});
# Flask route
@app.route('/checkout', methods=['POST'])
def checkout():
try:
charge = client.charge(
amount = request.json['amount'],
currency = 'KES',
card = request.json['card'],
metadata = {'user_id': session.get('user_id')},
)
# update your DB
return jsonify(success=True, reference=charge['reference'])
except NexuPayError as e:
return jsonify(error=str(e)), 402
// WordPress / WooCommerce
public function process_payment($order_id) {
$order = wc_get_order($order_id);
try {
$charge = $this->client->charge([
'amount' => (int)($order->get_total() * 100),
'currency' => 'KES',
'card' => $this->getCardFromPost(),
]);
$order->payment_complete($charge['reference']);
return ['result' => 'success', 'redirect' => $this->get_return_url($order)];
} catch (\NexusPay\NexuPayException $e) {
wc_add_notice($e->getMessage(), 'error');
return ['result' => 'failure'];
}
}
Charge a credit or debit card. Supports cross-currency — charge in USD/EUR and settle in KES automatically.
amountinteger✓Smallest unit. KES 100.00 = 10000currencystring✓ISO 4217: KES USD EUR GBP UGX TZScard.numberstring✓Card number (spaces stripped automatically)card.exp_monthstring✓2-digit month: 01–12card.exp_yearstring✓4-digit year: e.g. 2027card.cvvstring✓3 or 4 digitscard.namestring✓Cardholder namedescriptionstring—Stored with the transactionmetadataobject—Any key/value pairs — echoed in webhooksconst charge = await client.charge({
amount: 100000, currency: 'KES',
card: { number: '4242424242424242', exp_month: '12', exp_year: '2027', cvv: '123', name: 'Jane' },
});
// charge.reference, charge.auth_code, charge.statuscharge = client.charge(
amount=100000, currency='KES',
card={'number':'4242424242424242','exp_month':'12','exp_year':'2027','cvv':'123','name':'Jane'},
)$charge = $client->charge([
'amount' => 100000, 'currency' => 'KES',
'card' => ['number'=>'4242424242424242','exp_month'=>'12','exp_year'=>'2027','cvv'=>'123','name'=>'Jane'],
]);curl -X POST https://sawipayinc.com/api/v1/charge \
-H "Authorization: Bearer npk_test_..." \
-H "Content-Type: application/json" \
-d '{"amount":100000,"currency":"KES","card":{"number":"4242424242424242","exp_month":"12","exp_year":"2027","cvv":"123","name":"Jane"}}'{
"success": true,
"reference": "TXN-20250601-A1B2C3D4",
"status": "approved",
"message": "Payment approved.",
"auth_code": "F3A92C",
"amount": 100000,
"currency": "KES",
"card": { "last4": "4242", "network": "visa", "token": "tok_..." },
"created_at": "2025-06-01 14:32:01"
}
// Cross-currency charges also include:
"fx": {
"original_amount": 10000,
"original_currency": "USD",
"settled_amount": 1295000,
"settled_currency": "KES",
"rate": 129.5
}
referencestringTransaction reference (TXN-...)amountintegerOptional partial amount. Omit for full refund.// Full refund
await client.refund('TXN-20250601-A1B2C3D4');
// Partial — KES 500
await client.refund('TXN-20250601-A1B2C3D4', 50000);client.refund('TXN-20250601-A1B2C3D4') # full
client.refund('TXN-20250601-A1B2C3D4', 50000) # partial$client->refund('TXN-20250601-A1B2C3D4'); // full
$client->refund('TXN-20250601-A1B2C3D4', 50000); // partialcurl -X POST https://sawipayinc.com/api/v1/refund \
-H "Authorization: Bearer npk_test_..." \
-H "Content-Type: application/json" \
-d '{"reference":"TXN-20250601-A1B2C3D4","amount":50000}'Retrieve a transaction by its reference. Useful for polling status or confirming payment server-side.
const txn = await client.getTransaction('TXN-20250601-A1B2C3D4');
console.log(txn.status, txn.card.network);txn = client.get_transaction('TXN-20250601-A1B2C3D4')
print(txn['status'], txn['card']['network'])$txn = $client->getTransaction('TXN-20250601-A1B2C3D4');
echo $txn['status'];curl https://sawipayinc.com/api/v1/transaction/TXN-20250601-A1B2C3D4 \
-H "Authorization: Bearer npk_test_..."Send a payment prompt directly to a Kenyan phone number. The customer enters their M-Pesa PIN to complete payment.
/api/v1/mpesa/stkpushphonestringKenyan number: 0712345678 or 254712345678amountintegerKES whole number, min 1, max 150,000referencestringShort label on customer's M-Pesa promptdescriptionstringPayment description// Step 1: initiate push
const push = await client.mpesaPush({ phone: '0712345678', amount: 500 });
// Step 2: poll until done (or use your webhook)
const result = await client.mpesaWait(push.checkout_request_id, {
intervalMs: 3000, maxWaitMs: 90000,
});
console.log(result.status); // 'completed' | 'failed' | 'cancelled'push = client.mpesa_push(phone='0712345678', amount=500)
result = client.mpesa_wait(push['checkout_request_id'], interval_s=3)
print(result['status'])$push = $client->mpesaPush(['phone' => '0712345678', 'amount' => 500]);
$result = $client->mpesaWait($push['checkout_request_id']);
echo $result['status'];# Step 1: initiate
curl -X POST https://sawipayinc.com/api/v1/mpesa/stkpush \
-H "Authorization: Bearer npk_test_..." \
-H "Content-Type: application/json" \
-d '{"phone":"0712345678","amount":500,"reference":"Order-001"}'
# Step 2: poll status
curl https://sawipayinc.com/api/v1/mpesa/status/{checkout_request_id} \
-H "Authorization: Bearer npk_test_..."Live exchange rates updated hourly from open.er-api.com, cached so every request is fast.
const fx = await client.getFxRates();
console.log(fx.rates); // { KES: 129.5, USD: 1, EUR: 0.92, ... }
const conv = await client.convertCurrency(100, 'USD', 'KES');
console.log(conv.converted); // 12950fx = client.get_fx_rates()
print(fx['rates'])
conv = client.convert_currency(100, 'USD', 'KES')
print(conv['converted'])$fx = $client->getFxRates();
$conv = $client->convertCurrency(100, 'USD', 'KES');
echo $conv['converted'];curl https://sawipayinc.com/api/v1/fx/rates
curl "https://sawipayinc.com/api/v1/fx/convert?from=USD&to=KES&amount=100"SawiPay POSTs a signed JSON event to your webhook URL for every payment event. Configure it in Dashboard → Settings.
payment.successCard payment approvedpayment.failedCard payment declined or failedrefund.processedRefund successfully issueddispute.openedChargeback raised by customer's bank{
"event": "payment.success",
"id": "evt_a1b2c3d4e5f6",
"timestamp": 1717070000,
"data": {
"reference": "TXN-20250601-A1B2C3D4",
"amount": 100000,
"currency": "KES",
"status": "approved"
}
}
// Express — must use express.raw() to get raw body
app.post('/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['x-nexuspay-signature'];
const valid = await client.verifyWebhook(req.body, sig);
if (!valid) return res.status(401).json({ error: 'Invalid signature' });
const { event, data } = JSON.parse(req.body);
if (event === 'payment.success') { /* fulfil order */ }
res.json({ received: true });
});# Flask
@app.route('/webhooks', methods=['POST'])
def webhook():
sig = request.headers.get('X-NexusPay-Signature', '')
if not client.verify_webhook(request.get_data(), sig):
return jsonify(error='Invalid signature'), 401
event = request.get_json()
if event['event'] == 'payment.success':
pass # fulfil order
return jsonify(received=True)$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_NEXUSPAY_SIGNATURE'] ?? '';
if (!$client->verifyWebhook($raw, $sig)) {
http_response_code(401); exit('Invalid signature');
}
$event = json_decode($raw, true);
if ($event['event'] === 'payment.success') {
fulfillOrder($event['data']);
}
http_response_code(200); echo json_encode(['received' => true]);Use these card numbers in test mode. Any future expiry date and any 3-digit CVV work.
4242 4242 4242 4242✓ ApprovedStandard success4242 4242 4242 4000✕ DeclinedGeneric decline4242 4242 4242 9995✕ DeclinedInsufficient funds4242 4242 4242 9987✕ BlockedCard blocked by issuer4242 4242 4242 3155⌛ Pending3D Secure requiredAll amounts are in the smallest unit (cents). Cross-currency charges are automatically converted to KES at the live FX rate.
KESKenyan ShillingKES 100.00 = 10000USDUS Dollar$1.00 = 100EUREuro€1.00 = 100GBPBritish Pound£1.00 = 100UGXUgandan ShillingUGX 100 = 10000TZSTanzanian ShillingTZS 100 = 10000