API documentation

Base URL: https://norvionsolution.com. Send JSON bodies and read JSON responses. Every response has success, and either data or error.

Authentication

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.

Send an STK push

POST /v1/stkpush
FieldRequiredDescription
account_idYesAccount ID of the linked till, paybill or bank
secret_keyYesSecret key for that account
phoneYesCustomer phone: 0712345678, 254712345678 or +254…
amountYesWhole shillings, 1 – 250,000
referenceYesYour order or invoice number (max 100 characters)
descriptionNoShort description (first 13 characters are sent to M-Pesa)
callback_urlNoHTTPS 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.

Check transaction status

POST /v1/tstatus
{
  "account_id": "NRVJZ4D3TJZS8",
  "secret_key": "sk_...",
  "transaction_request_id": "TRX260924LH9GZ2DQFR"
}
statusMeaning
pendingWaiting for the customer to enter their PIN
successPaid. mpesa_receipt and transaction_date are set
cancelledCustomer cancelled the prompt
timeoutCustomer did not respond in time
failedOther failure, e.g. insufficient funds. See result_desc

Webhooks

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.

Verify the signature

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

Resend a webhook

POST /v1/webhook/resend

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

Errors

{
  "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" }
  }
}
HTTPcodeWhen
401invalid_credentialsWrong account_id or secret_key
403account_disabledThe linked account is disabled
404transaction_not_foundUnknown transaction_request_id for this account
422validation_errorMissing or invalid fields. See details
429rate_limitedToo many STK pushes in one minute
502mpesa_rejected / gateway_errorSafaricom refused the request or could not be reached