Overview
EasyPay is Uganda’s premier mobile money payment processor, offering fast and reliable payments through MTN and Airtel networks. Known for their exceptional speed and webhook reliability, EasyPay is perfect for businesses that need instant payment confirmation.
EasyPay specializes in rapid payment processing with average transaction completion times under 30
seconds, making it ideal for real-time applications and instant service delivery.
Coverage & Capabilities
Supported Networks
MTN Mobile Money Coverage: Uganda Currency: UGX Features: Collections, Status Check, Webhooks
Airtel Money Coverage: Uganda Currency: UGX Features: Collections, Status Check, Webhooks
Capabilities
Fast Collections - Average completion time < 30 seconds
Real-time Webhooks - Instant status notifications
Status Checking - Query transaction status anytime
Bulk Processing - Handle multiple transactions efficiently
High Success Rate - 98%+ success rate for valid transactions
Payouts - Coming soon
International - Uganda only
Installation
npm install @fundkit/easypay
Configuration
Basic Setup
import { EasyPay } from '@fundkit/easypay' ;
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
environment: 'sandbox' , // or 'production'
});
Configuration Options
Your EasyPay secret key. This is your main authentication credential.
Your EasyPay client ID. Used for API identification.
environment
'sandbox' | 'production'
default: "sandbox"
Environment mode. Use sandbox
for testing, production
for live payments.
Request timeout in milliseconds. EasyPay is fast, so shorter timeouts work well.
Number of retry attempts for failed requests.
URL to receive real-time webhook notifications.
Advanced Configuration
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
environment: 'production' ,
// Performance tuning
timeout: 20000 , // Shorter timeout for fast service
retries: 2 , // Fewer retries needed
retryDelay: 1000 , // Quick retry interval
// Webhook configuration
webhookUrl: 'https://api.myapp.com/webhooks/easypay' ,
webhookSecret: process . env . EASYPAY_WEBHOOK_SECRET ,
webhookEvents: [ 'payment.completed' , 'payment.failed' ],
// Rate limiting (very generous limits)
rateLimit: {
requests: 1000 ,
window: 60000 , // Per minute
},
// API customization
baseUrl: 'https://api.easypay.ug' ,
apiVersion: 'v1' ,
// Monitoring
logger: customLogger ,
logLevel: 'info' ,
metrics: true ,
});
Getting API Credentials
Sandbox Credentials
Get started immediately with sandbox testing:
Visit EasyPay Developer Portal
Create Sandbox App
Click “Create Application” and select “Sandbox Environment”
Get Your Credentials
Copy your Client ID and Secret Key from the dashboard
Configure Environment
EASYPAY_CLIENT_ID = ep_test_client_your_id_here
EASYPAY_SECRET = ep_test_secret_your_key_here
Production Credentials
For live payments, complete EasyPay’s streamlined onboarding:
Production Onboarding Process
Business Registration - Submit business documents and details 2. Quick Verification -
Automated verification (usually within 24 hours) 3. Integration Test - Complete a simple
integration verification 4. Go-Live - Receive production credentials immediately after
approval
EasyPay has one of the fastest onboarding processes in Uganda, typically completed within 1-2
business days.
Usage Examples
Basic Payment Collection
import { PaymentClient } from '@fundkit/core' ;
import { EasyPay } from '@fundkit/easypay' ;
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
environment: 'sandbox' ,
});
const client = new PaymentClient ({
apiKey: process . env . FUNDKIT_API_KEY ! ,
providers: [ easypay ],
environment: 'sandbox' ,
});
// Process payment
const transaction = {
amount: 15000 , // 150.00 UGX
currency: 'UGX' ,
operator: 'mtn' , // or 'airtel'
accountNumber: '256779280949' ,
externalId: 'invoice_67890' ,
reason: 'Service subscription' ,
};
try {
const result = await client . collection ( transaction );
console . log ( 'Payment initiated:' , result );
// EasyPay typically completes within 30 seconds
console . log ( 'Expected completion: < 30 seconds' );
} catch ( error ) {
console . error ( 'Payment failed:' , error . message );
}
Express.js Integration
import express from 'express' ;
import { PaymentClient } from '@fundkit/core' ;
import { EasyPay } from '@fundkit/easypay' ;
const app = express ();
app . use ( express . json ());
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
webhookUrl: 'https://api.myapp.com/webhooks/easypay' ,
});
const client = new PaymentClient ({
apiKey: process . env . FUNDKIT_API_KEY ! ,
providers: [ easypay ],
});
// Payment endpoint
app . post ( '/api/payments' , async ( req , res ) => {
try {
const { amount , phoneNumber , orderId } = req . body ;
// Validate input
if ( ! amount || ! phoneNumber || ! orderId ) {
return res . status ( 400 ). json ({
error: 'Missing required fields: amount, phoneNumber, orderId' ,
});
}
// Determine operator from phone number
const operator = phoneNumber . startsWith ( '25677' ) ? 'mtn' : 'airtel' ;
const transaction = {
amount: amount ,
currency: 'UGX' ,
operator: operator ,
accountNumber: phoneNumber ,
externalId: `order_ ${ orderId } ` ,
reason: `Payment for order ${ orderId } ` ,
};
const result = await client . collection ( transaction );
res . json ({
success: true ,
transactionId: result . data . transactionId ,
status: result . data . status ,
provider: result . provider ,
message: 'Payment initiated successfully' ,
});
} catch ( error ) {
console . error ( 'Payment error:' , error );
res . status ( 400 ). json ({
success: false ,
error: error . code || 'PAYMENT_FAILED' ,
message: error . message || 'Payment failed' ,
});
}
});
Bulk Payment Processing
async function processBulkPayments ( payments : Array < PaymentRequest >) {
const results = [];
const batchSize = 10 ; // Process 10 at a time
for ( let i = 0 ; i < payments . length ; i += batchSize ) {
const batch = payments . slice ( i , i + batchSize );
console . log ( `Processing batch ${ Math . floor ( i / batchSize ) + 1 } ...` );
const batchPromises = batch . map ( async payment => {
try {
const result = await client . collection ({
amount: payment . amount ,
currency: 'UGX' ,
operator: payment . operator ,
accountNumber: payment . phoneNumber ,
externalId: payment . orderId ,
reason: payment . description ,
});
return {
orderId: payment . orderId ,
success: true ,
transactionId: result . data . transactionId ,
};
} catch ( error ) {
return {
orderId: payment . orderId ,
success: false ,
error: error . message ,
};
}
});
const batchResults = await Promise . allSettled ( batchPromises );
results . push ( ... batchResults . map ( r => ( r . status === 'fulfilled' ? r . value : r . reason )));
// Brief pause between batches
if ( i + batchSize < payments . length ) {
await new Promise ( resolve => setTimeout ( resolve , 1000 ));
}
}
return results ;
}
Transaction Limits
Amount Limits
Minimum Transaction: 1,000 UGX
Maximum Transaction: 5,000,000 UGX
Daily Limit: 20,000,000 UGX per merchant
Monthly Limit: 100,000,000 UGX per merchant
Rate Limits
API Requests: 1,000 requests per minute
Concurrent Transactions: 100 simultaneous transactions
Bulk Processing: 1,000 transactions per batch
These limits apply to sandbox as well. Contact EasyPay support if you need higher limits for
testing or production.
Error Handling
Common EasyPay Errors
try {
const result = await client . collection ( transaction );
} catch ( error ) {
switch ( error . code ) {
case 'EP_INSUFFICIENT_FUNDS' :
showMessage ( 'Customer has insufficient balance' , 'error' );
// Suggest customer to top up
suggestTopUp ( transaction . accountNumber );
break ;
case 'EP_INVALID_PHONE' :
showMessage ( 'Invalid phone number. Please check and try again.' , 'error' );
// Highlight phone input field for correction
highlightPhoneInput ();
break ;
case 'EP_CUSTOMER_CANCELLED' :
showMessage ( 'Payment was cancelled by customer' , 'info' );
// Allow customer to retry
showRetryButton ();
break ;
case 'EP_NETWORK_ERROR' :
showMessage ( 'Mobile network is temporarily unavailable' , 'warning' );
// Retry automatically after delay
scheduleRetry ( transaction , 30000 ); // 30 seconds
break ;
case 'EP_DUPLICATE_EXTERNAL_ID' :
showMessage ( 'Duplicate transaction detected' , 'error' );
// Check if original transaction succeeded
await checkOriginalTransaction ( transaction . externalId );
break ;
case 'EP_AMOUNT_LIMIT_EXCEEDED' :
showMessage ( 'Transaction amount exceeds limits' , 'error' );
// Show the actual limits to user
showAmountLimits ();
break ;
default :
showMessage ( 'Payment failed. Please try again.' , 'error' );
logUnknownError ( error );
}
}
Error Categories
EP_INSUFFICIENT_FUNDS - Customer doesn’t have enough money EP_CUSTOMER_CANCELLED -
Customer declined the payment EP_PAYMENT_TIMEOUT - Customer didn’t respond in time
EP_INVALID_PIN - Wrong PIN entered multiple times
EP_INVALID_PHONE - Phone number format is wrong EP_PHONE_NOT_REGISTERED - Phone not
registered for mobile money EP_ACCOUNT_SUSPENDED - Customer account is suspended
EP_DAILY_LIMIT_EXCEEDED - Customer exceeded daily limits
EP_NETWORK_ERROR - Mobile network unavailable EP_SERVICE_UNAVAILABLE - EasyPay service
down EP_RATE_LIMITED - Too many requests EP_AUTHENTICATION_FAILED - Invalid credentials
EP_DUPLICATE_EXTERNAL_ID - External ID already used EP_AMOUNT_TOO_SMALL - Amount below
minimum EP_AMOUNT_TOO_LARGE - Amount above maximum EP_UNSUPPORTED_OPERATOR - Invalid
network operator
Webhooks
Real-time Status Updates
EasyPay provides excellent webhook reliability with sub-second delivery:
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
webhookUrl: 'https://api.myapp.com/webhooks/easypay' ,
webhookSecret: process . env . EASYPAY_WEBHOOK_SECRET ,
});
Webhook Handler Implementation
import express from 'express' ;
import crypto from 'crypto' ;
const app = express ();
app . use ( express . raw ({ type: 'application/json' }));
app . post ( '/webhooks/easypay' , ( req , res ) => {
const signature = req . headers [ 'x-easypay-signature' ];
const timestamp = req . headers [ 'x-easypay-timestamp' ];
const payload = req . body ;
// Verify webhook authenticity
if ( ! verifyEasyPayWebhook ( payload , signature , timestamp )) {
console . error ( 'Invalid webhook signature' );
return res . status ( 400 ). json ({ error: 'Invalid signature' });
}
const event = JSON . parse ( payload . toString ());
console . log ( 'Received webhook:' , event . type , event . data . externalId );
// Handle different event types
switch ( event . type ) {
case 'payment.completed' :
handlePaymentCompleted ( event . data );
break ;
case 'payment.failed' :
handlePaymentFailed ( event . data );
break ;
case 'payment.pending' :
handlePaymentPending ( event . data );
break ;
case 'payment.timeout' :
handlePaymentTimeout ( event . data );
break ;
default :
console . log ( 'Unknown webhook event:' , event . type );
}
// Always respond quickly to webhooks
res . status ( 200 ). json ({ received: true , processed: true });
});
function verifyEasyPayWebhook ( payload , signature , timestamp ) {
// Check timestamp to prevent replay attacks
const now = Math . floor ( Date . now () / 1000 );
const webhookTimestamp = parseInt ( timestamp );
if ( Math . abs ( now - webhookTimestamp ) > 300 ) {
// 5 minutes
return false ;
}
// Verify signature
const expectedSignature = crypto
. createHmac ( 'sha256' , process . env . EASYPAY_WEBHOOK_SECRET ! )
. update ( ` ${ timestamp } . ${ payload } ` )
. digest ( 'hex' );
return crypto . timingSafeEqual (
Buffer . from ( signature , 'hex' ),
Buffer . from ( expectedSignature , 'hex' )
);
}
async function handlePaymentCompleted ( payment ) {
console . log ( 'Payment completed:' , payment . externalId );
try {
// Update your database
await updatePaymentStatus ( payment . externalId , 'completed' , {
transactionId: payment . transactionId ,
completedAt: payment . completedAt ,
fees: payment . fees ,
netAmount: payment . netAmount ,
});
// Send success notification to customer
await sendSMS (
payment . customerPhone ,
`Payment of ${ payment . amount } UGX received successfully. Thank you!`
);
// Fulfill the order/service
await fulfillOrder ( payment . externalId );
// Send confirmation email
await sendConfirmationEmail ( payment . customerEmail , payment );
} catch ( error ) {
console . error ( 'Error processing completed payment:' , error );
// Consider implementing a retry mechanism
}
}
async function handlePaymentFailed ( payment ) {
console . log ( 'Payment failed:' , payment . externalId , payment . errorCode );
try {
// Update payment status
await updatePaymentStatus ( payment . externalId , 'failed' , {
errorCode: payment . errorCode ,
errorMessage: payment . errorMessage ,
failedAt: payment . failedAt ,
});
// Notify customer with helpful message
const userMessage = getFriendlyErrorMessage ( payment . errorCode );
await sendSMS ( payment . customerPhone , userMessage );
// Log for analysis
await logFailedPayment ( payment );
} catch ( error ) {
console . error ( 'Error processing failed payment:' , error );
}
}
Webhook Events
payment.completed Payment successful - money transferred
payment.failed Payment failed - includes error details
payment.pending Payment initiated - waiting for customer
payment.timeout Payment timed out - customer didn’t respond
Testing in Sandbox
Test Phone Numbers
EasyPay provides predictable test numbers for different scenarios:
const testScenarios = {
// MTN test numbers
mtnSuccess: '256777000001' , // Always succeeds quickly
mtnInsufficientFunds: '256777000002' , // Always fails - no money
mtnTimeout: '256777000003' , // Always times out
mtnCancelled: '256777000004' , // Customer cancels
mtnSlowSuccess: '256777000005' , // Succeeds after 20 seconds
// Airtel test numbers
airtelSuccess: '256701000001' , // Always succeeds quickly
airtelFailed: '256701000002' , // Always fails
airtelSlow: '256701000003' , // Succeeds after delay
// Invalid numbers
invalidFormat: '123456789' , // Wrong format
unregistered: '256777999999' , // Not registered
};
// Test different scenarios
async function runTestSuite () {
const scenarios = [
{ name: 'MTN Success' , phone: testScenarios . mtnSuccess },
{ name: 'MTN Insufficient Funds' , phone: testScenarios . mtnInsufficientFunds },
{ name: 'Airtel Success' , phone: testScenarios . airtelSuccess },
{ name: 'Invalid Format' , phone: testScenarios . invalidFormat },
];
for ( const scenario of scenarios ) {
console . log ( `Testing: ${ scenario . name } ` );
try {
const result = await client . collection ({
amount: 5000 ,
currency: 'UGX' ,
operator: scenario . phone . startsWith ( '25677' ) ? 'mtn' : 'airtel' ,
accountNumber: scenario . phone ,
externalId: `test_ ${ Date . now () } _ ${ Math . random () } ` ,
reason: `Test: ${ scenario . name } ` ,
});
console . log ( ` ${ scenario . name } : Success` , result . data . status );
} catch ( error ) {
console . log ( ` ${ scenario . name } : ${ error . code } - ${ error . message } ` );
}
// Brief pause between tests
await new Promise ( resolve => setTimeout ( resolve , 1000 ));
}
}
async function testPerformance () {
const startTime = Date . now ();
const concurrentTransactions = 50 ;
console . log ( `Starting ${ concurrentTransactions } concurrent transactions...` );
const promises = Array . from ({ length: concurrentTransactions }, ( _ , i ) =>
client
. collection ({
amount: 1000 ,
currency: 'UGX' ,
operator: 'mtn' ,
accountNumber: testScenarios . mtnSuccess ,
externalId: `perf_test_ ${ Date . now () } _ ${ i } ` ,
reason: 'Performance test' ,
})
. catch ( error => ({ error: error . message }))
);
const results = await Promise . all ( promises );
const successCount = results . filter ( r => ! r . error ). length ;
const duration = Date . now () - startTime ;
console . log ( `Results: ${ successCount } / ${ concurrentTransactions } successful` );
console . log ( `Duration: ${ duration } ms` );
console . log ( `Average: ${ Math . round ( duration / concurrentTransactions ) } ms per transaction` );
}
Production Optimization
Connection Pooling
import { Agent } from 'https' ;
const httpsAgent = new Agent ({
keepAlive: true ,
maxSockets: 50 ,
maxFreeSockets: 10 ,
timeout: 20000 ,
});
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
httpsAgent: httpsAgent ,
timeout: 20000 , // Shorter timeout for EasyPay's fast service
});
Monitoring and Metrics
class EasyPayMetrics {
private metrics = {
totalTransactions: 0 ,
successfulTransactions: 0 ,
failedTransactions: 0 ,
averageResponseTime: 0 ,
responseTimeSum: 0 ,
};
recordTransaction ( success : boolean , responseTime : number ) {
this . metrics . totalTransactions ++ ;
this . metrics . responseTimeSum += responseTime ;
this . metrics . averageResponseTime =
this . metrics . responseTimeSum / this . metrics . totalTransactions ;
if ( success ) {
this . metrics . successfulTransactions ++ ;
} else {
this . metrics . failedTransactions ++ ;
}
}
getSuccessRate () : number {
if ( this . metrics . totalTransactions === 0 ) return 0 ;
return this . metrics . successfulTransactions / this . metrics . totalTransactions ;
}
getMetrics () {
return {
... this . metrics ,
successRate: this . getSuccessRate (),
};
}
}
// Usage
const metrics = new EasyPayMetrics ();
const originalCollection = client . collection ;
client . collection = async function ( transaction ) {
const startTime = Date . now ();
try {
const result = await originalCollection . call ( this , transaction );
metrics . recordTransaction ( true , Date . now () - startTime );
return result ;
} catch ( error ) {
metrics . recordTransaction ( false , Date . now () - startTime );
throw error ;
}
};
// Log metrics every minute
setInterval (() => {
console . log ( 'EasyPay Metrics:' , metrics . getMetrics ());
}, 60000 );
Best Practices
Leverage Speed // Good: Use shorter timeouts with EasyPay
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
timeout: 20000 // 20 seconds is plenty
});
Use Webhooks // Good: Always configure webhooks
const easypay = new EasyPay ({
apiKey: process . env . EASYPAY_SECRET ! ,
clientId: process . env . EASYPAY_CLIENT_ID ! ,
webhookUrl: 'https://api.myapp.com/webhooks/easypay'
});
// Bad: Polling for status
// setInterval(() => checkStatus(), 5000);
Validate Phone Numbers // Good: Validate phone numbers
function validateUgandanPhone ( phone : string ) : boolean {
return / ^ 256 ( 77 | 78 | 70 | 75 | 74 ) [ 0-9 ] {7} $ / . test ( phone );
}
if ( ! validateUgandanPhone ( phoneNumber )) {
throw new Error ( 'Invalid Ugandan phone number' );
}
Smart Retry Logic // Good: Don't retry validation errors
if ( error . code === 'EP_INVALID_PHONE' ) {
throw error ; // Don't retry
}
// Retry system errors only
if ( error . code === 'EP_NETWORK_ERROR' ) {
await retryAfterDelay ( 5000 );
}
Support & Resources
Next Steps