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 });
});
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 );
}
}
Best Practices
Use Meaningful External IDs // Good: Descriptive external IDs
externalId : `order_ ${ orderId } _ ${ timestamp } `
externalId : `invoice_ ${ invoiceNumber } `
externalId : `subscription_ ${ userId } _ ${ month } `
Handle Timeouts Gracefully // Good: Proper timeout handling
try {
const result = await client . collection ( transaction );
} catch ( error ) {
if ( error . code === 'TIMEOUT' ) {
// Check status instead of immediate retry
const status = await client . getTransaction ({
provider: error . provider ,
txId: error . transactionId
});
}
}
Implement Idempotency // Good: Check for existing transaction
const existing = await getTransactionByExternalId ( externalId );
if ( existing ) {
return existing ; // Don't create duplicate
}
const result = await client . collection ( transaction );
Use Webhooks // Good: Real-time updates via webhooks
app . post ( '/webhook' , ( req , res ) => {
const event = req . body ;
updateTransactionStatus ( event . data );
res . status ( 200 ). json ({ ok: true });
});
Next Steps