Base URL: https://norvionsolution.com. Send JSON bodies and read JSON responses. Every response has success, and either data or error.
Each linked till, paybill or bank has its own account_id and secret_key. You can find them under Linked accounts. Send them in the JSON body, or as the X-Account-Id and X-Secret-Key headers. Keep the secret key on your server and never put it in a website or mobile app.
| Field | Required | Description |
|---|---|---|
account_id | Yes | Account ID of the linked till, paybill or bank |
secret_key | Yes | Secret key for that account |
phone | Yes | Customer phone: 0712345678, 254712345678 or +254… |
amount | Yes | Whole shillings, 1 – 250,000 |
reference | Yes | Your order or invoice number (max 100 characters) |
description | No | Short description (first 13 characters are sent to M-Pesa) |
callback_url | No | HTTPS URL for this payment's result. Overrides the account's webhook URL |
curl -X POST https://norvionsolution.com/v1/stkpush \
-H "Content-Type: application/json" \
-d '{
"account_id": "NRVJZ4D3TJZS8",
"secret_key": "sk_...",
"phone": "0712345678",
"amount": 100,
"reference": "INV-1001",
"callback_url": "https://yourshop.co.ke/mpesa/result"
}'
{
"success": true,
"message": "STK push sent. The customer should enter their M-Pesa PIN.",
"data": {
"transaction_request_id": "TRX260924LH9GZ2DQFR",
"status": "pending",
"amount": 100,
"phone": "254712345678",
"reference": "INV-1001",
"transaction_type": "CustomerBuyGoodsOnline",
"party_b": "5123456",
"checkout_request_id": "ws_CO_24092026003133...",
"customer_message": "Success. Request accepted for processing"
}
}
Save transaction_request_id. You need it to check the status or ask for the webhook again.
{
"account_id": "NRVJZ4D3TJZS8",
"secret_key": "sk_...",
"transaction_request_id": "TRX260924LH9GZ2DQFR"
}
| status | Meaning |
|---|---|
pending | Waiting for the customer to enter their PIN |
success | Paid. mpesa_receipt and transaction_date are set |
cancelled | Customer cancelled the prompt |
timeout | Customer did not respond in time |
failed | Other failure, e.g. insufficient funds. See result_desc |
When a payment finishes, we POST the result to the payment's callback_url, or to the webhook URL set on the account. Reply with any 2xx status. If your server does not answer, we retry after 1 min, 5 min, 15 min, 1 h and 6 h.
{
"event": "stkpush.success",
"delivery_id": 12,
"attempt": 1,
"sent_at": "2026-09-24T00:31:02+03:00",
"data": {
"transaction_request_id": "TRX260924LH9GZ2DQFR",
"status": "success",
"result_code": "0",
"result_desc": "The service request is processed successfully.",
"amount": 100,
"reference": "INV-1001",
"mpesa_receipt": "TIN4ABC123",
"transaction_date": "2026-09-24T00:31:00+03:00"
},
"safaricom": { "Body": { "stkCallback": { "...": "exactly what Safaricom sent" } } }
}
Events: stkpush.success, stkpush.failed, stkpush.cancelled, stkpush.timeout.
Every webhook has X-Norvion-Timestamp and X-Norvion-Signature headers. Check them with the account's webhook secret:
<?php
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_NORVION_TIMESTAMP'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, 'whsec_your_secret');
if (!hash_equals($expected, $_SERVER['HTTP_X_NORVION_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// mark order $event['data']['reference'] as paid when $event['data']['status'] === 'success'
http_response_code(200);
Sends a completed payment's result again, right away. Add an optional callback_url to send it somewhere else.
{
"account_id": "NRVJZ4D3TJZS8",
"secret_key": "sk_...",
"transaction_request_id": "TRX260924LH9GZ2DQFR",
"callback_url": "https://yourshop.co.ke/mpesa/result"
}
{
"success": false,
"error": {
"code": "validation_error",
"message": "Some fields are missing or invalid",
"details": { "phone": "Provide a valid Safaricom number, e.g. 0712345678 or 254712345678" }
}
}
| HTTP | code | When |
|---|---|---|
| 401 | invalid_credentials | Wrong account_id or secret_key |
| 403 | account_disabled | The linked account is disabled |
| 404 | transaction_not_found | Unknown transaction_request_id for this account |
| 422 | validation_error | Missing or invalid fields. See details |
| 429 | rate_limited | Too many STK pushes in one minute |
| 502 | mpesa_rejected / gateway_error | Safaricom refused the request or could not be reached |