To integrate Airtel Money in Kenya, you have two options. Option one: use the official Airtel Africa API directly. Register at developers.airtel.africa, get your client_id and client_secret, generate an OAuth token, then call the Collection API endpoint to trigger a payment prompt on the customer's phone. Option two: use Paystack as an aggregator. Initialize a charge with the mobile_money object, set the provider to "atl" and currency to "KES", and Paystack handles the rest. The direct API gives you more control. Paystack is faster to set up and handles multiple payment methods from one integration.
M-Pesa dominates Kenya's mobile money market, but Airtel Money is the second-largest player with millions of active subscribers. If your app only accepts M-Pesa, you are telling every Airtel Money user to go find an M-Pesa agent first. That is friction. And friction kills conversions.
Airtel Money has been growing steadily in Kenya, especially in price-sensitive segments where Airtel's cheaper data and call rates attract subscribers. Many users keep both an M-Pesa and an Airtel Money account. Some use Airtel Money as their primary wallet. Ignoring them means leaving money on the table.
The good news: integrating Airtel Money is straightforward. The Airtel Africa API follows modern REST conventions with OAuth 2.0 authentication and JSON payloads. If you have integrated M-Pesa's Daraja API before, the Airtel Money API will feel familiar. And if you use Paystack, you can add Airtel Money support with about ten lines of code.
This tutorial covers both approaches:
All code examples use Node.js with the built-in fetch API (Node 18+). The concepts apply to any language that can make HTTP requests.
The Airtel Africa API is the official developer interface for Airtel Money across 14 African countries, including Kenya. It supports collections (receiving payments), disbursements (sending money), and transaction status enquiries. The API is managed through the Airtel developer portal at developers.airtel.africa.
Here is the full flow you will implement:
client_id and client_secretLet us walk through each step.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Go to developers.airtel.africa and sign up for a developer account. You will need a valid email address and basic business information.
Once logged in, create a new application:
After creating the application, the portal gives you two credentials:
Store these in a .env file. Never commit them to version control:
AIRTEL_CLIENT_ID=your_client_id_here
AIRTEL_CLIENT_SECRET=your_client_secret_here
AIRTEL_ENV=sandbox
For Kenya, you also need to note two values you will use in every API request header:
X-Country: KEX-Currency: KESAirtel requires KYC (Know Your Customer) verification before you can access the production environment. For sandbox testing, your developer account is enough. The KYC process involves submitting business registration documents and can take a few days. Start this early if you know you will need production access soon.
Every API call to Airtel Money requires a Bearer token in the Authorization header. You get this token by sending your client credentials to the OAuth endpoint.
The base URLs are:
https://openapiuat.airtel.africahttps://openapi.airtel.africaHere is how to generate a token in Node.js:
async function getAirtelToken() {
const baseUrl = process.env.AIRTEL_ENV === 'production'
? 'https://openapi.airtel.africa'
: 'https://openapiuat.airtel.africa';
const response = await fetch(`${baseUrl}/auth/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: process.env.AIRTEL_CLIENT_ID,
client_secret: process.env.AIRTEL_CLIENT_SECRET,
grant_type: 'client_credentials',
}),
});
const data = await response.json();
console.log('Access Token:', data.access_token);
console.log('Expires in:', data.expires_in, 'seconds');
return data.access_token;
}
A successful response looks like this:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600,
"token_type": "bearer"
}
The token expires after 3600 seconds (one hour). In a production application, cache the token and refresh it before it expires. A simple approach: store the token and its expiry timestamp, then check before each API call whether you need a fresh token.
If you get a 401 or 400 error, check these common issues:
client_credentialsThe Collection API is what you use to request money from a customer. When you call it, Airtel sends a USSD push to the customer's phone asking them to enter their Airtel Money PIN to confirm the payment. This is similar to how M-Pesa's STK Push works.
The endpoint is POST /merchant/v2/payments/ on the base URL.
Here is a complete payment request in Node.js:
async function requestPayment(accessToken, phoneNumber, amount, reference) {
const baseUrl = process.env.AIRTEL_ENV === 'production'
? 'https://openapi.airtel.africa'
: 'https://openapiuat.airtel.africa';
const response = await fetch(`${baseUrl}/merchant/v2/payments/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'X-Country': 'KE',
'X-Currency': 'KES',
},
body: JSON.stringify({
reference: reference,
subscriber: {
country: 'KE',
currency: 'KES',
msisdn: phoneNumber,
},
transaction: {
amount: amount,
country: 'KE',
currency: 'KES',
id: reference,
},
}),
});
const data = await response.json();
console.log('Payment Response:', data);
return data;
}
// Usage
const token = await getAirtelToken();
const result = await requestPayment(
token,
'7XXXXXXXX', // Customer phone number without country code
500, // Amount in KES
'ORDER-12345' // Your unique reference
);
Important details about the request:
712345678.KE and KES.A successful response means Airtel received your request and will send the USSD push to the customer:
{
"data": {
"transaction": {
"id": "ABC123456789",
"status": "SUCCESS"
}
},
"status": {
"code": "200",
"message": "SUCCESS",
"result_code": "ESB000010",
"success": true
}
}
Like M-Pesa's STK Push, this response only confirms that Airtel accepted your request. It does not mean the customer has paid. The customer still needs to enter their PIN on their phone. The actual payment result comes through the callback.
After the customer confirms (or rejects) the payment on their phone, Airtel sends a POST request to the callback URL you configured in your application on the developer portal. This is where you find out whether the payment actually went through.
Set up a callback endpoint in your Express app:
const express = require('express');
const app = express();
app.use(express.json());
app.post('/airtel/callback', (req, res) => {
const payload = req.body;
console.log('Airtel Callback Received:', JSON.stringify(payload, null, 2));
const transactionId = payload.transaction?.id;
const status = payload.transaction?.status_code;
if (status === 'TS') {
// Payment successful
console.log('Payment confirmed for transaction:', transactionId);
// Update your database, fulfill the order, send confirmation
} else if (status === 'TF') {
// Payment failed
console.log('Payment failed for transaction:', transactionId);
// Notify the customer, allow retry
} else {
// Other status (TA = ambiguous, etc.)
console.log('Payment status:', status, 'for transaction:', transactionId);
}
// Always respond with 200 to acknowledge receipt
res.status(200).json({ status: 'received' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
The key status codes in the callback are:
During development, your localhost is not reachable from Airtel's servers. Use a tunneling tool like ngrok to expose your local server:
npx ngrok http 3000
This gives you a public URL like https://abc123.ngrok.io. Use https://abc123.ngrok.io/airtel/callback as your callback URL in the Airtel developer portal. If you have worked with M-Pesa callbacks before, this is the exact same concept. If your callbacks are not arriving, our guide on callback URLs not firing covers the common causes.
Sometimes callbacks do not arrive (network issues, server downtime, misconfigured URLs). The Transaction Status API lets you check the status of any payment by its transaction ID.
async function checkTransactionStatus(accessToken, transactionId) {
const baseUrl = process.env.AIRTEL_ENV === 'production'
? 'https://openapi.airtel.africa'
: 'https://openapiuat.airtel.africa';
const response = await fetch(
`${baseUrl}/standard/v1/payments/${transactionId}`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Country': 'KE',
'X-Currency': 'KES',
},
}
);
const data = await response.json();
console.log('Transaction Status:', data);
return data;
}
The response includes the transaction status and amount:
{
"data": {
"transaction": {
"airtel_money_id": "CI260720.1234.X12345",
"id": "ORDER-12345",
"status": "TS",
"message": "Transaction Successful"
}
},
"status": {
"code": "200",
"message": "SUCCESS",
"success": true
}
}
Use this endpoint as a fallback, not as your primary notification method. Polling the status API repeatedly is inefficient and can get your application rate-limited. The recommended pattern is: rely on callbacks as your primary notification, and use the status API only when a callback does not arrive within a reasonable timeframe (for example, after 2 to 3 minutes).
If you already use Paystack for card payments or M-Pesa in Kenya, adding Airtel Money takes minimal effort. Paystack supports Airtel Money through their Charge API with the provider code atl. This means one integration, one dashboard, and one webhook endpoint for all your payment methods.
Paystack's Airtel Money support is available to businesses registered in Kenya (and Ghana). You need a Paystack account with KES as a supported currency.
There are two ways to accept Airtel Money through Paystack:
Let us cover both.
The simplest approach is to initialize a transaction and let Paystack's checkout page show the Airtel Money option to your customer. You specify which payment channels to display.
async function initializePaystackTransaction(email, amount) {
const response = await fetch(
'https://api.paystack.co/transaction/initialize',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: amount * 100, // Paystack uses lowest currency unit (cents)
currency: 'KES',
channels: ['mobile_money'], // Only show mobile money options
callback_url: 'https://yoursite.com/payment/callback',
metadata: {
order_id: 'ORDER-12345',
custom_fields: [
{
display_name: 'Order Number',
variable_name: 'order_number',
value: 'ORDER-12345',
},
],
},
}),
}
);
const data = await response.json();
console.log('Checkout URL:', data.data.authorization_url);
console.log('Reference:', data.data.reference);
return data.data;
}
// Usage: KES 500 payment
const checkout = await initializePaystackTransaction(
'customer@example.com',
500
);
The response gives you a checkout URL:
{
"status": true,
"message": "Authorization URL created",
"data": {
"authorization_url": "https://checkout.paystack.com/nkdks46nymizns7",
"access_code": "nkdks46nymizns7",
"reference": "nms6uvr1pl"
}
}
Redirect your customer to the authorization_url. Paystack's checkout page will display available mobile money providers for Kenya (M-Pesa and Airtel Money). The customer selects Airtel Money, enters their phone number, and confirms the payment on their phone.
If you want customers to see all available payment methods (cards, M-Pesa, and Airtel Money), remove the channels parameter entirely. Paystack will show everything that is enabled for your account.
Note the amount field: Paystack expects the amount in the smallest currency unit. For KES, that means cents. So KES 500 becomes 50000. This is a common source of bugs. If your customers are being charged 100x the expected amount, check your amount conversion.
For full control over the payment flow (your own UI, no redirects to Paystack's checkout page), use the Charge API directly. This lets you collect the phone number in your own form and charge Airtel Money from your backend.
async function chargeAirtelMoney(email, amount, phoneNumber) {
const response = await fetch('https://api.paystack.co/charge', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: amount * 100, // Convert to cents
currency: 'KES',
mobile_money: {
phone: phoneNumber, // e.g., '0738123456'
provider: 'atl', // 'atl' = Airtel Money
},
}),
});
const data = await response.json();
console.log('Charge Response:', data);
return data;
}
// Usage
const charge = await chargeAirtelMoney(
'customer@example.com',
500,
'0738123456'
);
The response tells you the charge is pending customer authorization:
{
"status": true,
"message": "Charge attempted",
"data": {
"reference": "abc123def456",
"status": "pay_offline",
"display_text": "Please complete the payment on your mobile phone"
}
}
The pay_offline status means the customer has received a prompt on their phone and needs to enter their Airtel Money PIN to confirm. They have 180 seconds (3 minutes) to complete the authorization.
Key details about the Charge API:
atl for Airtel Money. Other options include mpesa for M-Pesa in Kenya.0738123456).After the customer completes (or fails) the payment, Paystack notifies your server through a webhook. You must set up a webhook URL in your Paystack dashboard under Settings > API Keys & Webhooks.
Here is a webhook handler in Express:
const crypto = require('crypto');
app.post('/paystack/webhook', (req, res) => {
// Verify the webhook signature
const hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(JSON.stringify(req.body))
.digest('hex');
if (hash !== req.headers['x-paystack-signature']) {
console.log('Invalid webhook signature');
return res.status(401).send('Unauthorized');
}
const event = req.body;
if (event.event === 'charge.success') {
const { reference, amount, currency, channel } = event.data;
console.log('Payment successful:', {
reference,
amount: amount / 100, // Convert back from cents
currency,
channel, // Will be 'mobile_money' for Airtel Money
});
// Verify the amount matches what you expected
// Update your database, fulfill the order
}
// Always respond with 200 immediately
res.status(200).send('OK');
});
Always verify the webhook signature using the x-paystack-signature header. This proves the webhook came from Paystack and was not forged by an attacker. The signature is an HMAC SHA512 hash of the raw request body, computed with your Paystack secret key.
As an additional safety measure, verify the transaction through the API after receiving a webhook:
async function verifyPaystackTransaction(reference) {
const response = await fetch(
`https://api.paystack.co/transaction/verify/${reference}`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
const data = await response.json();
if (data.data.status === 'success') {
// Payment confirmed
const amountPaid = data.data.amount / 100;
console.log('Verified payment:', amountPaid, data.data.currency);
return true;
}
return false;
}
If the customer does not complete the payment within 180 seconds, the charge fails. You will not receive a webhook for failed mobile money charges, so use the Verify Transaction API to check the status if no webhook arrives after 3 minutes.
Both approaches work. The right choice depends on your situation:
Choose the direct Airtel Africa API if:
Choose Paystack if:
For most Kenyan startups and small businesses, Paystack is the pragmatic choice. The transaction fees (1.5% for mobile money in Kenya) are reasonable, and the time saved on integration, reconciliation, and compliance is significant. If you are processing high volumes and every basis point matters, the direct API makes more financial sense.
Nothing stops you from using both. Some businesses use the direct Airtel API for high-volume automated payments and Paystack for their customer-facing checkout. The important thing is understanding how both work so you can make an informed decision.
Whether you use the direct API or Paystack, run through this checklist before accepting real payments:
For the direct Airtel Africa API:
openapiuat.airtel.africa to openapi.airtel.africa.client_id and client_secret.For Paystack:
For both approaches:
If you want to build production-ready payment integrations with proper error handling, retry logic, and reconciliation patterns, our Full-Stack Software and AI Engineering program (KES 120,000) covers payment integrations for the Kenyan market. You ship real projects that accept real money before you graduate. For a focused deep dive on mobile money, the M-Pesa Integration for Developers course (KES 9,999) covers both M-Pesa and Airtel Money patterns.
Here is a minimal but complete Express.js server that accepts Airtel Money payments through both methods. Use this as a starting point and build on it.
require('dotenv').config();
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// ---- Airtel Money Direct API ----
let airtelToken = null;
let airtelTokenExpiry = 0;
async function getAirtelToken() {
if (airtelToken && Date.now() < airtelTokenExpiry) {
return airtelToken;
}
const baseUrl = process.env.AIRTEL_ENV === 'production'
? 'https://openapi.airtel.africa'
: 'https://openapiuat.airtel.africa';
const response = await fetch(`${baseUrl}/auth/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.AIRTEL_CLIENT_ID,
client_secret: process.env.AIRTEL_CLIENT_SECRET,
grant_type: 'client_credentials',
}),
});
const data = await response.json();
airtelToken = data.access_token;
airtelTokenExpiry = Date.now() + (data.expires_in - 60) * 1000;
return airtelToken;
}
app.post('/pay/airtel-direct', async (req, res) => {
const { phone, amount, orderId } = req.body;
const token = await getAirtelToken();
const baseUrl = process.env.AIRTEL_ENV === 'production'
? 'https://openapi.airtel.africa'
: 'https://openapiuat.airtel.africa';
const response = await fetch(`${baseUrl}/merchant/v2/payments/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'X-Country': 'KE',
'X-Currency': 'KES',
},
body: JSON.stringify({
reference: orderId,
subscriber: { country: 'KE', currency: 'KES', msisdn: phone },
transaction: { amount, country: 'KE', currency: 'KES', id: orderId },
}),
});
const data = await response.json();
res.json(data);
});
app.post('/airtel/callback', (req, res) => {
const { transaction } = req.body;
if (transaction?.status_code === 'TS') {
// Payment successful - update your database
console.log('Airtel payment confirmed:', transaction.id);
}
res.status(200).json({ status: 'received' });
});
// ---- Paystack Airtel Money ----
app.post('/pay/paystack-airtel', async (req, res) => {
const { email, phone, amount } = req.body;
const response = await fetch('https://api.paystack.co/charge', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
amount: amount * 100,
currency: 'KES',
mobile_money: { phone, provider: 'atl' },
}),
});
const data = await response.json();
res.json(data);
});
app.post('/paystack/webhook', (req, res) => {
const hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(JSON.stringify(req.body))
.digest('hex');
if (hash !== req.headers['x-paystack-signature']) {
return res.status(401).send('Unauthorized');
}
if (req.body.event === 'charge.success') {
const { reference, amount } = req.body.data;
console.log('Paystack payment confirmed:', reference, amount / 100, 'KES');
// Update your database
}
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Payment server running on port 3000'));
This example handles token caching, both payment methods, callback verification, and webhook signature validation. In a real application, you would add input validation, database operations, proper error responses, and logging. But this gives you a working foundation to build on.
You now know how to accept Airtel Money payments in Kenya through both the direct API and Paystack. Here is where to go from here:
mpesa as another provider option. For direct integration, check our Daraja API tutorial for beginners./standard/v2/disbursements/ endpoint.Mobile money integration is one of the most marketable skills for a developer in Kenya. Every business that serves Kenyan customers needs this. If you want structured training with real projects, check out the courses at McTaba Academy. The M-Pesa Integration for Developers course (KES 9,999) is the fastest way to build production-ready payment skills for the Kenyan market.
KES 9,999
Master Daraja API, STK Push, C2B, B2C, and callback handling. Build a real payment flow from scratch. 10 modules, 57 lessons.
Enrol in M-Pesa Integration