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

1

Created

Transaction is created and validated by FundKit typescript const transaction ={' '} {/* transaction data */}; // Status: Not yet submitted
2

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
3

Processing

Provider processes the transaction with the mobile network typescript // Status: 'processing' // Customer receives SMS prompt to confirm payment
4

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);
}
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

Input 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);
}

Transaction Metadata

Adding Metadata

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 },
    ],
  },
};

Metadata Best Practices

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