MyPay API
MyPay lets you collect M-Pesa payments straight into your till, paybill or bank account with a single API call. You don't need your own Daraja app. We send the STK push, Safaricom tells us the result, and we forward it to your callback URL.
- Base URL
https://mypay.co.ke/api/v1- Format
- JSON request bodies (
Content-Type: application/json) or form data. JSON responses. - Method
- All endpoints use
POST.
How a payment works
- Your system calls STK Push with the amount, phone number and reference.
- The customer gets an M-Pesa prompt on their phone and enters their PIN.
- Money goes directly to your till, paybill or bank account.
- MyPay POSTs the result to the account's callback URL. You can also poll the status.
Account types
One MyPay login can link many accounts. Each account has its own API key, signin key, callback URL, payment pages and subscription.
| Type | Money goes to | M-Pesa transaction type | Account reference used |
|---|---|---|---|
| Till | Your till number | CustomerBuyGoodsOnline | The reference you send |
| Paybill | Your paybill number | CustomerPayBillOnline | account_number if you send it, otherwise reference |
| Bank account | Your bank's paybill | CustomerPayBillOnline | Always your bank account number. It is fixed when you link the account, and any reference or account number in the request is ignored. |
Whatever the account type, the reference you send is stored and returned to you as TransactionReference in callbacks and status responses, so you can match payments to orders.
Activation & billing
Every linked account costs KES 350 per month. New accounts are inactive. While an account is inactive or expired:
- STK Push requests are rejected with
MP402(never paid) orMP403(expired). - Its payment pages show "not accepting payments".
To activate, open the account in your dashboard and click Pay & activate. You'll get an M-Pesa prompt to pay paybill 4322685 (account MYPAY<account id>). The account activates automatically when payment is confirmed. You can pay for 1, 3, 6 or 12 months at once, and renewing early adds the time on top of your current expiry date.
Authentication
Each request includes the account's api_key and signin_key in the request body. You'll find them on the account page in your dashboard.
STK Push
Sends an M-Pesa payment prompt to the customer's phone.
https://mypay.co.ke/api/v1/stkpushRequest
| Field | Type | Description | |
|---|---|---|---|
api_key | string | Required | Your API key |
signin_key | string | Required | Your signin key |
amount | string | Required | Amount in KES, whole number (minimum 1) |
msisdn | string | Required | Customer's phone number, e.g. 0712345678 or 254712345678 |
reference | string | Required | Payment reference / description, e.g. your order number. Returned to you as TransactionReference. |
account_number | string | Optional | Account number for Paybill accounts. If you leave it out, reference is used. Ignored for till and bank accounts. |
Example: cURL
curl -X POST https://mypay.co.ke/api/v1/stkpush \
-H "Content-Type: application/json" \
-d '{
"api_key": "mp_live_xxxxxxxxxxxxxxxx",
"signin_key": "mpsk_xxxxxxxxxxxxxxxxxxxxxxxx",
"amount": "100",
"msisdn": "0712345678",
"reference": "ORDER-001"
}'
Example: PHP
$payload = [
'api_key' => 'mp_live_xxxxxxxxxxxxxxxx',
'signin_key' => 'mpsk_xxxxxxxxxxxxxxxxxxxxxxxx',
'amount' => '100',
'msisdn' => '0712345678',
'reference' => 'ORDER-001',
// 'account_number' => 'INV-2045', // paybill accounts only
];
$ch = curl_init('https://mypay.co.ke/api/v1/stkpush');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($response['success']) {
// Save this to match the callback later
$transactionRequestId = $response['transaction_request_id'];
} else {
echo $response['error_code'] . ': ' . $response['message'];
}
Example: JavaScript (Node.js)
const res = await fetch('https://mypay.co.ke/api/v1/stkpush', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
api_key: process.env.MYPAY_API_KEY,
signin_key: process.env.MYPAY_SIGNIN_KEY,
amount: '100',
msisdn: '0712345678',
reference: 'ORDER-001',
}),
});
const data = await res.json();
Example: Python
import requests
r = requests.post("https://mypay.co.ke/api/v1/stkpush", json={
"api_key": "mp_live_xxxxxxxxxxxxxxxx",
"signin_key": "mpsk_xxxxxxxxxxxxxxxxxxxxxxxx",
"amount": "100",
"msisdn": "0712345678",
"reference": "ORDER-001",
})
print(r.json())
Success response HTTP 200
{
"success": true,
"ResponseCode": 0,
"message": "STK push sent. Customer should enter their M-Pesa PIN.",
"transaction_request_id": "MPY20261010153045123456",
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_123456789",
"amount": 100,
"msisdn": "254712345678",
"reference": "ORDER-001"
}
A success response only means the prompt was sent. Wait for the callback or check the status to know whether the customer paid. Save transaction_request_id, because the callback includes it as TransactionID.
Error response HTTP 4xx / 5xx
{
"success": false,
"error_code": "MP402",
"message": "Account is inactive. Pay the KES 350 monthly subscription to activate it."
}
See all error codes.
Transaction status
Gets the current status of a payment. If a payment is still pending after 30 seconds, MyPay checks with Safaricom directly before it responds.
https://mypay.co.ke/api/v1/statusRequest
| Field | Type | Description | |
|---|---|---|---|
api_key | string | Required | Your API key |
signin_key | string | Required | Your signin key |
transaction_request_id | string | Required | Transaction ID from the STK Push response |
curl -X POST https://mypay.co.ke/api/v1/status \
-H "Content-Type: application/json" \
-d '{
"api_key": "mp_live_xxxxxxxxxxxxxxxx",
"signin_key": "mpsk_xxxxxxxxxxxxxxxxxxxxxxxx",
"transaction_request_id": "MPY20261010153045123456"
}'
Response
{
"success": true,
"status": "success",
"ResponseCode": 0,
"ResponseDescription": "Success",
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_123456789",
"TransactionID": "MPY20261010153045123456",
"TransactionAmount": 100,
"TransactionReceipt": "SHJ7ABCDEF",
"TransactionDate": "20261010153045",
"TransactionReference": "ORDER-001",
"Msisdn": "254712345678"
}
status is one of pending, success or failed. While a payment is pending, ResponseCode is null.
Callbacks (webhooks)
Add a callback URL to each account in your dashboard. Each account must use its own, unique callback URL. When a payment succeeds or fails, MyPay sends a POST with a JSON body to that URL.
Successful payment
{
"ResponseCode": 0,
"ResponseDescription": "Success",
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_123456789",
"TransactionID": "MPY20261010153045123456",
"TransactionAmount": 100,
"TransactionReceipt": "SHJ7ABCDEF",
"TransactionDate": "20261010153045",
"TransactionReference": "ORDER-001",
"Msisdn": "254712345678"
}
Failed or cancelled payment
{
"ResponseCode": 1032,
"ResponseDescription": "Request cancelled by user",
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_123456789",
"TransactionID": "MPY20261010153045123456",
"TransactionAmount": 100,
"TransactionReceipt": null,
"TransactionDate": null,
"TransactionReference": "ORDER-001",
"Msisdn": "254712345678"
}
ResponseCode 0 means paid. Any other value is an M-Pesa result code explaining why the payment failed.
Headers
| Header | Description |
|---|---|
X-MyPay-Event | payment.success, payment.failed or test |
X-MyPay-Signature | HMAC-SHA256 of the raw request body, using the account's webhook secret as the key (hex encoded) |
Delivery & retries
- Respond with any 2xx status within 15 seconds to acknowledge.
- If your endpoint fails, MyPay retries up to 6 times, waiting longer before each retry (1, 2, 4, 8 and 16 minutes).
- You can resend any callback from the dashboard. Use Send test callback to try out your handler.
- The same payment can arrive more than once, so check
TransactionIDand skip payments you've already processed.
Webhook handler sample
<?php
// webhook.php - Handle MyPay webhook notifications
$json = file_get_contents('php://input');
// Optional but recommended: verify the signature
$secret = 'whsec_xxxxxxxxxxxxxxxx'; // account webhook secret from the dashboard
$expected = hash_hmac('sha256', $json, $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_MYPAY_SIGNATURE'] ?? '')) {
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($json, true);
// Validate webhook data
if (!$data || !isset($data['TransactionID'])) {
http_response_code(400);
echo "Invalid webhook data";
exit;
}
$responseCode = $data['ResponseCode'];
$transactionId = $data['TransactionID'];
$amount = $data['TransactionAmount'];
$receipt = $data['TransactionReceipt'];
$phone = $data['Msisdn'];
$reference = $data['TransactionReference'];
if ($responseCode == 0) {
// Payment successful - update your database
processSuccessfulPayment($transactionId, $amount, $receipt, $phone, $reference);
http_response_code(200);
echo json_encode(['status' => 'success']);
} else {
// Payment failed
processFailedPayment($transactionId, $responseCode);
http_response_code(200);
echo json_encode(['status' => 'received']);
}
Payment pages
Don't have a website? Create a payment page for any account in the dashboard and share its link, such as https://mypay.co.ke/pay/mama-mboga. Customers don't need to log in. They enter their phone number (and the amount, if it isn't fixed) and get an M-Pesa prompt.
- Fixed amount: every customer pays the same amount, e.g. a KES 500 ticket.
- Customer enters amount: you can set a minimum amount.
- Till and paybill pages can let the customer type their own reference or account number, e.g. a house number.
- Bank pages always pay to your linked bank account number.
- Page payments are sent to the account's callback URL like any API payment.
MyPay error codes
| Code | HTTP | Meaning |
|---|---|---|
MP400 | 400 | Invalid request body. Send JSON or form data. |
MP401 | 401 | Missing or invalid api_key / signin_key. |
MP402 | 402 | Account is inactive. Pay the KES 350 monthly subscription to activate it. |
MP403 | 403 | Account subscription has expired. Renew to continue receiving payments. |
MP404 | 404 | Transaction not found for this account. |
MP405 | 405 | Method not allowed. Use POST. |
MP410 | 403 | Account is suspended. Contact MyPay support. |
MP420 | 422 | Invalid amount. It must be a whole number of at least 1. |
MP421 | 422 | Invalid msisdn. Use 07XXXXXXXX, 01XXXXXXXX or 2547XXXXXXXX. |
MP422 | 422 | Reference is required. |
MP423 | 422 | transaction_request_id is required. |
MP424 | 422 | Account number is too long (max 40 characters). |
MP500 | 500 | Internal server error. Try again. |
MP502 | 502 | Could not reach Safaricom. Try again shortly. |
MP503 | 502 | Safaricom rejected the STK push request. |
MP429 | 429 | Too many payment requests for this phone number (payment pages). Wait a minute. |
M-Pesa result codes
These come from Safaricom and are passed through as ResponseCode in callbacks and status responses.
| Code | Meaning |
|---|---|
0 | Success. The customer paid. |
1 | Insufficient M-Pesa balance |
1001 | Another transaction is already in progress for this customer |
1019 | Transaction expired |
1025 / 9999 | Error sending the push request |
1032 | Cancelled by the customer |
1037 | Customer's phone could not be reached (offline or timed out) |
2001 | Wrong M-Pesa PIN |