Technical

Why mobile money APIs don't have testnets (and how FundKit gives you one)

The fundamental problem with mobile money testing and why traditional testnets don't work. How FundKit's virtual mobile money network solves this.

1/5/2024
9 min read
testnetsandboxvirtual mobile moneytestingFundKit

Why Mobile Money APIs Don't Have Testnets

If you've ever tried to test mobile money integrations, you know the frustration. Unlike cryptocurrencies with testnets or traditional APIs with sandboxes, mobile money providers offer limited—or no—testing environments. Here's why, and how FundKit solves this fundamental problem.

The Mobile Money Testing Problem

Why Traditional Testnets Don't Work

Cryptocurrency testnets work because:

  • Virtual tokens have no real value
  • Network effects are simulated
  • Consensus mechanisms can be simplified
  • No regulatory oversight required

Mobile money is different because:

  • Real money is always involved
  • Regulatory compliance is mandatory
  • Banking relationships are required
  • User verification is essential

The Provider's Dilemma

Mobile money providers face a fundamental conflict:

They need to:

  • Comply with regulations (AML, KYC, financial oversight)
  • Protect real money and user accounts
  • Maintain banking relationships with strict requirements
  • Prevent fraud and money laundering

But developers need:

  • Safe testing environments without real money
  • Realistic simulation of user flows
  • Error handling for edge cases
  • Performance testing under load

Result: No good testing solution exists

Current Testing Approaches (And Why They Fail)

1. Provider Sandboxes (Limited & Restrictive)

What they offer:

  • Basic API testing with mock responses
  • Limited transaction types (usually just collections)
  • No real user flows or prompts
  • Restricted access (compliance required)

Problems:

// Provider sandbox - unrealistic
const response = await mtnSandbox.collect({
  amount: 1000,
  phone: "+256700000000",
});

// Returns: { success: true, transactionId: 'mock_123' }
// Reality: No actual mobile money prompts, no user interaction

2. Real Money Testing (Expensive & Risky)

What it involves:

  • Real transactions with actual money
  • Real user accounts and phone numbers
  • Compliance requirements for test accounts
  • Limited test data per provider

Problems:

  • Cost: $100-500 per test session
  • Risk: Real money transactions
  • Compliance: Full regulatory requirements
  • Scale: Can't test high-volume scenarios

3. Mock Services (Unrealistic & Incomplete)

What they provide:

  • HTTP mocks that return expected responses
  • No business logic or validation
  • No user interaction simulation
  • No error scenarios testing

Problems:

// Mock service - too simple
const mockResponse = {
  success: true,
  transactionId: "test_123",
  status: "completed",
};

// Reality: No validation, no user prompts, no edge cases

The FundKit Solution: Virtual Mobile Money Network

How It Works

FundKit creates a virtual mobile money network that simulates real provider behavior without real money:

// Virtual mobile money testing
const client = new PaymentClient({
  apiKey: "sk_test_your_key",
  environment: "sandbox",
  providers: ["virtual_mtn", "virtual_airtel", "virtual_mpesa"],
});

// Test with virtual providers
const payment = await client.collection({
  provider: "virtual_mtn",
  amount: 5000,
  currency: "UGX",
  accountNumber: "+256700000000",
});

What Makes It Different

1. Realistic User Flows

  • Actual mobile money prompts (simulated)
  • User interaction simulation
  • Real-world scenarios (insufficient balance, network issues)
  • Provider-specific behavior patterns

2. Complete Business Logic

  • Validation rules (phone number format, amount limits)
  • Error handling (all real-world error cases)
  • Transaction states (pending, completed, failed, reversed)
  • Webhook simulation (real-time notifications)

3. Comprehensive Testing

  • All transaction types (collections, disbursements, transfers)
  • Edge cases (network timeouts, invalid data, rate limits)
  • Performance testing (high-volume scenarios)
  • Integration testing (webhook handling, error recovery)

Technical Implementation

Virtual Provider Architecture

// Virtual provider implementation
class VirtualMTNProvider {
  async collect(params) {
    // Simulate real MTN validation
    if (!this.validatePhoneNumber(params.accountNumber)) {
      throw new Error("INVALID_PHONE_NUMBER");
    }

    if (params.amount > this.getMaxAmount()) {
      throw new Error("AMOUNT_EXCEEDS_LIMIT");
    }

    // Simulate user interaction
    const userResponse = await this.simulateUserPrompt({
      provider: "MTN",
      amount: params.amount,
      phone: params.accountNumber,
      message: "Confirm payment of UGX 5,000 to +256700000000?",
    });

    if (!userResponse.confirmed) {
      throw new Error("USER_CANCELLED");
    }

    // Simulate transaction processing
    const transaction = await this.processTransaction({
      ...params,
      status: "pending",
      timestamp: new Date(),
    });

    // Simulate webhook notification
    await this.sendWebhook({
      event: "payment.initiated",
      transactionId: transaction.id,
      status: "pending",
    });

    return transaction;
  }

  async simulateUserPrompt(prompt) {
    // Simulate real mobile money user experience
    return {
      confirmed: Math.random() > 0.1, // 90% success rate
      responseTime: Math.random() * 5000 + 1000, // 1-6 seconds
      userAction: "confirmed", // or 'cancelled', 'timeout'
    };
  }
}

Realistic Error Simulation

// Comprehensive error testing
const errorScenarios = [
  {
    name: "Insufficient Balance",
    condition: (amount) => amount > 10000,
    error: { code: "INSUFFICIENT_BALANCE", message: "Account balance too low" },
  },
  {
    name: "Invalid Phone Number",
    condition: (phone) => !phone.startsWith("+256"),
    error: { code: "INVALID_PHONE", message: "Invalid phone number format" },
  },
  {
    name: "Network Timeout",
    condition: () => Math.random() < 0.05, // 5% chance
    error: { code: "NETWORK_TIMEOUT", message: "Request timed out" },
  },
  {
    name: "Provider Maintenance",
    condition: () => Math.random() < 0.01, // 1% chance
    error: {
      code: "SERVICE_UNAVAILABLE",
      message: "Provider under maintenance",
    },
  },
];

Webhook Simulation

// Real-time webhook testing
class WebhookSimulator {
  async simulatePaymentFlow(transaction) {
    // Simulate payment initiation
    await this.sendWebhook({
      event: "payment.initiated",
      transactionId: transaction.id,
      status: "pending",
      timestamp: new Date(),
    });

    // Simulate processing delay
    await this.delay(2000 + Math.random() * 3000); // 2-5 seconds

    // Simulate completion or failure
    const success = Math.random() > 0.1; // 90% success rate

    if (success) {
      await this.sendWebhook({
        event: "payment.completed",
        transactionId: transaction.id,
        status: "completed",
        timestamp: new Date(),
      });
    } else {
      await this.sendWebhook({
        event: "payment.failed",
        transactionId: transaction.id,
        status: "failed",
        error: "USER_CANCELLED",
        timestamp: new Date(),
      });
    }
  }
}

Testing Scenarios

1. Basic Transaction Testing

// Test successful collection
const payment = await client.collection({
  provider: "virtual_mtn",
  amount: 1000,
  currency: "UGX",
  accountNumber: "+256700000000",
});

console.log("Payment:", payment);
// Output: { id: 'txn_123', status: 'completed', amount: 1000, ... }

2. Error Handling Testing

// Test insufficient balance
try {
  await client.collection({
    provider: "virtual_mtn",
    amount: 50000, // Large amount
    currency: "UGX",
    accountNumber: "+256700000000",
  });
} catch (error) {
  console.log("Expected error:", error.code); // INSUFFICIENT_BALANCE
}

3. Webhook Testing

// Test webhook handling
app.post("/webhook", (req, res) => {
  const { event, transactionId, status } = req.body;

  console.log(`Received webhook: ${event} for ${transactionId}`);

  if (event === "payment.completed") {
    // Update your database
    updateTransactionStatus(transactionId, "completed");
  }

  res.status(200).send("OK");
});

4. Performance Testing

// Test high-volume scenarios
const concurrentPayments = Array.from({ length: 100 }, (_, i) =>
  client.collection({
    provider: "virtual_mtn",
    amount: 1000,
    currency: "UGX",
    accountNumber: `+25670000000${i}`,
  })
);

const results = await Promise.allSettled(concurrentPayments);
console.log(`Processed ${results.length} payments`);

Benefits of Virtual Mobile Money

1. Realistic Testing

  • Actual user flows and prompts
  • Real-world error scenarios
  • Provider-specific behavior
  • Complete transaction lifecycle

2. Cost Effective

  • No real money required
  • Unlimited testing scenarios
  • No compliance requirements
  • Instant setup

3. Comprehensive Coverage

  • All transaction types supported
  • All error cases covered
  • Performance testing enabled
  • Integration testing included

4. Developer Friendly

  • Simple API for testing
  • Detailed logging and debugging
  • Webhook simulation included
  • Documentation and examples

Comparison: Traditional vs Virtual Testing

Traditional Testing

// Limited sandbox testing
const response = await mtnSandbox.collect({
  amount: 1000,
  phone: "+256700000000",
});
// Returns: { success: true, id: 'mock_123' }
// Problems: No user interaction, no error cases, no webhooks

Virtual Mobile Money Testing

// Comprehensive virtual testing
const client = new PaymentClient({
  apiKey: "sk_test_your_key",
  environment: "sandbox",
  providers: ["virtual_mtn", "virtual_airtel", "virtual_mpesa"],
});

const payment = await client.collection({
  provider: "virtual_mtn",
  amount: 1000,
  currency: "UGX",
  accountNumber: "+256700000000",
});
// Returns: Real transaction with full lifecycle simulation
// Benefits: User interaction, error cases, webhooks, performance testing

Getting Started with Virtual Testing

1. Set Up Your Environment

npm install @fundkit/core
const client = new PaymentClient({
  apiKey: "sk_test_your_key",
  environment: "sandbox",
  providers: ["virtual_mtn", "virtual_airtel", "virtual_mpesa"],
});

2. Test Basic Flows

// Test successful payment
const payment = await client.collection({
  provider: "virtual_mtn",
  amount: 1000,
  currency: "UGX",
  accountNumber: "+256700000000",
});

3. Test Error Scenarios

// Test various error cases
const errorTests = [
  { amount: 50000, expectedError: "INSUFFICIENT_BALANCE" },
  { phone: "invalid", expectedError: "INVALID_PHONE" },
  { amount: 0, expectedError: "INVALID_AMOUNT" },
];

for (const test of errorTests) {
  try {
    await client.collection({
      provider: "virtual_mtn",
      amount: test.amount,
      currency: "UGX",
      accountNumber: test.phone || "+256700000000",
    });
  } catch (error) {
    console.log(`Expected ${test.expectedError}, got ${error.code}`);
  }
}

4. Test Webhooks

// Set up webhook endpoint
app.post("/webhook", (req, res) => {
  const { event, transactionId, status } = req.body;
  console.log(`Webhook: ${event} for ${transactionId}`);
  res.status(200).send("OK");
});

Conclusion

Mobile money APIs don't have testnets because they deal with real money and regulatory requirements. But that doesn't mean you can't test effectively.

FundKit's virtual mobile money network provides:

  • Realistic testing without real money
  • Complete transaction simulation including user interaction
  • Comprehensive error handling for all edge cases
  • Performance testing capabilities
  • Webhook simulation for integration testing

The result: Confident deployment knowing your integration works in all scenarios.

Next Steps

Ready to test mobile money integrations properly?

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

Get Started Free → View Documentation →

Ready to Start Building?

Join thousands of developers building mobile money integrations with FundKit

Why mobile money APIs don't have testnets (and how FundKit gives you one) | FundKit Developer Blog