š PHP Examples
Complete Payment Integration (Updated with Proper Account Reference)
ā ļø Critical: Account reference usage depends on account type.
Paybill accounts require merchant's account number for proper routing.
Till accounts can use order references.
Vanilla PHP - Complete Workflow
<?php
class PaynexusClient {
private $baseUrl = 'https://paynexus.co.ke/api';
private $secretKey = 'your_secret_api_key_here';
public function getPaymentAccounts() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->baseUrl . '/merchant/payment-accounts');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . $this->secretKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true)['data'] ?? []
];
}
public function validatePhone($phone) {
$data = ['phone' => $phone];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->baseUrl . '/mpesa/validate-phone');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . $this->secretKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true)['data'] ?? []
];
}
public function initiatePayment($paymentAccountId, $amount, $phone, $accountReference, $description = null) {
$data = [
'payment_account_id' => $paymentAccountId,
'amount' => $amount,
'phone' => $phone,
'account_reference' => $accountReference, // CRITICAL: Must be set correctly
'description' => $description ?: 'Payment via PayNexus',
'remark' => 'Website Payment'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->baseUrl . '/mpesa/payment/initiate');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . $this->secretKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true)['data'] ?? []
];
}
}
// COMPLETE USAGE EXAMPLE WITH PROPER ACCOUNT REFERENCE HANDLING
$paynexus = new PaynexusClient();
// Step 1: Get payment accounts to determine account type
$accountsResponse = $paynexus->getPaymentAccounts();
if (!$accountsResponse['success']) {
die('Failed to get payment accounts: ' . json_encode($accountsResponse));
}
$mpesaAccount = null;
foreach ($accountsResponse['data'] as $account) {
if ($account['provider'] === 'mpesa') {
$mpesaAccount = $account;
break;
}
}
if (!$mpesaAccount) {
die('No M-Pesa payment account found');
}
// Step 2: Validate phone number
$phoneValidation = $paynexus->validatePhone('0746990866');
if (!$phoneValidation['success'] || !$phoneValidation['data']['valid']) {
die('Invalid phone number: ' . $phoneValidation['data']['error'] ?? 'Unknown error');
}
$normalizedPhone = $phoneValidation['data']['normalized'];
// Step 3: CRITICAL - Determine account reference based on account type
if ($mpesaAccount['type'] === 'paybill') {
// FOR PAYBILL: Use merchant's account number for proper fund routing
$accountReference = $mpesaAccount['account_number'];
if (!$accountReference) {
die('Paybill account number is required for proper routing');
}
$accountReference = substr($accountReference, 0, 12); // M-Pesa limit
echo "Using Paybill account - Account Reference: $accountReference\n";
} elseif ($mpesaAccount['type'] === 'till') {
// FOR TILL: Can use order reference (not used for routing)
$accountReference = 'ORDER_' . time();
$accountReference = substr($accountReference, 0, 12);
echo "Using Till account - Account Reference: $accountReference\n";
} else {
die('Unsupported payment account type: ' . $mpesaAccount['type']);
}
// Step 4: Initiate payment with correct account reference
$payment = $paynexus->initiatePayment(
$mpesaAccount['id'], // Payment account ID
100, // Amount in KES
$normalizedPhone,
$accountReference, // CRITICAL: Correct account reference
'Payment for website order'
);
if ($payment['success']) {
echo "ā
Payment initiated successfully!\n";
echo "š Checkout Request ID: " . $payment['data']['checkout_request_id'] . "\n";
echo "š¬ Customer message: " . $payment['data']['customer_message'] . "\n";
echo "š Account Type: " . $mpesaAccount['type'] . "\n";
echo "š Account Reference Used: " . $accountReference . "\n";
// Store checkout_request_id for status tracking
// Redirect customer to payment confirmation page
} else {
echo "ā Payment failed: " . ($payment['data']['error'] ?? 'Unknown error') . "\n";
}
?>
Laravel Integration
Service Class
// app/Services/PaynexusService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class PaynexusService {
private $baseUrl;
private $secretKey;
public function __construct() {
$this->baseUrl = 'https://paynexus.co.ke/api';
$this->secretKey = config('services.paynexus.secret_key');
}
public function initiatePayment(array $data) {
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'X-API-Key' => $this->secretKey
])->post($this->baseUrl . '/mpesa/payment/initiate', $data);
return [
'success' => $response->successful(),
'data' => $response->json()
];
}
}
?>
Controller Usage - Updated with Account Reference Logic
// app/Http/Controllers/PaymentController.php
<?php
namespace App\Http\Controllers;
use App\Services\PaynexusService;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
protected $paynexus;
public function __construct(PaynexusService $paynexus)
{
$this->paynexus = $paynexus;
}
public function initiate(Request $request)
{
try {
// Step 1: Get payment accounts to determine account type
$accountsResponse = $this->paynexus->getPaymentAccounts();
if (!$accountsResponse['success']) {
return response()->json([
'success' => false,
'error' => 'Failed to get payment accounts'
], 400);
}
$mpesaAccount = null;
foreach ($accountsResponse['data'] as $account) {
if ($account['provider'] === 'mpesa') {
$mpesaAccount = $account;
break;
}
}
if (!$mpesaAccount) {
return response()->json([
'success' => false,
'error' => 'No M-Pesa payment account found'
], 400);
}
// Step 2: Validate phone number
$phoneValidation = $this->paynexus->validatePhone($request->phone);
if (!$phoneValidation['success'] || !$phoneValidation['data']['valid']) {
return response()->json([
'success' => false,
'error' => 'Invalid phone number: ' . ($phoneValidation['data']['error'] ?? 'Unknown')
], 400);
}
// Step 3: CRITICAL - Determine account reference based on account type
if ($mpesaAccount['type'] === 'paybill') {
// FOR PAYBILL: Use merchant's account number for proper fund routing
$accountReference = $mpesaAccount['account_number'];
if (!$accountReference) {
return response()->json([
'success' => false,
'error' => 'Paybill account number is required for proper routing'
], 400);
}
$accountReference = substr($accountReference, 0, 12); // M-Pesa limit
} elseif ($mpesaAccount['type'] === 'till') {
// FOR TILL: Can use order reference (not used for routing)
$accountReference = $request->accountReference ?: 'ORDER_' . time();
$accountReference = substr($accountReference, 0, 12);
} else {
return response()->json([
'success' => false,
'error' => 'Unsupported payment account type: ' . $mpesaAccount['type']
], 400);
}
// Step 4: Initiate payment with correct account reference
$data = [
'payment_account_id' => $mpesaAccount['id'],
'amount' => $request->amount,
'phone' => $phoneValidation['data']['normalized'],
'account_reference' => $accountReference, // CRITICAL: Correct account reference
'description' => $request->description ?: 'Payment via PayNexus',
'remark' => 'Website Payment'
];
$result = $this->paynexus->initiatePayment($data);
return response()->json([
'success' => $result['success'],
'data' => $result['data'],
'account_type' => $mpesaAccount['type'],
'account_reference_used' => $accountReference,
'message' => $result['success'] ? 'Payment initiated successfully' : 'Payment failed'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'error' => 'Payment processing failed: ' . $e->getMessage()
], 500);
}
}
}
?>
š¢ JavaScript/Node.js Examples
Complete Node.js Integration (Updated with Proper Account Reference)
ā ļø Critical: Account reference usage depends on account type.
Paybill accounts require merchant's account number for proper routing.
Till accounts can use order references.
Payment Class - Complete Workflow
const https = require('https');
class PaynexusClient {
constructor(secretKey) {
this.baseUrl = 'https://paynexus.co.ke/api';
this.secretKey = secretKey;
}
async getPaymentAccounts() {
return this.makeRequest('/merchant/payment-accounts', null, 'GET');
}
async validatePhone(phone) {
const data = { phone: phone };
return this.makeRequest('/mpesa/validate-phone', data);
}
async initiatePayment(paymentAccountId, amount, phone, accountReference, description = null) {
const data = {
payment_account_id: paymentAccountId,
amount: amount,
phone: phone,
account_reference: accountReference, // CRITICAL: Must be set correctly
description: description || 'Payment via PayNexus',
remark: 'Website Payment'
};
return this.makeRequest('/mpesa/payment/initiate', data);
}
makeRequest(endpoint, data = null, method = 'POST') {
return new Promise((resolve, reject) => {
let postData = '';
let path = '/api' + endpoint;
if (method === 'POST' && data) {
postData = JSON.stringify(data);
}
const options = {
hostname: 'paynexus.co.ke',
path: path,
method: method,
headers: {
'Content-Type': 'application/json',
'X-API-Key': this.secretKey,
...(method === 'POST' && postData ? { 'Content-Length': Buffer.byteLength(postData) } : {})
}
};
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', (chunk) => responseData += chunk);
res.on('end', () => {
try {
const jsonResponse = JSON.parse(responseData);
resolve({
success: res.statusCode === 200,
data: jsonResponse.data || jsonResponse
});
} catch (error) {
resolve({
success: false,
error: 'Invalid JSON response'
});
}
});
});
req.on('error', reject);
if (method === 'POST' && postData) {
req.write(postData);
}
req.end();
});
}
}
// COMPLETE USAGE EXAMPLE WITH PROPER ACCOUNT REFERENCE HANDLING
async function processPayment() {
const paynexus = new PaynexusClient('your_secret_api_key_here');
try {
// Step 1: Get payment accounts to determine account type
const accountsResponse = await paynexus.getPaymentAccounts();
if (!accountsResponse.success) {
console.error('ā Failed to get payment accounts:', accountsResponse.error);
return;
}
const mpesaAccount = accountsResponse.data.find(acc => acc.provider === 'mpesa');
if (!mpesaAccount) {
console.error('ā No M-Pesa payment account found');
return;
}
// Step 2: Validate phone number
const phoneValidation = await paynexus.validatePhone('0746990866');
if (!phoneValidation.success || !phoneValidation.data.valid) {
console.error('ā Invalid phone number:', phoneValidation.data.error);
return;
}
const normalizedPhone = phoneValidation.data.normalized;
// Step 3: CRITICAL - Determine account reference based on account type
let accountReference;
if (mpesaAccount.type === 'paybill') {
// FOR PAYBILL: Use merchant's account number for proper fund routing
accountReference = mpesaAccount.account_number;
if (!accountReference) {
console.error('ā Paybill account number is required for proper routing');
return;
}
accountReference = accountReference.substring(0, 12); // M-Pesa limit
console.log('š Using Paybill account - Account Reference:', accountReference);
} else if (mpesaAccount.type === 'till') {
// FOR TILL: Can use order reference (not used for routing)
accountReference = 'ORDER_' + Date.now();
accountReference = accountReference.substring(0, 12);
console.log('š Using Till account - Account Reference:', accountReference);
} else {
console.error('ā Unsupported payment account type:', mpesaAccount.type);
return;
}
// Step 4: Initiate payment with correct account reference
const payment = await paynexus.initiatePayment(
mpesaAccount.id, // Payment account ID
100, // Amount in KES
normalizedPhone,
accountReference, // CRITICAL: Correct account reference
'Payment for website order'
);
if (payment.success) {
console.log('ā
Payment initiated successfully!');
console.log('š Checkout Request ID:', payment.data.checkout_request_id);
console.log('š¬ Customer message:', payment.data.customer_message);
console.log('š Account Type:', mpesaAccount.type);
console.log('š Account Reference Used:', accountReference);
// Store checkout_request_id for status tracking
// Show payment confirmation to user
} else {
console.error('ā Payment failed:', payment.data.error || 'Unknown error');
}
} catch (error) {
console.error('ā Payment processing error:', error.message);
}
}
// Execute the payment processing
processPayment();
Express.js Integration
Express Server
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());
const PAYNEXUS_URL = 'https://paynexus.co.ke/api';
const SECRET_KEY = 'your_secret_api_key_here';
app.post('/api/payment/initiate', async (req, res) => {
try {
const response = await axios.post(
`${PAYNEXUS_URL}/mpesa/payment/initiate`,
{
payment_account_id: 1,
amount: req.body.amount,
phone: req.body.phone,
account_reference: req.body.accountReference,
description: 'Payment',
remark: 'Payment'
},
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': SECRET_KEY
}
}
);
res.json({
success: true,
transactionId: response.data.transaction_id,
message: 'Payment initiated'
});
} catch (error) {
res.status(400).json({
success: false,
error: error.response?.data || error.message
});
}
});
// Webhook endpoint
app.post('/webhook/mpesa', (req, res) => {
const payload = req.body;
console.log('Webhook received:', payload);
if (payload.ResultCode === 0) {
// Payment successful - update database
console.log(`Payment successful: ${payload.TransactionID}`);
}
res.json({ ResultCode: 0, ResultDesc: 'Success' });
});
app.listen(3000);
š Python Examples
Basic Python
Payment Class
import requests
import json
class PaynexusMpesa:
def __init__(self, secret_key):
self.base_url = 'https://paynexus.co.ke/api'
self.secret_key = secret_key
self.headers = {
'Content-Type': 'application/json',
'X-API-Key': self.secret_key
}
def initiate_payment(self, phone, amount, account_reference):
data = {
'payment_account_id': 1,
'amount': amount,
'phone': phone,
'account_reference': account_reference,
'description': 'Payment',
'remark': 'Payment'
}
response = requests.post(
f"{self.base_url}/mpesa/payment/initiate",
headers=self.headers,
json=data
)
return {
'success': response.status_code == 200,
'data': response.json()
}
# Usage
mpesa = PaynexusMpesa('your_secret_api_key_here')
result = mpesa.initiate_payment('254712345678', 100, 'ORDER123')
if result['success']:
print(f"Payment initiated: {result['data']['transaction_id']}")
else:
print(f"Payment failed: {result['data']}")
Flask Integration
Flask App
from flask import Flask, request, jsonify
import requests
app = Flask(__name__)
PAYNEXUS_URL = 'https://paynexus.co.ke/api'
SECRET_KEY = 'your_secret_api_key_here'
@app.route('/api/payment/initiate', methods=['POST'])
def initiate_payment():
data = request.get_json()
response = requests.post(
f"{PAYNEXUS_URL}/mpesa/payment/initiate",
headers={
'Content-Type': 'application/json',
'X-API-Key': SECRET_KEY
},
json={
'payment_account_id': 1,
'amount': data['amount'],
'phone': data['phone'],
'account_reference': data['accountReference'],
'description': 'Payment',
'remark': 'Payment'
}
)
if response.status_code == 200:
return jsonify({
'success': True,
'transactionId': response.json()['transaction_id']
})
return jsonify({
'success': False,
'error': response.json()
}), 400
@app.route('/webhook/mpesa', methods=['POST'])
def mpesa_webhook():
payload = request.get_json()
print(f"Webhook received: {payload}")
if payload.get('ResultCode') == 0:
# Payment successful
print(f"Payment successful: {payload.get('TransactionID')}")
return jsonify({'ResultCode': 0, 'ResultDesc': 'Success'})
if __name__ == '__main__':
app.run(debug=True)
š cURL Examples
Complete API Workflow (Updated with Proper Account Reference)
ā ļø Critical: Account reference usage depends on account type.
Paybill accounts require merchant's account number for proper routing.
Till accounts can use order references.
API Test Script - Complete Workflow
#!/bin/bash
BASE_URL="https://paynexus.co.ke/api"
SECRET_KEY="your_secret_api_key_here"
PUBLIC_KEY="your_public_api_key_here"
echo "š Step 1: Health check"
curl -X GET "${BASE_URL}/health" \
-H "Content-Type: application/json"
echo -e "\nš Step 2: Get merchant info"
curl -X GET "${BASE_URL}/merchant" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${PUBLIC_KEY}"
echo -e "\nš³ Step 3: Get payment accounts (CRITICAL for account reference)"
ACCOUNTS_RESPONSE=$(curl -s -X GET "${BASE_URL}/merchant/payment-accounts" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${PUBLIC_KEY}")
echo "Payment accounts response:"
echo "$ACCOUNTS_RESPONSE" | jq '.'
# Extract account type and account number (requires jq)
ACCOUNT_TYPE=$(echo "$ACCOUNTS_RESPONSE" | jq -r '.data[] | select(.provider=="mpesa") | .type')
ACCOUNT_NUMBER=$(echo "$ACCOUNTS_RESPONSE" | jq -r '.data[] | select(.provider=="mpesa") | .account_number')
echo -e "\nš Account Type: $ACCOUNT_TYPE"
echo "š Account Number: $ACCOUNT_NUMBER"
echo -e "\nš Step 4: Validate phone number"
curl -X POST "${BASE_URL}/mpesa/validate-phone" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${PUBLIC_KEY}" \
-d '{"phone": "254712345678"}'
echo -e "\nš° Step 5: Initiate payment with CORRECT account reference"
# CRITICAL: Set account reference based on account type
if [ "$ACCOUNT_TYPE" = "paybill" ]; then
# FOR PAYBILL: Use merchant's account number for proper fund routing
ACCOUNT_REFERENCE="$ACCOUNT_NUMBER"
echo "š Using Paybill account - Account Reference: $ACCOUNT_REFERENCE"
elif [ "$ACCOUNT_TYPE" = "till" ]; then
# FOR TILL: Can use order reference (not used for routing)
ACCOUNT_REFERENCE="ORDER_$(date +%s)"
echo "š Using Till account - Account Reference: $ACCOUNT_REFERENCE"
else
echo "ā Unknown account type: $ACCOUNT_TYPE"
exit 1
fi
# Ensure account reference is 12 characters or less
ACCOUNT_REFERENCE=$(echo "$ACCOUNT_REFERENCE" | cut -c1-12)
echo "š Final Account Reference (12 chars max): $ACCOUNT_REFERENCE"
curl -X POST "${BASE_URL}/mpesa/payment/initiate" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${SECRET_KEY}" \
-d "{
\"payment_account_id\": 1,
\"amount\": 100,
\"phone\": \"254712345678\",
\"account_reference\": \"$ACCOUNT_REFERENCE\",
\"description\": \"Payment for website order\",
\"remark\": \"Website Payment\"
}"
echo -e "\nš Step 6: Check payment status (replace with actual checkout_request_id)"
curl -X POST "${BASE_URL}/mpesa/payment/status" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${SECRET_KEY}" \
-d '{"transaction_id": "YOUR_CHECKOUT_REQUEST_ID"}'
echo -e "\nā
Workflow complete!"
echo "š Remember to store the checkout_request_id for status tracking"
echo "š Account reference used: $ACCOUNT_REFERENCE (Account Type: $ACCOUNT_TYPE)"
š§ Common Integration Patterns
Error Handling
try {
const result = await mpesa.initiatePayment(phone, amount, ref);
if (result.success) {
// Handle success
console.log('Payment successful:', result.data.transaction_id);
} else {
// Handle API error
console.error('API Error:', result.data.message);
}
} catch (error) {
// Handle network error
console.error('Network Error:', error.message);
}
try:
result = mpesa.initiate_payment(phone, amount, ref)
if result['success']:
# Handle success
print(f"Payment successful: {result['data']['transaction_id']}")
else:
# Handle API error
print(f"API Error: {result['data']['message']}")
except requests.exceptions.RequestException as e:
# Handle network error
print(f"Network Error: {e}")
Webhook Security
Signature Verification (PHP)
<?php
function verifyWebhookSignature($payload, $signature) {
$secret = 'your_webhook_secret';
$expectedSignature = hash_hmac('sha256', json_encode($payload), $secret);
return hash_equals($signature, $expectedSignature);
}
// In your webhook handler
$signature = $_SERVER['HTTP_X_PAYNEXUS_SIGNATURE'] ?? '';
$payload = json_decode(file_get_contents('php://input'), true);
if (!verifyWebhookSignature($payload, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
?>
Payment Status Polling
Reliable Status Checking
// Poll payment status
async function pollPaymentStatus(transactionId, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
const status = await mpesa.checkPaymentStatus(transactionId);
if (status.completed) {
return status;
}
// Wait 5 seconds before next check
await new Promise(resolve => setTimeout(resolve, 5000));
}
throw new Error('Payment status check timeout');
}