M-Pesa Onramp - Convert KES to Crypto
Sphere M-Pesa Onramp is a seamless integration that allows Kenyan users to convert their Kenyan Shillings (KES) directly into cryptocurrency using Safaricom's M-Pesa mobile money platform. This feature enables instant crypto purchases through the familiar M-Pesa STK Push interface.
Table of Contents
- Overview
- How It Works
- Flow Diagram
- Getting Started
- API Reference
- Code Examples
- Transaction History
- Error Handling
- Best Practices
- Troubleshooting
- Use Cases
Overview
Key Features
- 🚀 Instant Conversion: Convert KES to crypto in real-time
- 📱 STK Push Integration: Familiar M-Pesa payment flow
- 🔄 Auto-Polling: Automatic transaction status monitoring
- 📊 Transaction History: Complete transaction tracking and filtering
- 🔐 Secure: Enterprise-grade security and reliability
- 💱 Multi-Asset Support: Support for popular crypto assets
Supported Crypto Assets
Asset | Description | Network |
---|---|---|
POL-USDC | USDC on Polygon | Polygon |
BERA-USDC | USDC on Berachain | Berachain |
ETH | Ethereum | Ethereum |
WBERA | Wrapped BERA | Berachain |
How It Works
The Sphere M-Pesa onramp process involves three main components:
- STK Push Initiation: Trigger M-Pesa payment request to user's phone
- Payment Processing: User completes payment on their phone
- Crypto Transfer: Automatic crypto transfer to user's wallet
Process Flow
- User initiates onramp with amount, phone number, and crypto preferences
- System sends STK push to user's M-Pesa registered phone
- User enters M-Pesa PIN on their phone to complete payment
- System monitors payment status and processes crypto transfer
- Crypto is transferred to user's specified wallet address
Flow Diagram
The sequence diagram below shows the complete M-Pesa onramp flow from initiation to completion.
Getting Started
Prerequisites
- Node.js 16+ or compatible JavaScript environment
- Sphere SDK installed
- Valid Sphere API key
- M-Pesa API access (handled by Sphere)
Installation
npm install @stratosphere-network/wallet
Basic Setup
import { Sphere, Environment } from "@stratosphere-network/wallet";
// Initialize Sphere SDK
const sphere = new Sphere({
apiKey: "your-sphere-api-key",
environment: Environment.PRODUCTION, // or Environment.DEVELOPMENT
});
Quick Start Example
async function quickMpesaOnramp() {
try {
// Step 1: Initiate STK push
const response = await sphere.onramp.initiateSafaricomSTK({
amount: 100, // 100 KES
phone: "0713322025", // User's M-Pesa phone
cryptoAsset: "POL-USDC",
cryptoWalletAddress: "0x31DEBea3ba4101bb582dc31fDB3068bE686791b0",
externalReference: "user_12345",
});
console.log("STK Push sent:", response.data.checkoutRequestID);
// Step 2: Auto-poll for completion
const result = await sphere.onramp.pollSafaricomTransactionStatus(
response.data.checkoutRequestID,
undefined,
10, // max attempts
10000 // 10 second intervals
);
// Step 3: Handle result
if (result.status === "success") {
console.log("✅ Onramp successful!");
console.log("Crypto amount:", result.data.cryptoAmount);
console.log("TX hash:", result.data.cryptoTxHash);
}
} catch (error) {
console.error("Onramp failed:", error.message);
}
}
API Reference
Core Methods
initiateSafaricomSTK(request)
Initiates an M-Pesa STK push for crypto onramping.
Parameters:
interface MpesaSTKInitiateRequest {
email?: string; // Optional user email
amount: number; // Amount in KES (Kenyan Shillings)
phone: string; // M-Pesa phone number (e.g., "0713322025")
cryptoAsset: "POL-USDC" | "BERA-USDC" | "ETH" | "WBERA";
cryptoWalletAddress: string; // Destination wallet address
externalReference: string; // Unique identifier (user ID, telegram ID, etc.)
}
Response:
interface MpesaSTKInitiateResponse {
success: boolean;
message: string;
data: {
message: string;
merchantRequestID: string;
checkoutRequestID: string;
safaricomResponse: {
MerchantRequestID: string;
CheckoutRequestID: string;
ResponseCode: string;
ResponseDescription: string;
CustomerMessage: string;
};
cryptoIntent: {
asset: string;
walletAddress: string;
};
note: string;
};
}
Example:
const stkResponse = await sphere.onramp.initiateSafaricomSTK({
email: "user@example.com", // Optional
amount: 500, // 500 KES
phone: "0713322025",
cryptoAsset: "POL-USDC",
cryptoWalletAddress: "0x31DEBea3ba4101bb582dc31fDB3068bE686791b0",
externalReference: "telegram_user_67890",
});
getSafaricomTransactionStatus(request)
Gets the current status of a Safaricom transaction.
Parameters:
interface MpesaTransactionStatusRequest {
checkoutRequestId?: string; // From STK initiation response
merchantRequestId?: string; // From STK initiation response
}
Response:
interface MpesaTransactionStatusResponse {
success: boolean;
status: "pending" | "success" | "failed";
data: {
id: string;
checkoutRequestId: string;
merchantRequestId: string;
status: "pending" | "success" | "failed";
amount: number;
currency: string;
phoneNumber: string;
mpesaReceiptNumber: string | null;
transactionDate: string | null;
cryptoStatus: "pending" | "success" | "failed" | null;
cryptoTxHash: string | null;
cryptoAmount: number | null;
amountInUSD: number | null;
failureReason: string | null;
cryptoFailureReason: string | null;
// ... additional fields
};
}
Example:
// Check by checkout request ID
const status = await sphere.onramp.getSafaricomTransactionStatus({
checkoutRequestId: "ws_CO_05062025134410304713322025",
});
// Or check by merchant request ID
const statusByMerchant = await sphere.onramp.getSafaricomTransactionStatus({
merchantRequestId: "ed4e-4482-896f-139740cf342c4176666",
});
pollSafaricomTransactionStatus(checkoutRequestId?, merchantId?, maxAttempts?, intervalMs?)
Automatically polls transaction status until completion or timeout.
Parameters:
checkoutRequestId
(optional): Checkout request ID from STK initiationmerchantId
(optional): Merchant request ID from STK initiationmaxAttempts
(optional): Maximum polling attempts (default: 10)intervalMs
(optional): Polling interval in milliseconds (default: 10000)
Note: At least one of checkoutRequestId
or merchantId
must be provided.
Example:
// Poll using checkout request ID
const finalStatus = await sphere.onramp.pollSafaricomTransactionStatus(
"ws_CO_05062025134410304713322025", // checkoutRequestId
undefined, // merchantId not needed
15, // max 15 attempts
5000 // poll every 5 seconds
);
// Poll using merchant request ID
const finalStatusByMerchant =
await sphere.onramp.pollSafaricomTransactionStatus(
undefined, // checkoutRequestId not needed
"ed4e-4482-896f-139740cf342c4176666", // merchantId
10,
10000
);
getUserTransactionHistory(filters?)
Retrieves user's M-Pesa onramp transaction history with filtering and pagination.
Parameters:
interface MpesaTransactionHistoryRequest {
email?: string;
externalReference?: string;
status?: "pending" | "success" | "failed";
cryptoStatus?: "pending" | "success" | "failed";
cryptoTxHash?: string;
mpesaReceiptNumber?: string;
minAmount?: number;
maxAmount?: number;
startDate?: string; // YYYY-MM-DD format
endDate?: string; // YYYY-MM-DD format
page?: number;
limit?: number;
sortBy?: string;
sortOrder?: "asc" | "desc";
}
Code Examples
Complete Onramp Flow
async function completeMpesaOnramp() {
try {
console.log("🚀 Starting M-Pesa onramp...");
// Step 1: Initiate STK push
const stkResponse = await sphere.onramp.initiateSafaricomSTK({
amount: 1000, // 1000 KES
phone: "0713322025",
cryptoAsset: "POL-USDC",
cryptoWalletAddress: "0x31DEBea3ba4101bb582dc31fDB3068bE686791b0",
externalReference: "telegram_user_12345",
email: "user@example.com", // Optional
});
console.log("📱 STK Push initiated:", stkResponse.data.checkoutRequestID);
if (!stkResponse.success) {
throw new Error(`STK initiation failed: ${stkResponse.message}`);
}
// Step 2: Auto-poll for completion
console.log("⏳ Waiting for M-Pesa payment...");
const result = await sphere.onramp.pollSafaricomTransactionStatus(
stkResponse.data.checkoutRequestID,
undefined,
20, // Wait up to 20 attempts (200 seconds total)
10000 // Check every 10 seconds
);
// Step 3: Handle final result
switch (result.status) {
case "success":
console.log("🎉 Onramp successful!");
console.log(
`💰 Received: ${result.data.cryptoAmount} ${result.data.cryptoIntent.asset}`
);
console.log(`🔗 Crypto TX: ${result.data.cryptoTxHash}`);
console.log(`📱 M-Pesa Receipt: ${result.data.mpesaReceiptNumber}`);
console.log(`💵 USD Value: $${result.data.amountInUSD}`);
break;
case "failed":
console.log("❌ Onramp failed");
console.log(`💔 M-Pesa Error: ${result.data.failureReason}`);
if (result.data.cryptoFailureReason) {
console.log(`🔗 Crypto Error: ${result.data.cryptoFailureReason}`);
}
break;
case "pending":
console.log("⏳ Transaction still pending after maximum wait time");
console.log("💡 Continue checking manually or extend polling time");
break;
}
return result;
} catch (error) {
console.error("💥 M-Pesa onramp error:", error.message);
throw error;
}
}
Manual Status Checking
async function manualStatusTracking(checkoutRequestId: string) {
const maxChecks = 5;
let attempts = 0;
while (attempts < maxChecks) {
try {
const status = await sphere.onramp.getSafaricomTransactionStatus({
checkoutRequestId,
});
console.log(`📊 Status check ${attempts + 1}:`, status.status);
if (status.status === "success" || status.status === "failed") {
console.log("🏁 Final status:", status);
return status;
}
// Wait 15 seconds before next check
await new Promise((resolve) => setTimeout(resolve, 15000));
attempts++;
} catch (error) {
console.error(`❌ Status check failed:`, error.message);
attempts++;
}
}
console.log("⏰ Maximum checks reached, transaction may still be processing");
}
Batch Processing
async function batchMpesaOnramp(
requests: Array<{
phone: string;
amount: number;
walletAddress: string;
reference: string;
}>
) {
const results = [];
for (const request of requests) {
try {
console.log(`🚀 Processing onramp for ${request.phone}...`);
const stkResponse = await sphere.onramp.initiateSafaricomSTK({
amount: request.amount,
phone: request.phone,
cryptoAsset: "POL-USDC",
cryptoWalletAddress: request.walletAddress,
externalReference: request.reference,
});
results.push({
phone: request.phone,
checkoutRequestId: stkResponse.data.checkoutRequestID,
success: stkResponse.success,
});
// Add delay between requests to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 2000));
} catch (error) {
console.error(
`❌ Failed to initiate for ${request.phone}:`,
error.message
);
results.push({
phone: request.phone,
error: error.message,
success: false,
});
}
}
return results;
}
Transaction History
Comprehensive History Filtering
async function demonstrateHistoryFiltering() {
// Get all successful transactions from last month
const successfulTxs = await sphere.onramp.getUserTransactionHistory({
status: "success",
cryptoStatus: "success",
startDate: "2024-12-01",
endDate: "2024-12-31",
sortBy: "createdAt",
sortOrder: "desc",
limit: 50,
});
console.log("✅ Successful transactions:", successfulTxs.data.length);
// Get high-value transactions
const highValueTxs = await sphere.onramp.getUserTransactionHistory({
minAmount: 5000, // 5000 KES and above
status: "success",
sortBy: "amount",
sortOrder: "desc",
});
console.log("💰 High-value transactions:", highValueTxs.data.length);
// Get transactions for specific user
const userTxs = await sphere.onramp.getUserTransactionHistory({
email: "user@example.com",
externalReference: "telegram_12345",
page: 1,
limit: 10,
});
console.log("👤 User-specific transactions:", userTxs.data.length);
// Get failed transactions for debugging
const failedTxs = await sphere.onramp.getUserTransactionHistory({
status: "failed",
sortBy: "updatedAt",
sortOrder: "desc",
limit: 20,
});
console.log("❌ Failed transactions:", failedTxs.data.length);
failedTxs.data.forEach((tx) => {
console.log(`- ${tx.id}: ${tx.failureReason}`);
});
}
Pagination Example
async function paginateTransactionHistory() {
let currentPage = 1;
const limit = 10;
let hasMore = true;
console.log("📄 Fetching paginated transaction history...");
while (hasMore) {
const response = await sphere.onramp.getUserTransactionHistory({
page: currentPage,
limit,
sortBy: "createdAt",
sortOrder: "desc",
});
console.log(`📋 Page ${currentPage}: ${response.data.length} transactions`);
// Process transactions
response.data.forEach((tx, index) => {
console.log(
` ${index + 1}. ${tx.amount} KES → ${tx.cryptoAmount} ${
tx.cryptoIntent.asset
}`
);
});
// Check if there are more pages
hasMore = response.pagination.hasNextPage;
currentPage++;
// Safety check to prevent infinite loops
if (currentPage > 10) {
console.log("🛑 Stopping pagination at page 10");
break;
}
}
console.log("✅ Pagination complete");
}
Error Handling
Comprehensive Error Handling
async function robustMpesaOnramp(request: MpesaSTKInitiateRequest) {
try {
const stkResponse = await sphere.onramp.initiateSafaricomSTK(request);
if (!stkResponse.success) {
throw new Error(`STK initiation failed: ${stkResponse.message}`);
}
return await sphere.onramp.pollSafaricomTransactionStatus(
stkResponse.data.checkoutRequestID
);
} catch (error: any) {
// Handle specific HTTP errors
switch (error.status) {
case 400:
console.error("❌ Bad Request:", error.message);
if (error.message.includes("phone")) {
throw new Error(
"Please provide a valid M-Pesa phone number (e.g., 0713322025)"
);
} else if (error.message.includes("amount")) {
throw new Error("Amount must be between minimum and maximum limits");
} else if (error.message.includes("asset")) {
throw new Error(
"Unsupported crypto asset. Use POL-USDC, BERA-USDC, ETH, or WBERA"
);
}
break;
case 401:
console.error("🔐 Unauthorized:", error.message);
throw new Error(
"Invalid API key. Please check your Sphere API credentials"
);
case 429:
console.error("⏰ Rate Limited:", error.message);
throw new Error("Too many requests. Please wait before retrying");
case 500:
console.error("🔧 Server Error:", error.message);
throw new Error(
"M-Pesa service is temporarily unavailable. Please try again later"
);
default:
if (error.name === "TypeError" && error.message.includes("fetch")) {
throw new Error(
"Network error. Please check your internet connection"
);
}
console.error("💥 Unexpected error:", error);
throw new Error(
"An unexpected error occurred. Please contact support if this persists"
);
}
}
}
Retry Logic with Exponential Backoff
async function retryableOnramp(
request: MpesaSTKInitiateRequest,
maxRetries: number = 3
) {
let attempts = 0;
let lastError: Error;
while (attempts < maxRetries) {
try {
attempts++;
console.log(`🔄 Attempt ${attempts}/${maxRetries}`);
return await sphere.onramp.initiateSafaricomSTK(request);
} catch (error: any) {
lastError = error;
// Don't retry on client errors (4xx)
if (error.status >= 400 && error.status < 500) {
throw error;
}
if (attempts === maxRetries) {
throw lastError;
}
// Exponential backoff: 2^attempt seconds
const delay = Math.pow(2, attempts) * 1000;
console.log(`⏳ Waiting ${delay}ms before retry...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError!;
}
Best Practices
1. Input Validation
function validateMpesaRequest(request: MpesaSTKInitiateRequest): void {
// Validate phone number format
const phoneRegex = /^(?:\+254|254|0)?([7][0-9]{8})$/;
if (!phoneRegex.test(request.phone)) {
throw new Error("Invalid phone number format. Use format: 0713322025");
}
// Validate amount limits
const minAmount = 1; // 1 KES
const maxAmount = 150000; // 150,000 KES (current M-Pesa limit)
if (request.amount < minAmount || request.amount > maxAmount) {
throw new Error(`Amount must be between ${minAmount} and ${maxAmount} KES`);
}
// Validate wallet address
const addressRegex = /^0x[a-fA-F0-9]{40}$/;
if (!addressRegex.test(request.cryptoWalletAddress)) {
throw new Error("Invalid wallet address format");
}
// Validate crypto asset
const supportedAssets = ["POL-USDC", "BERA-USDC", "ETH", "WBERA"];
if (!supportedAssets.includes(request.cryptoAsset)) {
throw new Error(
`Unsupported crypto asset. Use one of: ${supportedAssets.join(", ")}`
);
}
}
2. Transaction Monitoring
class TransactionMonitor {
private activeTransactions = new Map<
string,
{
checkoutRequestId: string;
startTime: Date;
status: string;
}
>();
async monitorTransaction(checkoutRequestId: string): Promise<void> {
this.activeTransactions.set(checkoutRequestId, {
checkoutRequestId,
startTime: new Date(),
status: "pending",
});
try {
const result = await sphere.onramp.pollSafaricomTransactionStatus(
checkoutRequestId,
undefined,
30, // Extended monitoring: 30 attempts
10000 // 10 second intervals
);
// Update status
const transaction = this.activeTransactions.get(checkoutRequestId);
if (transaction) {
transaction.status = result.status;
}
// Log completion
const duration =
Date.now() -
this.activeTransactions.get(checkoutRequestId)!.startTime.getTime();
console.log(
`📊 Transaction ${checkoutRequestId} completed in ${duration}ms with status: ${result.status}`
);
} finally {
// Clean up
this.activeTransactions.delete(checkoutRequestId);
}
}
getActiveTransactions(): Array<{
checkoutRequestId: string;
duration: number;
status: string;
}> {
const now = Date.now();
return Array.from(this.activeTransactions.values()).map((tx) => ({
checkoutRequestId: tx.checkoutRequestId,
duration: now - tx.startTime.getTime(),
status: tx.status,
}));
}
}
Troubleshooting
Common Issues and Solutions
1. STK Push Not Received
Symptoms:
- User doesn't receive STK push notification
- Transaction remains in pending state
Possible Causes & Solutions:
async function troubleshootSTKPush(phone: string, checkoutRequestId: string) {
console.log("🔍 Troubleshooting STK push issues...");
// Check phone number format
const cleanPhone = phone.replace(/[\s\-\(\)]/g, "");
const phoneRegex = /^(?:\+254|254|0)?([7][0-9]{8})$/;
if (!phoneRegex.test(cleanPhone)) {
console.log("❌ Invalid phone number format");
console.log("✅ Solution: Use format 0713322025 or +254713322025");
return;
}
// Check if phone is M-Pesa registered
console.log("📱 Ensure phone number is registered with M-Pesa");
console.log("📱 Check if phone has sufficient airtime/data");
console.log("📱 Try restarting the phone");
// Check transaction status
const status = await sphere.onramp.getSafaricomTransactionStatus({
checkoutRequestId,
});
console.log("📊 Current status:", status.status);
if (status.status === "pending") {
console.log("⏳ Transaction is still processing");
console.log("💡 STK push may arrive within 1-2 minutes");
}
}
2. Transaction Timeouts
Symptoms:
- Polling times out
- Transaction stuck in pending state
Solutions:
async function handleTimeouts(checkoutRequestId: string) {
console.log("⏰ Handling transaction timeout...");
try {
// Extended polling with shorter intervals
const result = await sphere.onramp.pollSafaricomTransactionStatus(
checkoutRequestId,
undefined,
60, // Increase to 60 attempts
5000 // Check every 5 seconds
);
return result;
} catch (error) {
console.log("🔍 Timeout occurred, checking final status...");
// Manual final check
const finalStatus = await sphere.onramp.getSafaricomTransactionStatus({
checkoutRequestId,
});
console.log("📊 Final status:", finalStatus.status);
if (finalStatus.status === "pending") {
console.log(
"💡 Transaction may complete later. Continue checking periodically."
);
console.log(
"📞 Contact user to confirm if they completed M-Pesa payment"
);
}
return finalStatus;
}
}
Debugging Tools
class MpesaDebugger {
static async diagnoseTransaction(checkoutRequestId: string) {
console.log("🔍 Starting transaction diagnosis...");
const status = await sphere.onramp.getSafaricomTransactionStatus({
checkoutRequestId,
});
const transaction = status.data;
const now = new Date();
const createdAt = new Date(transaction.createdAt);
const ageMinutes = (now.getTime() - createdAt.getTime()) / (1000 * 60);
console.log("📊 Transaction Diagnosis:");
console.log(` ID: ${transaction.id}`);
console.log(` Age: ${ageMinutes.toFixed(1)} minutes`);
console.log(` M-Pesa Status: ${transaction.status}`);
console.log(` Crypto Status: ${transaction.cryptoStatus || "N/A"}`);
console.log(` Amount: ${transaction.amount} ${transaction.currency}`);
console.log(` Phone: ${transaction.phoneNumber}`);
if (transaction.failureReason) {
console.log(` ❌ M-Pesa Error: ${transaction.failureReason}`);
}
if (transaction.cryptoFailureReason) {
console.log(` ❌ Crypto Error: ${transaction.cryptoFailureReason}`);
}
if (transaction.mpesaReceiptNumber) {
console.log(` 📱 M-Pesa Receipt: ${transaction.mpesaReceiptNumber}`);
}
if (transaction.cryptoTxHash) {
console.log(` 🔗 Crypto TX: ${transaction.cryptoTxHash}`);
}
// Provide recommendations
this.provideRecommendations(transaction, ageMinutes);
}
private static provideRecommendations(transaction: any, ageMinutes: number) {
console.log("\n💡 Recommendations:");
if (transaction.status === "pending" && ageMinutes > 10) {
console.log(" ⏰ Transaction is taking longer than usual");
console.log(" 📞 Contact user to confirm they received STK push");
}
if (transaction.status === "success" && !transaction.cryptoStatus) {
console.log(" ⚡ M-Pesa payment complete, waiting for crypto transfer");
console.log(" ⏳ Crypto transfer usually takes 1-2 minutes");
}
if (transaction.status === "failed") {
console.log(" ❌ M-Pesa payment failed");
console.log(" 🔄 User can try again with the same or different amount");
}
if (transaction.cryptoStatus === "failed") {
console.log(
" 🔗 Crypto transfer failed but will be automatically retried"
);
console.log(" 📞 Contact support if issue persists");
}
}
}
Use Cases
1. Telegram Bot Integration
class TelegramMpesaBot {
async handleOnrampCommand(
telegramUserId: string,
amount: number,
phone: string,
walletAddress: string
) {
try {
// Initiate onramp with Telegram user ID as reference
const response = await sphere.onramp.initiateSafaricomSTK({
amount,
phone,
cryptoAsset: "POL-USDC",
cryptoWalletAddress: walletAddress,
externalReference: `telegram_${telegramUserId}`,
});
// Send confirmation message
await this.sendMessage(
telegramUserId,
`📱 STK push sent to ${phone}! Check your phone and enter your M-Pesa PIN.\n\n` +
`💰 Amount: ${amount} KES\n` +
`🎯 You'll receive: ~${(amount / 136).toFixed(4)} USDC\n` +
`⏰ Please complete payment within 2 minutes.`
);
// Start monitoring in background
this.monitorTransaction(telegramUserId, response.data.checkoutRequestID);
} catch (error) {
await this.sendMessage(
telegramUserId,
`❌ Failed to initiate M-Pesa payment: ${error.message}`
);
}
}
private async monitorTransaction(
telegramUserId: string,
checkoutRequestId: string
) {
try {
const result = await sphere.onramp.pollSafaricomTransactionStatus(
checkoutRequestId,
undefined,
20, // 20 attempts
15000 // 15 second intervals
);
switch (result.status) {
case "success":
await this.sendMessage(
telegramUserId,
`🎉 Payment successful!\n\n` +
`💰 Received: ${result.data.cryptoAmount} ${result.data.cryptoIntent.asset}\n` +
`🔗 TX Hash: ${result.data.cryptoTxHash}\n` +
`📱 M-Pesa Receipt: ${result.data.mpesaReceiptNumber}`
);
break;
case "failed":
await this.sendMessage(
telegramUserId,
`❌ Payment failed: ${result.data.failureReason}\n\n` +
`💡 You can try again with /onramp command.`
);
break;
case "pending":
await this.sendMessage(
telegramUserId,
`⏳ Payment is still processing. We'll notify you when it completes.\n\n` +
`🔍 Transaction ID: ${result.data.id}`
);
break;
}
} catch (error) {
await this.sendMessage(
telegramUserId,
`⚠️ Error monitoring payment: ${error.message}\n\n` +
`💡 Please check your transaction history or contact support.`
);
}
}
private async sendMessage(telegramUserId: string, message: string) {
// Implement Telegram bot message sending
console.log(`📤 To ${telegramUserId}: ${message}`);
}
}
2. Web Application Integration
class WebAppMpesaIntegration {
private activePurchases = new Map<
string,
{
userId: string;
amount: number;
status: string;
onStatusUpdate: (status: any) => void;
}
>();
async initiatePurchase(
userId: string,
amount: number,
phone: string,
walletAddress: string,
onStatusUpdate: (status: any) => void
) {
try {
// Validate user session
await this.validateUserSession(userId);
// Initiate M-Pesa onramp
const response = await sphere.onramp.initiateSafaricomSTK({
amount,
phone,
cryptoAsset: "POL-USDC",
cryptoWalletAddress: walletAddress,
externalReference: `webapp_${userId}`,
email: await this.getUserEmail(userId),
});
const checkoutRequestId = response.data.checkoutRequestID;
// Store purchase info
this.activePurchases.set(checkoutRequestId, {
userId,
amount,
status: "initiated",
onStatusUpdate,
});
// Start real-time monitoring
this.startRealtimeMonitoring(checkoutRequestId);
return {
success: true,
checkoutRequestId,
message: "STK push sent to your phone",
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
}
private async startRealtimeMonitoring(checkoutRequestId: string) {
const purchase = this.activePurchases.get(checkoutRequestId);
if (!purchase) return;
try {
// Update status to monitoring
purchase.status = "monitoring";
purchase.onStatusUpdate({
status: "monitoring",
message: "Waiting for M-Pesa payment...",
});
// Poll with real-time updates
let attempts = 0;
const maxAttempts = 24; // 4 minutes total (24 * 10 seconds)
while (attempts < maxAttempts) {
const status = await sphere.onramp.getSafaricomTransactionStatus({
checkoutRequestId,
});
// Send real-time update
purchase.onStatusUpdate({
status: status.status,
data: status.data,
attempt: attempts + 1,
maxAttempts,
});
if (status.status === "success" || status.status === "failed") {
break;
}
await new Promise((resolve) => setTimeout(resolve, 10000));
attempts++;
}
} catch (error) {
purchase.onStatusUpdate({
status: "error",
error: error.message,
});
} finally {
// Clean up
this.activePurchases.delete(checkoutRequestId);
}
}
async getUserPurchaseHistory(
userId: string,
page: number = 1,
limit: number = 10
) {
return await sphere.onramp.getUserTransactionHistory({
externalReference: `webapp_${userId}`,
page,
limit,
sortBy: "createdAt",
sortOrder: "desc",
});
}
private async validateUserSession(userId: string): Promise<void> {
// Implement user session validation
}
private async getUserEmail(userId: string): Promise<string> {
// Implement user email retrieval
return "user@example.com";
}
}
Important Considerations
- Network Reliability: M-Pesa STK push requires stable network connectivity
- User Education: Ensure users understand the M-Pesa PIN entry process
- Transaction Monitoring: Implement robust monitoring for transaction status
- Error Handling: Handle network failures, timeout scenarios, and payment failures gracefully
- Rate Limiting: Respect API rate limits to avoid service disruption
- Security: Store sensitive data securely and validate all inputs
- Compliance: Ensure compliance with local financial regulations
Next Steps
- Explore Payment Requests & Send Links for additional payment options
- Learn about Sending Transactions & Viewing History for crypto transfers
- Check out DeFi Integrations for token swapping capabilities
For comprehensive details, consult the Sphere SDK API reference or join our community for support.