Integration Guide

Base URL: https://sawipayinc.com/api/v1  ·  Drop-in JS: https://sawipayinc.com/assets/js/nexuspay.js  ·  SDKs: Node · Python · PHP

Quick Start

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.

Authentication

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.

Your secret key (nsk_...) is only for webhook signature verification — never include it in client-side JavaScript or mobile apps.

Error Handling

All errors return JSON with an error field plus an HTTP status code. Successful charges return 200; declined cards return 402.

HTTPMeaning
200Success
400Bad request — missing or malformed data
401Unauthorized — invalid or missing API key
402Payment declined — card refused, insufficient funds, etc.
422Validation error — invalid card number, expired card, etc.
500Server error — safe to retry with exponential backoff

Official SDKs

Drop a single file into your project — no package manager required for any SDK.

JS
Node.js SDK
Node 18+ · Native fetch · ESM + CJS · Zero dependencies
sdk/node/nexuspay.js
PY
Python SDK
Python 3.8+ · stdlib urllib · Auto-uses requests if installed
sdk/python/nexuspay.py
PHP
PHP SDK
PHP 7.4+ · Built-in cURL · No Composer needed
sdk/php/NexusPay.php

Node.js SDK

Setup

# No npm install — just copy the file
cp sdk/node/nexuspay.js your-project/lib/

Charge a card

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

M-Pesa STK Push

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);
}

Refund

// Full refund
const refund = await client.refund('TXN-20250601-A1B2C3D4');

// Partial refund (KES 500)
const partial = await client.refund('TXN-20250601-A1B2C3D4', 50000);

Webhook verification (Express)

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 });
});

FX Rates + Payment Links

// 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);

Python SDK

Setup

# 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)

Charge a card

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}')

M-Pesa STK Push

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

refund = client.refund('TXN-20250601-A1B2C3D4')          # full
partial = client.refund('TXN-20250601-A1B2C3D4', 50000) # partial KES 500

Webhook verification (Flask)

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)

Django webhook view

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})

PHP SDK

Setup

# No Composer needed — just copy the file
cp sdk/php/NexusPay.php your-project/lib/

Charge a card

<?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()}";
}

M-Pesa STK Push

$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']}";
}

Webhook verification

$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]);

Laravel integration

// 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,
    ]);
    // ...
}

E-commerce Plugins

Already run a store on WooCommerce or Shopify? Drop in a SawiPay plugin and start accepting card & M-Pesa payments without writing integration code.

WC
WooCommerce
WordPress 5.8+ · WooCommerce 5.0+ · Hosted checkout, M-Pesa STK, refund sync, webhooks
plugins/woocommerce-sawipay/
SP
Shopify
Node.js 18+ · Payment-link pattern · Works on all Shopify plans, no certification required
plugins/shopify-sawipay/

WooCommerce — Quick Setup

  1. Upload woocommerce-sawipay as a zip via Plugins → Add New → Upload Plugin
  2. Activate, then go to WooCommerce → Settings → Payments → SawiPay
  3. Paste your API Key + Secret Key from Dashboard → API Keys
  4. Copy the shown webhook URL into Dashboard → Settings → Webhooks
  5. Enable the gateway — customers now see "Card / M-Pesa (SawiPay)" at checkout

Shopify — Quick Setup

  1. Deploy the shopify-sawipay Node app (Heroku, Railway, Fly.io, etc.)
  2. Install on your store via the generated OAuth URL
  3. Paste your SawiPay API Key + Secret Key into the app's settings page
  4. Add a Manual Payment method named to include "SawiPay" or "M-Pesa"
  5. New orders using that method automatically get a SawiPay checkout link — paid orders are marked paid in Shopify automatically

Full setup guides, troubleshooting, and file structure are in each plugin's README.


Drop-in Widget

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.

Add the script + initialise

<!-- 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>
Call NexusPay.open() programmatically from any click handler if you don't want to use buttonId.

Custom Form + JavaScript

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);
    }
}

Server-Side Integration

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.

Node.js
Python
PHP / cURL
// 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'];
    }
}

POST /api/v1/charge

Charge a credit or debit card. Supports cross-currency — charge in USD/EUR and settle in KES automatically.

ParameterTypeReqDescription
amountintegerSmallest unit. KES 100.00 = 10000
currencystringISO 4217: KES USD EUR GBP UGX TZS
card.numberstringCard number (spaces stripped automatically)
card.exp_monthstring2-digit month: 01–12
card.exp_yearstring4-digit year: e.g. 2027
card.cvvstring3 or 4 digits
card.namestringCardholder name
descriptionstringStored with the transaction
metadataobjectAny key/value pairs — echoed in webhooks
Node.js
Python
PHP
cURL
const 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.status
charge = 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 Response (200)

{
  "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
}

POST /api/v1/refund

ParameterTypeDescription
referencestringTransaction reference (TXN-...)
amountintegerOptional partial amount. Omit for full refund.
Node.js
Python
PHP
cURL
// 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);   // partial
curl -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}'

GET /api/v1/transaction/:ref

Retrieve a transaction by its reference. Useful for polling status or confirming payment server-side.

Node.js
Python
PHP
cURL
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_..."

M-Pesa STK Push

Send a payment prompt directly to a Kenyan phone number. The customer enters their M-Pesa PIN to complete payment.

POST/api/v1/mpesa/stkpush
ParameterTypeDescription
phonestringKenyan number: 0712345678 or 254712345678
amountintegerKES whole number, min 1, max 150,000
referencestringShort label on customer's M-Pesa prompt
descriptionstringPayment description
Node.js
Python
PHP
cURL
// 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_..."

FX Rates

Live exchange rates updated hourly from open.er-api.com, cached so every request is fast.

Node.js
Python
PHP
cURL
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); // 12950
fx   = 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"

Webhooks

SawiPay POSTs a signed JSON event to your webhook URL for every payment event. Configure it in Dashboard → Settings.

EventFired when
payment.successCard payment approved
payment.failedCard payment declined or failed
refund.processedRefund successfully issued
dispute.openedChargeback raised by customer's bank

Payload structure

{
  "event":     "payment.success",
  "id":        "evt_a1b2c3d4e5f6",
  "timestamp": 1717070000,
  "data": {
    "reference": "TXN-20250601-A1B2C3D4",
    "amount":    100000,
    "currency":  "KES",
    "status":    "approved"
  }
}

Verify the signature (choose your language)

Node.js
Python
PHP
// 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]);
Always respond 200 immediately, even before you finish processing. SawiPay retries on any non-2xx response. View all delivery attempts in Admin → Webhooks.

Test Cards

Use these card numbers in test mode. Any future expiry date and any 3-digit CVV work.

4242 4242 4242 4242✓ ApprovedStandard success
4242 4242 4242 4000✕ DeclinedGeneric decline
4242 4242 4242 9995✕ DeclinedInsufficient funds
4242 4242 4242 9987✕ BlockedCard blocked by issuer
4242 4242 4242 3155⌛ Pending3D Secure required

Transaction Statuses

StatusMeaningWhat to do
approvedPayment authorisedFulfil the order
declinedCard declinedAsk customer to try another card
pendingAwaiting 3DS authPoll GET /transaction
refundedFully refundedUpdate your records
failedTechnical errorRetry or contact support

Supported Currencies

All amounts are in the smallest unit (cents). Cross-currency charges are automatically converted to KES at the live FX rate.

CodeCurrencyExample
KESKenyan ShillingKES 100.00 = 10000
USDUS Dollar$1.00 = 100
EUREuro€1.00 = 100
GBPBritish Pound£1.00 = 100
UGXUgandan ShillingUGX 100 = 10000
TZSTanzanian ShillingTZS 100 = 10000