This is the shortest path from zero to a live M-Pesa payment in your system: get an API key, create an STK Push, confirm the result. All four code samples below do the same thing — pick the one closest to your stack.
Before you start
You need three things:
- An account — create one and sign in.
- An approved channel — add a paybill, till or bank channel from the dashboard; its Channel ID is shown in the channels list.
- An API key — create it under Settings & API keys. Keys look like
pb_live_....
Amounts are whole KES integers everywhere in the API: "amount": 150 is KES 150.
1. Take a payment
Every STK request states its channel explicitly — channel_id is required, so a payment can never land on the wrong account.
cURL
curl -X POST https://fortpesa.co.ke/api/v1/payments/stk \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "254712345678",
"amount": 150,
"channel_id": 42,
"reference": "ORDER-1042"
}'
JavaScript
const res = await fetch('https://fortpesa.co.ke/api/v1/payments/stk', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone: '254712345678',
amount: 150,
channel_id: 42,
reference: 'ORDER-1042',
}),
});
const { data } = await res.json();
console.log(data.id, data.status);
Python
import requests
res = requests.post(
"https://fortpesa.co.ke/api/v1/payments/stk",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"phone": "254712345678",
"amount": 150,
"channel_id": 42,
"reference": "ORDER-1042",
},
timeout=30,
)
tx = res.json()["data"]
print(tx["id"], tx["status"])
PHP
$ch = curl_init('https://fortpesa.co.ke/api/v1/payments/stk');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'phone' => '254712345678',
'amount' => 150,
'channel_id' => 42,
'reference' => 'ORDER-1042',
]),
]);
$data = json_decode(curl_exec($ch), true)['data'];
curl_close($ch);
A 201 response means the prompt was dispatched — the customer's phone is ringing with the M-Pesa prompt right now. The response also carries platform_fee, the exact fee for this amount.
{
"data": {
"id": "2f0c6a6e-...",
"status": "pending",
"amount": 150,
"platform_fee": 6,
"reference": "ORDER-1042"
}
}
2. Confirm the payment
pending means the customer has not finished the prompt yet. Two ways to learn the outcome:
Poll the transaction until it turns terminal:
curl https://fortpesa.co.ke/api/v1/transactions/2f0c6a6e-... \
-H "Authorization: Bearer YOUR_API_KEY"
Terminal statuses are success and failed. Prompts typically resolve within seconds; treat five minutes as the outer bound.
Or receive a webhook — push instead of poll. Your endpoint receives an HMAC-signed payload:
import crypto from 'crypto';
// timestamp.raw_body is what gets signed
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.FORTPESA_WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
Verify the signature against the raw body before parsing JSON, then mark the order paid. The full retry schedule and payload shapes are in the webhooks documentation.
3. Errors you might hit
| Code | Meaning | Fix |
|---|---|---|
401 |
Bad or missing key | Check the Authorization header |
402 |
Wallet cannot cover the platform fee | Top up your wallet |
422 |
Validation failed | Missing channel_id, bad phone format or amount ≤ 0 |
503 |
No approved channel available | Approve a channel in the dashboard |
Error bodies follow one envelope everywhere: {"error": {"code": "...", "message": "..."}}.
Where to go next
- Full endpoint reference: API documentation.
- Conceptual guide for non-developers: How to accept M-Pesa payments on your website.
- Pricing you can plan against: What M-Pesa payments cost your business.
Create your account, add a channel, and you can be taking real payments before your coffee cools.