Tutorial

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.

1/3/2024
5 min read
sandboxunified testingMTNAirtelM-PesaFundKittutorial

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?

  1. Sign up for FundKit (free sandbox access)
  2. Get your API key (instant)
  3. Start testing all providers with one API
  4. Deploy with confidence

Get Started Free → View Documentation → See Examples →

Ready to Start Building?

Join thousands of developers building mobile money integrations with FundKit

How to test MTN, Airtel, and M-Pesa in a single sandbox (FundKit for developers) | FundKit Developer Blog