How to test MTN, Airtel, and M-Pesa in a single sandbox (FundKit for developers)
Stop juggling multiple sandbox environments. Learn how to test all major African mobile money providers in one unified sandbox with FundKit.
How to Test MTN, Airtel, and M-Pesa in a Single Sandbox
If you've ever tried to test mobile money integrations across multiple providers, you know the pain. Each provider has their own sandbox environment, different authentication methods, and unique testing requirements. Here's how FundKit solves this with a unified sandbox that works for all providers.
The Multi-Provider Testing Problem
Traditional Approach (The Mess)
MTN Sandbox:
// MTN-specific setup
const mtnClient = new MTNClient({
apiKey: "mtn_sandbox_key",
environment: "sandbox",
baseUrl: "https://sandbox.mtn.com/api",
});
// MTN-specific testing
const mtnPayment = await mtnClient.collect({
amount: 1000,
currency: "UGX",
phone: "+256700000000",
});
Airtel Sandbox:
// Airtel-specific setup
const airtelClient = new AirtelClient({
apiKey: "airtel_sandbox_key",
environment: "sandbox",
baseUrl: "https://sandbox.airtel.com/api",
});
// Airtel-specific testing
const airtelPayment = await airtelClient.collect({
amount: 1000,
currency: "UGX",
phone: "+256700000000",
});
M-Pesa Sandbox:
// M-Pesa-specific setup
const mpesaClient = new MpesaClient({
consumerKey: "mpesa_sandbox_key",
consumerSecret: "mpesa_sandbox_secret",
environment: "sandbox",
baseUrl: "https://sandbox.safaricom.co.ke/api",
});
// M-Pesa-specific testing
const mpesaPayment = await mpesaClient.collect({
amount: 1000,
currency: "KES",
phone: "+254700000000",
});
Problems:
- 3 different APIs to learn
- 3 different authentication methods
- 3 different sandbox environments
- 3 different error formats
- 3 different webhook structures
FundKit Approach (The Solution)
Unified Sandbox:
// Single setup for all providers
const client = new PaymentClient({
apiKey: "sk_test_your_key",
environment: "sandbox",
providers: ["mtn", "airtel", "mpesa"],
});
// Same API for all providers
const payment = await client.collection({
provider: "mtn", // or 'airtel', 'mpesa'
amount: 1000,
currency: "UGX",
accountNumber: "+256700000000",
});
Benefits:
- 1 API to learn
- 1 authentication method
- 1 sandbox environment
- 1 error format
- 1 webhook structure
Step-by-Step Tutorial
Step 1: Set Up Your Environment
# Install FundKit
npm install @fundkit/core
# Set up environment variables
echo "FUNDKIT_API_KEY=sk_test_your_key" >> .env
// Initialize the client
import { PaymentClient } from "@fundkit/core";
const client = new PaymentClient({
apiKey: process.env.FUNDKIT_API_KEY,
environment: "sandbox",
providers: ["mtn", "airtel", "mpesa"],
});
Step 2: Test All Providers with the Same Code
// Test function that works for all providers
async function testProvider(provider, currency, phonePrefix) {
try {
const payment = await client.collection({
provider,
amount: 1000,
currency,
accountNumber: `${phonePrefix}700000000`,
});
console.log(`${provider} test successful:`, payment.id);
return payment;
} catch (error) {
console.error(`${provider} test failed:`, error.message);
return null;
}
}
// Test all providers
const providers = [
{ name: "mtn", currency: "UGX", phonePrefix: "+256" },
{ name: "airtel", currency: "UGX", phonePrefix: "+256" },
{ name: "mpesa", currency: "KES", phonePrefix: "+254" },
];
for (const provider of providers) {
await testProvider(provider.name, provider.currency, provider.phonePrefix);
}
Step 3: Test Different Transaction Types
// Test collections (money coming in)
const collection = await client.collection({
provider: "mtn",
amount: 1000,
currency: "UGX",
accountNumber: "+256700000000",
description: "Test collection",
});
// Test disbursements (money going out)
const disbursement = await client.disbursement({
provider: "airtel",
amount: 2000,
currency: "UGX",
accountNumber: "+256700000001",
description: "Test disbursement",
});
// Test transfers (money between accounts)
const transfer = await client.transfer({
provider: "mpesa",
amount: 1500,
currency: "KES",
fromAccount: "+254700000000",
toAccount: "+254700000001",
description: "Test transfer",
});
Step 4: Test Error Scenarios
// Test error handling for all providers
const errorTests = [
{
name: "Invalid Phone Number",
params: { provider: "mtn", amount: 1000, accountNumber: "invalid" },
expectedError: "INVALID_PHONE",
},
{
name: "Insufficient Balance",
params: {
provider: "airtel",
amount: 999999,
accountNumber: "+256700000000",
},
expectedError: "INSUFFICIENT_BALANCE",
},
{
name: "Invalid Amount",
params: { provider: "mpesa", amount: 0, accountNumber: "+254700000000" },
expectedError: "INVALID_AMOUNT",
},
];
for (const test of errorTests) {
try {
await client.collection(test.params);
console.log(`❌ ${test.name}: Expected error but got success`);
} catch (error) {
if (error.code === test.expectedError) {
console.log(`✅ ${test.name}: Got expected error ${error.code}`);
} else {
console.log(
`⚠️ ${test.name}: Expected ${test.expectedError}, got ${error.code}`
);
}
}
}
Step 5: Test Webhook Handling
// Set up webhook endpoint
app.post("/webhook", (req, res) => {
const { event, transactionId, provider, status } = req.body;
console.log(
`Webhook received: ${event} for ${provider} transaction ${transactionId}`
);
// Handle different events
switch (event) {
case "payment.initiated":
console.log("Payment started");
break;
case "payment.completed":
console.log("Payment successful");
// Update your database
updateTransactionStatus(transactionId, "completed");
break;
case "payment.failed":
console.log("Payment failed");
// Handle failure
handlePaymentFailure(transactionId, req.body.error);
break;
}
res.status(200).send("OK");
});
// Test webhook with different providers
const webhookTests = [
{ provider: "mtn", amount: 1000 },
{ provider: "airtel", amount: 2000 },
{ provider: "mpesa", amount: 1500 },
];
for (const test of webhookTests) {
const payment = await client.collection({
provider: test.provider,
amount: test.amount,
currency: "UGX",
accountNumber: "+256700000000",
});
console.log(`Testing webhook for ${test.provider}: ${payment.id}`);
}
Advanced Testing Scenarios
1. Concurrent Testing
// Test multiple providers simultaneously
const concurrentTests = async () => {
const promises = [
client.collection({
provider: "mtn",
amount: 1000,
currency: "UGX",
accountNumber: "+256700000001",
}),
client.collection({
provider: "airtel",
amount: 2000,
currency: "UGX",
accountNumber: "+256700000002",
}),
client.collection({
provider: "mpesa",
amount: 1500,
currency: "KES",
accountNumber: "+254700000000",
}),
];
const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
const provider = ["mtn", "airtel", "mpesa"][index];
if (result.status === "fulfilled") {
console.log(`✅ ${provider}: ${result.value.id}`);
} else {
console.log(`❌ ${provider}: ${result.reason.message}`);
}
});
};
await concurrentTests();
2. Performance Testing
// Test high-volume scenarios
const performanceTest = async () => {
const startTime = Date.now();
const promises = [];
// Create 100 concurrent payments across all providers
for (let i = 0; i < 100; i++) {
const provider = ["mtn", "airtel", "mpesa"][i % 3];
promises.push(
client.collection({
provider,
amount: 1000,
currency: "UGX",
accountNumber: `+25670000000${i}`,
})
);
}
const results = await Promise.allSettled(promises);
const endTime = Date.now();
const successful = results.filter((r) => r.status === "fulfilled").length;
const failed = results.filter((r) => r.status === "rejected").length;
console.log(`Performance Test Results:`);
console.log(`- Total: ${results.length} payments`);
console.log(`- Successful: ${successful}`);
console.log(`- Failed: ${failed}`);
console.log(`- Time: ${endTime - startTime}ms`);
console.log(
`- Rate: ${results.length / ((endTime - startTime) / 1000)} payments/sec`
);
};
await performanceTest();
3. Provider-Specific Testing
// Test provider-specific features
const providerSpecificTests = async () => {
// Test MTN-specific features
const mtnPayment = await client.collection({
provider: "mtn",
amount: 1000,
currency: "UGX",
accountNumber: "+256700000000",
// MTN-specific parameters
merchantId: "test_merchant",
callbackUrl: "https://your-app.com/callback",
});
// Test Airtel-specific features
const airtelPayment = await client.collection({
provider: "airtel",
amount: 2000,
currency: "UGX",
accountNumber: "+256700000000",
// Airtel-specific parameters
reference: "test_ref_123",
description: "Test payment",
});
// Test M-Pesa-specific features
const mpesaPayment = await client.collection({
provider: "mpesa",
amount: 1500,
currency: "KES",
accountNumber: "+254700000000",
// M-Pesa-specific parameters
businessShortCode: "174379",
accountReference: "test_account",
});
console.log("Provider-specific tests completed");
};
Testing Best Practices
1. Use Environment Variables
// .env file
FUNDKIT_API_KEY = sk_test_your_key;
FUNDKIT_ENVIRONMENT = sandbox;
// In your code
const client = new PaymentClient({
apiKey: process.env.FUNDKIT_API_KEY,
environment: process.env.FUNDKIT_ENVIRONMENT,
providers: ["mtn", "airtel", "mpesa"],
});
2. Test All Error Cases
// Comprehensive error testing
const testAllErrors = async () => {
const errorScenarios = [
{ provider: "mtn", amount: 0, expected: "INVALID_AMOUNT" },
{ provider: "airtel", accountNumber: "invalid", expected: "INVALID_PHONE" },
{ provider: "mpesa", amount: 999999999, expected: "INSUFFICIENT_BALANCE" },
];
for (const scenario of errorScenarios) {
try {
await client.collection(scenario);
} catch (error) {
if (error.code === scenario.expected) {
console.log(`✅ ${scenario.provider}: ${error.code}`);
} else {
console.log(
`❌ ${scenario.provider}: Expected ${scenario.expected}, got ${error.code}`
);
}
}
}
};
3. Monitor Webhook Delivery
// Webhook monitoring
const webhookMonitor = {
received: [],
log(event, data) {
this.received.push({ event, data, timestamp: new Date() });
console.log(`Webhook: ${event} at ${new Date().toISOString()}`);
},
getStats() {
const events = this.received.map((w) => w.event);
const counts = events.reduce((acc, event) => {
acc[event] = (acc[event] || 0) + 1;
return acc;
}, {});
return counts;
},
};
// Use in webhook handler
app.post("/webhook", (req, res) => {
webhookMonitor.log(req.body.event, req.body);
res.status(200).send("OK");
});
Production Readiness
1. Switch to Production
// Production configuration
const productionClient = new PaymentClient({
apiKey: "sk_live_your_key",
environment: "production",
providers: {
mtn: { apiKey: "your_mtn_production_key" },
airtel: { apiKey: "your_airtel_production_key" },
mpesa: {
consumerKey: "your_mpesa_consumer_key",
consumerSecret: "your_mpesa_consumer_secret",
},
},
});
2. Add Monitoring
// Production monitoring
const monitorPayment = async (payment) => {
console.log("Payment processed:", {
id: payment.id,
provider: payment.provider,
amount: payment.amount,
status: payment.status,
timestamp: new Date().toISOString(),
});
// Send to monitoring service
await sendToMonitoring({
event: "payment.processed",
data: payment,
});
};
Conclusion
Testing multiple mobile money providers doesn't have to be complicated. With FundKit's unified sandbox:
- Test all providers with the same API
- Use consistent error handling across providers
- Simulate real-world scenarios safely
- Deploy with confidence knowing your integration works
The key is choosing a solution that abstracts away the complexity, so you can focus on building your product instead of managing multiple provider integrations.
Next Steps
Ready to test all providers in one sandbox?
- Sign up for FundKit (free sandbox access)
- Get your API key (instant)
- Start testing all providers with one API
- Deploy with confidence