Overview
Transactions are the core data structure in FundKit, representing a payment request or money transfer. Understanding the transaction lifecycle is crucial for building robust payment integrations.
A transaction in FundKit represents a single payment operation - whether that’s collecting money
from a customer, sending money to a recipient, or checking the status of a previous payment.
Transaction Structure
Basic Transaction
interface Transaction {
amount : number ; // Amount in smallest currency unit
currency : string ; // ISO currency code (UGX, KES, etc.)
operator : string ; // Mobile network operator
accountNumber : string ; // Phone number or account identifier
externalId : string ; // Your unique transaction ID
reason ?: string ; // Description of the transaction
}
Example Transaction
const transaction = {
amount: 10000 , // 100.00 UGX (cents)
currency: 'UGX' , // Ugandan Shillings
operator: 'mtn' , // MTN Uganda
accountNumber: '256779280949' , // Customer's phone number
externalId: 'order_12345' , // Your order/invoice ID
reason: 'Payment for order #12345' , // Human-readable description
};
Transaction Types
Collections
Collect money from customers (charge them):
// Customer paying for an order
const collectionTx = {
amount: 25000 , // 250.00 UGX
currency: 'UGX' ,
operator: 'airtel' ,
accountNumber: '256701234567' ,
externalId: 'invoice_789' ,
reason: 'Invoice payment' ,
};
const result = await client . collection ( collectionTx );
Payouts (Coming Soon)
Send money to recipients:
// Paying a seller or refunding a customer
const payoutTx = {
amount: 15000 , // 150.00 UGX
currency: 'UGX' ,
operator: 'mtn' ,
accountNumber: '256778123456' ,
externalId: 'payout_456' ,
reason: 'Seller commission payment' ,
};
const result = await client . payout ( payoutTx );
Transaction Lifecycle
Created
Transaction is created and validated by FundKit typescript const transaction ={' '} {/* transaction data */}; // Status: Not yet submitted
Submitted
Transaction is sent to the selected payment provider typescript const result = await client.collection(transaction); // Status: 'pending' // Provider: Selected provider name // TransactionId: Provider's transaction ID
Processing
Provider processes the transaction with the mobile network typescript // Status: 'processing' // Customer receives SMS prompt to confirm payment
Completed or Failed
Final status is determined typescript // Success: Status: 'completed' // Failure: Status: 'failed' // Timeout: Status: 'timeout' // Cancelled: Status: 'cancelled'
Transaction States
Pending
Transaction has been submitted but not yet processed:
{
id : 'txn_abc123' ,
status : 'pending' ,
amount : 10000 ,
currency : 'UGX' ,
externalId : 'order_12345' ,
provider : 'honeycoin' ,
createdAt : '2024-01-15T10:00:00Z'
}
What it means:
Provider has accepted the transaction
Waiting for mobile network to process
Customer may receive SMS prompt
Outcome is not yet determined
Processing
Transaction is actively being processed:
{
id : 'txn_abc123' ,
status : 'processing' ,
amount : 10000 ,
currency : 'UGX' ,
externalId : 'order_12345' ,
provider : 'honeycoin' ,
processingStartedAt : '2024-01-15T10:01:30Z'
}
What it means:
Mobile network is processing payment
Customer is likely seeing payment prompt
Should complete within 1-3 minutes
Completed
Transaction was successful:
{
id : 'txn_abc123' ,
status : 'completed' ,
amount : 10000 ,
currency : 'UGX' ,
externalId : 'order_12345' ,
provider : 'honeycoin' ,
completedAt : '2024-01-15T10:02:45Z' ,
fees : 100 , // Provider fees
netAmount : 9900 , // Amount after fees
providerReference : 'HC_REF_789' // Provider's reference
}
What it means:
Payment was successful
Money has been transferred
You can fulfill the order/service
Customer has been charged
Failed
Transaction was unsuccessful:
{
id : 'txn_abc123' ,
status : 'failed' ,
amount : 10000 ,
currency : 'UGX' ,
externalId : 'order_12345' ,
provider : 'honeycoin' ,
failedAt : '2024-01-15T10:05:00Z' ,
errorCode : 'INSUFFICIENT_FUNDS' ,
errorMessage : 'Customer has insufficient balance' ,
details : {
providerError : 'Account balance too low' ,
canRetry : true
}
}
Common failure reasons:
INSUFFICIENT_FUNDS - Customer doesn’t have enough money
INVALID_PHONE_NUMBER - Phone number is incorrect
CUSTOMER_CANCELLED - Customer declined the payment
NETWORK_ERROR - Mobile network issue
ACCOUNT_BLOCKED - Customer’s account is blocked
Timeout
Transaction took too long to complete:
{
id : 'txn_abc123' ,
status : 'timeout' ,
amount : 10000 ,
currency : 'UGX' ,
externalId : 'order_12345' ,
provider : 'honeycoin' ,
timeoutAt : '2024-01-15T10:10:00Z' ,
details : {
reason : 'Customer did not respond to payment prompt' ,
canRetry : true
}
}
What it means:
Transaction exceeded maximum processing time
Customer may not have seen or responded to SMS
Transaction can usually be retried
Cancelled
Transaction was cancelled before completion:
{
id : 'txn_abc123' ,
status : 'cancelled' ,
amount : 10000 ,
currency : 'UGX' ,
externalId : 'order_12345' ,
provider : 'honeycoin' ,
cancelledAt : '2024-01-15T10:03:00Z' ,
cancelReason : 'Customer requested cancellation'
}
Checking Transaction Status
Single Status Check
const status = await client . getTransaction ({
provider: 'honeycoin' ,
txId: 'txn_abc123' ,
});
console . log ( `Transaction ${ status . externalId } is ${ status . status } ` );
Polling for Updates
async function waitForCompletion ( provider : string , txId : string ) {
const maxAttempts = 20 ; // Maximum polling attempts
const pollInterval = 5000 ; // 5 seconds between checks
for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) {
const status = await client . getTransaction ({ provider , txId });
console . log ( `Attempt ${ attempt } : Status is ${ status . status } ` );
// Check if transaction is in final state
if ([ 'completed' , 'failed' , 'timeout' , 'cancelled' ]. includes ( status . status )) {
return status ;
}
// Wait before next check
if ( attempt < maxAttempts ) {
await new Promise ( resolve => setTimeout ( resolve , pollInterval ));
}
}
throw new Error ( 'Transaction status check timed out' );
}
// Usage
try {
const finalStatus = await waitForCompletion ( 'honeycoin' , 'txn_abc123' );
console . log ( 'Final status:' , finalStatus );
} catch ( error ) {
console . error ( 'Status check failed:' , error . message );
}
Using Webhooks (Recommended)
Instead of polling, use webhooks for real-time updates:
// Configure webhook URL in provider settings
const honeycoin = new HoneyCoin ({
apiKey: process . env . HONEYCOIN_API_KEY ! ,
publicKey: process . env . HONEYCOIN_PUBLIC_KEY ! ,
webhookUrl: 'https://api.myapp.com/webhooks/honeycoin' ,
});
// Handle webhook in your API
app . post ( '/webhooks/honeycoin' , ( req , res ) => {
const event = req . body ;
if ( event . type === 'transaction.completed' ) {
const transaction = event . data ;
console . log ( `Transaction ${ transaction . externalId } completed!` );
// Update your database
// Send confirmation to customer
// Fulfill the order
}
res . status ( 200 ). json ({ received: true });
});
Testing Transactions
Auto-Confirm Testing (Sandbox Only)
In sandbox mode, you can test transactions without opening the emulator by using special test phone numbers:
Auto-confirm feature: Use phone numbers ending in 11111111 for automatic success or
00000000 for automatic failure. No emulator interaction required!
Auto-Success Transactions
Use a phone number with country code + 11111111 to automatically simulate a successful transaction:
// Automatically succeeds - no emulator needed
const successTx = {
amount: 10000 ,
currency: 'UGX' ,
operator: 'mtn' ,
accountNumber: '25611111111' , // Country code + 11111111
externalId: 'test_success_123' ,
reason: 'Auto-success test' ,
};
const result = await client . collection ( successTx );
// Transaction will automatically complete successfully
Auto-Failed Transactions
Use a phone number with country code + 00000000 to automatically simulate a failed transaction:
// Automatically fails - no emulator needed
const failedTx = {
amount: 10000 ,
currency: 'UGX' ,
operator: 'mtn' ,
accountNumber: '25600000000' , // Country code + 00000000
externalId: 'test_failed_123' ,
reason: 'Auto-fail test' ,
};
const result = await client . collection ( failedTx );
// Transaction will automatically fail
Supported formats:
25611111111 (Uganda - auto-success)
25600000000 (Uganda - auto-fail)
25411111111 (Kenya - auto-success)
25400000000 (Kenya - auto-fail)
25511111111 (Tanzania - auto-success)
25500000000 (Tanzania - auto-fail)
Works with any country code + 11111111 or 00000000
This feature only works in sandbox mode. Use these test numbers to quickly iterate on your
integration without waiting for emulator interactions.
Transaction Validation
FundKit validates all transaction data before processing:
// Valid transaction
const validTx = {
amount: 5000 , // Positive number
currency: 'UGX' , // Supported currency
operator: 'mtn' , // Valid operator
accountNumber: '256779280949' , // Valid phone format
externalId: 'order_123' , // Unique identifier
reason: 'Test payment' , // Optional description
};
// Invalid transaction examples
const invalidTx = {
amount: - 1000 , // Negative amount
currency: 'USD' , // Unsupported currency
operator: 'vodafone' , // Unsupported operator
accountNumber: '123' , // Invalid phone number
externalId: '' , // Empty external ID
};
Custom Validation
Add your own validation before sending transactions:
function validateTransaction ( tx : Transaction ) : void {
// Check amount limits
if ( tx . amount < 1000 ) {
throw new Error ( 'Minimum transaction amount is 1,000 UGX' );
}
if ( tx . amount > 5000000 ) {
throw new Error ( 'Maximum transaction amount is 5,000,000 UGX' );
}
// Validate phone number format
const phoneRegex = / ^ 256 [ 0-9 ] {9} $ / ;
if ( ! phoneRegex . test ( tx . accountNumber )) {
throw new Error ( 'Phone number must be in format 256XXXXXXXXX' );
}
// Check external ID format
if ( ! tx . externalId . startsWith ( 'order_' )) {
throw new Error ( 'External ID must start with "order_"' );
}
}
// Use validation before processing
try {
validateTransaction ( transaction );
const result = await client . collection ( transaction );
} catch ( error ) {
console . error ( 'Validation failed:' , error . message );
}
Store additional information with transactions:
const transaction = {
amount: 10000 ,
currency: 'UGX' ,
operator: 'mtn' ,
accountNumber: '256779280949' ,
externalId: 'order_12345' ,
reason: 'Product purchase' ,
// Additional metadata
metadata: {
customerId: 'cust_789' ,
productId: 'prod_456' ,
campaignId: 'campaign_123' ,
userAgent: 'MyApp/1.0' ,
ipAddress: '192.168.1.1' ,
orderItems: [
{ id: 'item_1' , quantity: 2 , price: 3000 },
{ id: 'item_2' , quantity: 1 , price: 4000 },
],
},
};
Use Consistent Keys // Good: Consistent naming
metadata : {
customer_id : 'cust_123' ,
order_id : 'order_456' ,
product_id : 'prod_789'
}
Avoid Sensitive Data // Good: Safe metadata
metadata : {
customer_id : 'cust_123' ,
order_total : 10000
}
// Bad: Sensitive data
metadata : {
credit_card : '4111-1111-1111-1111' ,
password : 'secret123'
}
Keep It Small // Good: Compact metadata
metadata : {
customer : 'c123' ,
product : 'p456'
}
// Bad: Large objects
metadata : {
full_customer_profile : { /* huge object */ }
}
Make It Searchable // Good: Searchable fields
metadata : {
campaign_id : 'holiday2024' ,
category : 'electronics' ,
region : 'kampala'
}
Transaction Reporting
Basic Reporting
// Get transactions for a date range
const transactions = await client . getTransactions ({
startDate: '2024-01-01' ,
endDate: '2024-01-31' ,
status: 'completed' ,
limit: 100 ,
});
// Calculate totals
const total = transactions . reduce (( sum , tx ) => sum + tx . amount , 0 );
const fees = transactions . reduce (( sum , tx ) => sum + ( tx . fees || 0 ), 0 );
console . log ( `Total volume: ${ total } UGX` );
console . log ( `Total fees: ${ fees } UGX` );
console . log ( `Net amount: ${ total - fees } UGX` );
Advanced Analytics
// Group by provider
const byProvider = transactions . reduce (( acc , tx ) => {
acc [ tx . provider ] = acc [ tx . provider ] || { count: 0 , volume: 0 };
acc [ tx . provider ]. count ++ ;
acc [ tx . provider ]. volume += tx . amount ;
return acc ;
}, {});
// Success rates
const successRate =
transactions . filter ( tx => tx . status === 'completed' ). length / transactions . length ;
// Average transaction amount
const avgAmount = total / transactions . length ;
console . log ( 'Analytics:' , {
successRate: ` ${ ( successRate * 100 ). toFixed ( 2 ) } %` ,
avgAmount: ` ${ avgAmount . toFixed ( 0 ) } UGX` ,
byProvider ,
});
Error Handling
Transaction-Specific Errors
try {
const result = await client . collection ( transaction );
} catch ( error ) {
// Handle validation errors
if ( error . code === 'VALIDATION_ERROR' ) {
console . error ( 'Invalid transaction data:' , error . details );
// Show user-friendly validation message
return ;
}
// Handle provider errors
if ( error . code === 'PROVIDER_ERROR' ) {
console . error ( 'Provider issue:' , error . message );
// Try different provider or retry later
return ;
}
// Handle specific transaction errors
switch ( error . code ) {
case 'INSUFFICIENT_FUNDS' :
// Customer doesn't have enough money
showMessage ( 'Insufficient funds. Please top up and try again.' );
break ;
case 'INVALID_PHONE_NUMBER' :
// Phone number is wrong
showMessage ( 'Invalid phone number. Please check and try again.' );
break ;
case 'CUSTOMER_CANCELLED' :
// Customer declined payment
showMessage ( 'Payment was cancelled.' );
break ;
default :
// Unknown error
showMessage ( 'Payment failed. Please try again.' );
logError ( error );
}
}
Next Steps