The canonical string#
Take the eleven values below exactly as you will send them (we trim leading and trailing spaces, so send them trimmed), join them with a newline (\n) in this order, and compute HMAC-SHA256 with your API secret. Send the lowercase hex digest as signature.
merchant_id, invoice_id, amount, currency, description, return_url, cancel_url, notify_url, customer_phone, customer_email, timestamp
Empty optional values still take their place in the list (an empty line). Sign the amount as the same string you post: "1500.00" and "1500" produce different signatures.
Code examples#
PHP: build the auto-submitting form#
<?php
// Run on YOUR server. Use the TEST Merchant ID/secret in the sandbox, the live ones in production.
$MERCHANT_ID = 'MRT1A2B3C4D';
$API_SECRET = 'sk_test_...';
$BASE = 'https://maainvest.co.ke';
$fields = [
'merchant_id' => $MERCHANT_ID,
'invoice_id' => 'ORD-1001', // your order id, new for every test
'amount' => '1500.00',
'currency' => 'KES',
'description' => 'Order #1001',
'return_url' => 'https://shop.example/thanks',
'cancel_url' => 'https://shop.example/cart',
'notify_url' => 'https://shop.example/maainvest-webhook.php',
'customer_phone' => '', // or '0712345678'
'customer_email' => '',
'timestamp' => (string)time(),
];
$order = ['merchant_id','invoice_id','amount','currency','description','return_url',
'cancel_url','notify_url','customer_phone','customer_email','timestamp'];
$canonical = implode("\n", array_map(fn($k) => $fields[$k], $order));
$fields['signature'] = hash_hmac('sha256', $canonical, $API_SECRET);
?>
<form id="pay" method="post" action="<?= $BASE ?>/pay/start.php">
<?php foreach ($fields as $k => $v): ?>
<input type="hidden" name="<?= htmlspecialchars($k) ?>" value="<?= htmlspecialchars($v) ?>">
<?php endforeach; ?>
</form>
<script>document.getElementById('pay').submit();</script>Node.js#
const crypto = require('crypto');
const order = ['merchant_id','invoice_id','amount','currency','description','return_url',
'cancel_url','notify_url','customer_phone','customer_email','timestamp'];
const fields = {
merchant_id: process.env.MAA_MERCHANT_ID, invoice_id: 'ORD-1001', amount: '1500.00', currency: 'KES',
description: 'Order #1001', return_url: 'https://shop.example/thanks', cancel_url: 'https://shop.example/cart',
notify_url: 'https://shop.example/maainvest-webhook', customer_phone: '', customer_email: '',
timestamp: String(Math.floor(Date.now() / 1000)),
};
const canonical = order.map(k => fields[k]).join('\n');
fields.signature = crypto.createHmac('sha256', process.env.MAA_API_SECRET).update(canonical).digest('hex');
// Render an auto-submitting <form method="post" action="https://maainvest.co.ke/pay/start.php"> with these fields.Python#
import hmac, hashlib, time
order = ['merchant_id','invoice_id','amount','currency','description','return_url',
'cancel_url','notify_url','customer_phone','customer_email','timestamp']
fields = {
'merchant_id': MERCHANT_ID, 'invoice_id': 'ORD-1001', 'amount': '1500.00', 'currency': 'KES',
'description': 'Order #1001', 'return_url': 'https://shop.example/thanks', 'cancel_url': 'https://shop.example/cart',
'notify_url': 'https://shop.example/maainvest-webhook', 'customer_phone': '', 'customer_email': '',
'timestamp': str(int(time.time())),
}
canonical = '\n'.join(fields[k] for k in order)
fields['signature'] = hmac.new(API_SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
# POST fields (form-encoded) from the customer's browser to https://maainvest.co.ke/pay/start.phpIf you get “could not verify”#
Most common mistake: “We could not verify this payment request.” means the signature did not match. Check the field order, that empty fields are included as empty lines, that you signed the same
amount string you posted, that you used the secret that matches the Merchant ID (test with test, live with live) and that your server clock is correct.
MAA INVEST Pay Docs