Mobile Wallet Credit Integration Guide
Supported Mobile Wallet Operators
The framework supports the following mobile wallet operators and will accept these from a payment request.
| Operator | Code your service receives | Description |
|---|---|---|
| MTN | MTN | MTN Mobile Money (Ghana, South Africa, and other African markets) |
| Vodafone | VDF | Vodafone Mobile Money (Ghana) |
| Airtel | ATL | Airtel Mobile Money |
| Tigo | TGO | Tigo Mobile Money (Ghana) |
| AT (AirtelTigo) | ATG | AirtelTigo Mobile Money (Ghana) |
| G-Money | GMY | G-Money Wallet (GCB Bank, Ghana) |
| Zeepay | ZPY | Zeepay Wallet (Ghana) |
| GhanaPay | GHP | GhanaPay (interoperable scheme, Ghana) |
| Telecel | TCG | Telecel Mobile Money (formerly Vodafone Ghana) |
| Vodacom | VDC | Vodacom Mobile Money (South Africa) |
| Cell C | CLC | Cell C Mobile Money (South Africa) |
| Telkom | TKM | Telkom Mobile Money (South Africa) |
| Moov | MOV | Moov Africa Mobile Money (Benin, Burkina Faso, Côte d'Ivoire, Niger, Togo) |
| Orange | ORG | Orange Money (Senegal, Mali, Côte d'Ivoire, Cameroon, Madagascar) |
| Wave | WAV | Wave Mobile Money (Senegal, Côte d'Ivoire, Burkina Faso, Mali) |
| M-Pesa | MPE | M-Pesa (Kenya, Tanzania, DRC, Mozambique, Lesotho, Ghana, Egypt) |
| TNM | TNM | Telekom Networks Malawi Mobile Money (Malawi) |
| Movitel | MVT | Movitel Mobile Money (Mozambique) |
| Free | FRE | Free / Sonatel Mobile Money (Senegal and West Africa) |
| Halotel | HTL | Halotel (Viettel Tanzania) Mobile Money (Tanzania) |
| Zamtel | ZTL | Zamtel Mobile Money (Zambia) |
Integration Architecture
Third-party processors implement a REST API service that VantagePay's payment services will call to process transactions. The service runs within a private network (typically as a pod in a Kubernetes cluster) and requires no external security as it's only accessible by VantagePay's internal payment services.
Refer to the diagrams below to understand the typical flows that will occur for mobile wallet payments.
graph LR
A[VantagePay Payment Services] --> B[Your Service API]
B --> C[Mobile Wallet Providers]
subgraph "Internal Infrastructure"
B
end
subgraph "External Services"
C
endCredit Mobile Wallet Transaction Flow
The framework orchestrates all steps. Your service only needs to respond to each API call - it never needs to drive the flow itself.
sequenceDiagram
participant VP as VantagePay Payment Services
participant Service as Your Service API
participant MW as Mobile Wallet Provider
Note over VP,MW: Optional: Float Balance Check (framework-driven)
VP->>Service: POST /v1/vantagepay/mobile-money/credit/balance
Service-->>VP: Current float balance response
alt Balance < transaction amount
Note over VP: Framework queues transaction for delayed processing
Note over VP: Your service is NOT called until balance is sufficient
end
Note over VP,MW: Optional: Pre-transaction Validation
VP->>Service: POST /v1/vantagepay/mobile-money/credit/validate
Service->>Service: Validate request data
Service->>MW: Check operator availability
MW-->>Service: Availability response
Service-->>VP: Can process credit response
Note over VP,MW: Optional: Account Holder Lookup
VP->>Service: POST /v1/vantagepay/mobile-money/account-holder
Service->>MW: Lookup account holder name
MW-->>Service: Account holder name
Service-->>VP: Account holder response
Note over VP,MW: Main Transaction Processing
VP->>Service: POST /v1/vantagepay/mobile-money/credit/process
Service->>MW: Credit mobile wallet
MW-->>Service: Transaction result
Service-->>VP: Credit transaction response
Note over VP,MW: Optional: Status Polling Loop (if PENDING status returned)
alt Transaction Status is PENDING
loop Poll until non-PENDING status
VP->>Service: POST /v1/vantagepay/mobile-money/credit/status
Service->>MW: Check transaction status
MW-->>Service: Current status
Service-->>VP: Status response
alt Status still PENDING
VP->>VP: Pause until next check interval
else Status change (SUCCESS/FAILED/etc)
Note over VP: Exit polling loop
end
end
endDelayed Payment Behaviour
The delayed-payment flow is entirely framework-driven and transparent to your service. When BalanceCheckEnabled is true and SupportsDelayedTransactions is true:
- The framework calls
credit/balancebefore each credit attempt. - If the returned balance is less than the transaction amount, the framework queues the transaction for later - your service never receives the
credit/processcall. - The framework periodically replays the queued transaction, calling
credit/balanceagain each time, until sufficient balance is available. - If the total in-flight delayed liability for your processor would exceed
MaxAllowableDelayedLiabilityInCents, the transaction is failed rather than queued and a manual refund is required.
Core Data Models
Enumerations
TransactionStatus
Status of a credit transaction. Used by credit/process and credit/status responses.
| Value | Numeric | Description |
|---|---|---|
Success | 1 | Transaction completed successfully - funds credited to wallet |
Failed | 2 | Transaction failed; the provider rejected or could not complete the credit |
Pending | 4 | Status unknown - VantagePay will poll credit/status until a final status is received |
Declined | 6 | Transaction explicitly declined by the mobile wallet provider |
ValidationError | 7 | Failed due to validation issues (invalid MSISDN, operator not supported, etc.) |
InsufficientFunds | 11 | Credit could not be completed due to insufficient float/balance at the provider |
The numeric values are stable across API versions and safe to use as integer keys if your language does not support string enums.
FeeType
Types of fees that can be associated with a credit transaction. Returned in the fees map on credit/process responses.
| Value | Numeric | Description |
|---|---|---|
ProcessingFee | 1 | Fee charged to VantagePay for processing (not passed to the consumer) |
UserFee | 3 | Fee charged to the user (added to the credit amount sent to the wallet) |
Commission | 4 | Commission fee deducted from the credit amount |
The numeric values are stable across API versions and safe to use as integer keys if your language does not support string enums.
API Endpoints
1. Float Balance Check (Optional)
Endpoint: POST /v1/vantagepay/mobile-money/credit/balance
Purpose: Returns the current float (funding) balance available to credit mobile wallets. The framework uses this to decide whether to attempt an immediate credit or queue the transaction for delayed processing. It is also used for monitoring and alerting on low balance conditions.
Configuration: Called automatically when both BalanceCheckEnabled and SupportsDelayedTransactions are true. Periodically checked for monitoring purposes.
Important
You do not decide whether to delay - only report the current balance. The framework makes all delay decisions based on MaxAllowableDelayedLiabilityInCents.
Request Model: GetMobileWalletFloatBalanceRequest
{
"currency": "GHS",
"mobileWalletOperator": "MTN",
"country": "GHA"
}| Field | Type | Required | Description |
|---|---|---|---|
currency | Currency (ISO 4217) | Yes | Currency for which to check the float balance, e.g. "GHS" |
mobileWalletOperator | MobileWalletOperator? | No | Operator code (see Supported Mobile Wallet Operators) for which to check the balance, if per-operator floats are maintained |
country | Country? (ISO 3166-1 alpha-3) | No | Country of the consumer associated with the transaction, e.g. "GHA". May be null when the consumer's country is unknown |
Response Model: GetMobileWalletFloatBalanceResponse
{
"balanceInCents": 5000000,
"currency": "GHS"
}| Field | Type | Description |
|---|---|---|
balanceInCents | long | Current available float balance in cents |
currency | Currency | Currency of the balance |
Implementation Examples
# FastAPI
@app.post("/v1/vantagepay/mobile-money/credit/balance")
async def get_mobile_wallet_float_balance(
request: GetMobileWalletFloatBalanceRequest
) -> GetMobileWalletFloatBalanceResponse:
try:
balance = await get_float_balance(request.currency, request.mobileWalletOperator)
return GetMobileWalletFloatBalanceResponse(
balanceInCents=balance.balance_in_cents,
currency=request.currency
)
except Exception as e:
logger.warning(f"Balance check failed: {str(e)}")
# Return 0 to signal no available float - the framework will re-queue
return GetMobileWalletFloatBalanceResponse(
balanceInCents=0,
currency=request.currency
)// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/credit/balance")
public GetMobileWalletFloatBalanceResponse getFloatBalance(
@RequestBody GetMobileWalletFloatBalanceRequest request) {
try {
FloatBalance balance = floatBalanceService.getBalance(
request.getCurrency(),
request.getMobileWalletOperator()
);
return GetMobileWalletFloatBalanceResponse.builder()
.balanceInCents(balance.getBalanceInCents())
.currency(request.getCurrency())
.build();
} catch (Exception e) {
logger.warn("Balance check failed: " + e.getMessage());
// Return 0 to signal no available float - the framework will re-queue
return GetMobileWalletFloatBalanceResponse.builder()
.balanceInCents(0L)
.currency(request.getCurrency())
.build();
}
}// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/credit/balance")]
public async Task<GetMobileWalletFloatBalanceResponse> GetFloatBalance(
[FromBody] GetMobileWalletFloatBalanceRequest request)
{
try
{
var balance = await _floatBalanceService.GetBalanceAsync(
request.Currency,
request.MobileWalletOperator
);
return new GetMobileWalletFloatBalanceResponse
{
BalanceInCents = balance.BalanceInCents,
Currency = request.Currency
};
}
catch (Exception ex)
{
_logger.LogWarning("Balance check failed: {Message}", ex.Message);
// Return 0 to signal no available float - the framework will re-queue
return new GetMobileWalletFloatBalanceResponse
{
BalanceInCents = 0,
Currency = request.Currency
};
}
}// Express
app.post('/v1/vantagepay/mobile-money/credit/balance', async (req, res) => {
const { currency, mobileWalletOperator } = req.body;
try {
const balance = await getFloatBalance(currency, mobileWalletOperator);
res.json({
balanceInCents: balance.balanceInCents,
currency
});
} catch (error) {
console.warn('Balance check failed:', error.message);
// Return 0 to signal no available float - the framework will re-queue
res.json({
balanceInCents: 0,
currency
});
}
});2. Transaction Validation (Optional)
Endpoint: POST /v1/vantagepay/mobile-money/credit/validate
Purpose: Pre-validates whether a credit can be processed before attempting the actual transaction. It is not necessary to check for supported operators, currency, country or minimum/maximum transaction amounts as these are checked by VantagePay based on processor configuration.
Configuration: Enabled when RequestValidationCheckEnabled setting is true.
Request Model: ValidateMobileWalletCreditRequest
{
"amountInCents": 50000,
"currency": "GHS",
"mobileWalletOperator": "MTN",
"country": "GHA",
"msisdn": "233241234567"
}| Field | Type | Required | Description |
|---|---|---|---|
amountInCents | integer | Yes | Transaction amount in cents |
currency | Currency (ISO 4217) | Yes | Currency code, e.g. "GHS" |
mobileWalletOperator | MobileWalletOperator | Yes | Operator code (see Supported Mobile Wallet Operators) |
country | Country? (ISO 3166-1 alpha-3) | No | Country of the consumer, e.g. "GHA". May be null when unknown |
msisdn | string | Yes | Mobile number in international format (no leading + or 00), e.g. "233241234567" |
Response Model: ValidateMobileWalletCreditResponse
{
"canProcess": true,
"reason": null
}| Field | Type | Description |
|---|---|---|
canProcess | boolean | Whether the service can process this credit |
reason | string? | Optional reason to log if the credit cannot be processed |
Implementation Examples
# FastAPI
@app.post("/v1/vantagepay/mobile-money/credit/validate")
async def can_process_mobile-wallet-credit(
request: ValidateMobileWalletCreditRequest
) -> ValidateMobileWalletCreditResponse:
# Check operator availability, validate MSISDN format, etc.
operator_available = await check_operator_status(request.mobileWalletOperator)
valid_msisdn = validate_msisdn(request.msisdn)
if not operator_available:
return ValidateMobileWalletCreditResponse(
canProcess=False,
reason="Operator temporarily unavailable"
)
if not valid_msisdn:
return ValidateMobileWalletCreditResponse(
canProcess=False,
reason="Invalid MSISDN format"
)
return ValidateMobileWalletCreditResponse(canProcess=True)// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/credit/validate")
public ValidateMobileWalletCreditResponse validateMobileWalletCredit(
@RequestBody ValidateMobileWalletCreditRequest request) {
boolean operatorAvailable = checkOperatorStatus(request.getMobileWalletOperator());
boolean validMsisdn = validateMsisdn(request.getMsisdn());
if (!operatorAvailable) {
return new ValidateMobileWalletCreditResponse(false, "Operator temporarily unavailable");
}
if (!validMsisdn) {
return new ValidateMobileWalletCreditResponse(false, "Invalid MSISDN format");
}
return new ValidateMobileWalletCreditResponse(true, null);
}// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/credit/validate")]
public async Task<ValidateMobileWalletCreditResponse> ValidateMobileWalletCredit(
[FromBody] ValidateMobileWalletCreditRequest request)
{
var operatorAvailable = await CheckOperatorStatusAsync(request.MobileWalletOperator);
var validMsisdn = ValidateMsisdn(request.Msisdn);
if (!operatorAvailable)
{
return new ValidateMobileWalletCreditResponse
{
CanProcess = false,
Reason = "Operator temporarily unavailable"
};
}
if (!validMsisdn)
{
return new ValidateMobileWalletCreditResponse
{
CanProcess = false,
Reason = "Invalid MSISDN format"
};
}
return new ValidateMobileWalletCreditResponse { CanProcess = true };
}// Express
app.post('/v1/vantagepay/mobile-money/credit/validate', async (req, res) => {
const { amountInCents, currency, mobileWalletOperator, msisdn } = req.body;
const operatorAvailable = await checkOperatorStatus(mobileWalletOperator);
const validMsisdn = validateMsisdn(msisdn);
if (!operatorAvailable) {
return res.json({
canProcess: false,
reason: "Operator temporarily unavailable"
});
}
if (!validMsisdn) {
return res.json({
canProcess: false,
reason: "Invalid MSISDN format"
});
}
res.json({ canProcess: true });
});3. Account Holder Lookup (Optional, Shared)
Endpoint: POST /v1/vantagepay/mobile-money/account-holder
Purpose: Retrieves the account holder name associated with a mobile wallet, used for display and confirmation purposes during the payment process.
Configuration: Enabled when AccountHolderLookupEnabled setting is true.
TIP
This endpoint is shared with the Mobile Wallet Debit integration. If you implement both debit and credit, a single implementation serves both. See the Mobile Wallet Debit Integration Guide for the full request/response specification and code examples.
Quick Reference
// Request
{
"mobileWalletOperator": "MTN",
"msisdn": "233241234567"
}
// Response
{
"accountHolder": "John Doe"
}Return null for accountHolder if the name cannot be determined - VantagePay will fall back to displaying the MSISDN.
4. Process Credit Transaction (Required)
Endpoint: POST /v1/vantagepay/mobile-money/credit/process
Purpose: The main endpoint that credits funds to a mobile wallet. This is the only required endpoint.
⚠️ Important: Store or cache the transactionReference and map it to your internal provider reference for follow-up status checks. If a network timeout occurs before your response reaches VantagePay, the status check may rely on transactionReference alone to look up the outcome.
⚠️ Idempotency: If you receive a credit/process request for a transactionReference you have already successfully processed, return the original successful response. Do not credit the wallet twice.
Request Model: ProcessMobileWalletCreditTransactionRequest
{
"transactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
"amountInCents": 50000,
"currency": "GHS",
"mobileWalletOperator": "MTN",
"country": "GHA",
"msisdn": "233241234567",
"description": "Payment from Acme Corp",
"transactionMetaData": {
"correlationId": "CORR_ABC123",
"authCode": "AUTH_456789"
}
}| Field | Type | Required | Description |
|---|---|---|---|
transactionReference | guid | Yes | Unique transaction reference from VantagePay. Use this as your idempotency key |
amountInCents | integer | Yes | Transaction amount in cents |
currency | Currency (ISO 4217) | Yes | Currency code, e.g. "GHS" |
mobileWalletOperator | MobileWalletOperator | Yes | Operator code (see Supported Mobile Wallet Operators) |
country | Country? (ISO 3166-1 alpha-3) | No | Country of the consumer, e.g. "GHA". May be null when unknown |
msisdn | string | Yes | Mobile number in international format (no leading + or 00) |
accountHolder | string? | No | Account holder name if previously resolved via account-holder |
description | string? | No | Payment description/reference, may appear on the wallet statement |
transactionMetaData | Dict<string, string> | No | Additional debugging/tracking data previously set on the transaction |
Response Model: ProcessMobileWalletCreditTransactionResponse
{
"transactionStatus": "SUCCESS",
"processorReference": "PROC_12345678",
"processorMessage": "Credit processed successfully",
"fees": {
"ProcessingFee": 100
},
"transactionMetaData": {
"providerTransactionId": "MTN_TXN_987654",
"processingTime": "1250",
"authorizationCode": "AUTH_789"
}
}| Field | Type | Description |
|---|---|---|
transactionStatus | TransactionStatus | Final or intermediate status of the credit |
processorReference | string? | Your internal/provider reference for this transaction |
processorMessage | string? | Human-readable message describing the outcome |
fees | Dict<FeeType, int>? | Fees in cents associated with this credit, by fee type |
transactionMetaData | Dict<string, string>? | Additional data to store and round-trip with future requests |
Implementation Examples
# FastAPI
@app.post("/v1/vantagepay/mobile-money/credit/process")
async def process_mobile-wallet-credit_transaction(
request: ProcessMobileWalletCreditTransactionRequest
) -> ProcessMobileWalletCreditTransactionResponse:
# Check idempotency - return existing result if already processed
existing = await get_existing_result(request.transactionReference)
if existing:
return existing
# Store transaction reference mapping early for status checks
internal_ref = generate_internal_reference()
await store_transaction_mapping(request.transactionReference, internal_ref)
try:
result = await credit_mobile_wallet(
operator=request.mobileWalletOperator,
msisdn=request.msisdn,
amount=request.amountInCents,
currency=request.currency,
description=request.description,
reference=internal_ref
)
fees = {}
if result.processing_fee_in_cents > 0:
fees[FeeType.PROCESSING_FEE] = result.processing_fee_in_cents
return ProcessMobileWalletCreditTransactionResponse(
transactionStatus=TransactionStatus.SUCCESS,
processorReference=result.provider_reference,
processorMessage="Credit processed successfully",
fees=fees,
transactionMetaData={
"internal_ref": internal_ref,
"provider_transaction_id": result.provider_transaction_id,
"processing_time": str(result.processing_time_ms)
}
)
except InsufficientFundsError:
return ProcessMobileWalletCreditTransactionResponse(
transactionStatus=TransactionStatus.INSUFFICIENT_FUNDS,
processorMessage="Insufficient float balance"
)
except ProviderDeclinedError as e:
return ProcessMobileWalletCreditTransactionResponse(
transactionStatus=TransactionStatus.DECLINED,
processorMessage=e.message
)
except ProviderPendingError as e:
# Provider accepted the request but is processing asynchronously
return ProcessMobileWalletCreditTransactionResponse(
transactionStatus=TransactionStatus.PENDING,
processorReference=e.reference,
processorMessage="Credit is being processed",
transactionMetaData={"internal_ref": internal_ref}
)
except Exception as e:
return ProcessMobileWalletCreditTransactionResponse(
transactionStatus=TransactionStatus.FAILED,
processorMessage=str(e)
)// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/credit/process")
public ProcessMobileWalletCreditTransactionResponse processCreditTransaction(
@RequestBody ProcessMobileWalletCreditTransactionRequest request) {
// Check idempotency - return existing result if already processed
Optional<ProcessMobileWalletCreditTransactionResponse> existing =
idempotencyService.getExistingResult(request.getTransactionReference());
if (existing.isPresent()) {
return existing.get();
}
String internalRef = generateInternalReference();
transactionMappingService.store(request.getTransactionReference(), internalRef);
try {
CreditResult result = mobileWalletService.creditWallet(
request.getMobileWalletOperator(),
request.getMsisdn(),
request.getAmountInCents(),
request.getCurrency(),
request.getDescription(),
internalRef
);
Map<FeeType, Integer> fees = new HashMap<>();
if (result.getProcessingFeeInCents() > 0) {
fees.put(FeeType.PROCESSING_FEE, result.getProcessingFeeInCents());
}
Map<String, String> metaData = new HashMap<>();
metaData.put("internal_ref", internalRef);
metaData.put("provider_transaction_id", result.getProviderTransactionId());
return ProcessMobileWalletCreditTransactionResponse.builder()
.transactionStatus(TransactionStatus.SUCCESS)
.processorReference(result.getProviderReference())
.processorMessage("Credit processed successfully")
.fees(fees)
.transactionMetaData(metaData)
.build();
} catch (InsufficientFundsException e) {
return ProcessMobileWalletCreditTransactionResponse.builder()
.transactionStatus(TransactionStatus.INSUFFICIENT_FUNDS)
.processorMessage("Insufficient float balance")
.build();
} catch (ProviderDeclinedException e) {
return ProcessMobileWalletCreditTransactionResponse.builder()
.transactionStatus(TransactionStatus.DECLINED)
.processorMessage(e.getMessage())
.build();
} catch (ProviderPendingException e) {
return ProcessMobileWalletCreditTransactionResponse.builder()
.transactionStatus(TransactionStatus.PENDING)
.processorReference(e.getReference())
.processorMessage("Credit is being processed")
.build();
}
}// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/credit/process")]
public async Task<ProcessMobileWalletCreditTransactionResponse> ProcessCreditTransaction(
[FromBody] ProcessMobileWalletCreditTransactionRequest request)
{
// Check idempotency - return existing result if already processed
var existing = await _idempotencyService.GetExistingResultAsync(request.TransactionReference);
if (existing != null)
{
return existing;
}
var internalRef = GenerateInternalReference();
await _transactionMappingService.StoreAsync(request.TransactionReference, internalRef);
try
{
var result = await _mobileWalletService.CreditWalletAsync(
request.MobileWalletOperator,
request.Msisdn,
request.AmountInCents,
request.Currency,
request.Description,
internalRef
);
var fees = new Dictionary<FeeType, int>();
if (result.ProcessingFeeInCents > 0)
{
fees[FeeType.ProcessingFee] = result.ProcessingFeeInCents;
}
return new ProcessMobileWalletCreditTransactionResponse
{
TransactionStatus = TransactionStatus.Success,
ProcessorReference = result.ProviderReference,
ProcessorMessage = "Credit processed successfully",
Fees = fees,
TransactionMetaData = new Dictionary<string, string>
{
{ "internal_ref", internalRef },
{ "provider_transaction_id", result.ProviderTransactionId }
}
};
}
catch (InsufficientFundsException)
{
return new ProcessMobileWalletCreditTransactionResponse
{
TransactionStatus = TransactionStatus.InsufficientFunds,
ProcessorMessage = "Insufficient float balance"
};
}
catch (ProviderDeclinedException ex)
{
return new ProcessMobileWalletCreditTransactionResponse
{
TransactionStatus = TransactionStatus.Declined,
ProcessorMessage = ex.Message
};
}
catch (ProviderPendingException ex)
{
return new ProcessMobileWalletCreditTransactionResponse
{
TransactionStatus = TransactionStatus.Pending,
ProcessorReference = ex.Reference,
ProcessorMessage = "Credit is being processed",
TransactionMetaData = new Dictionary<string, string> { { "internal_ref", internalRef } }
};
}
}// Express
app.post('/v1/vantagepay/mobile-money/credit/process', async (req, res) => {
const {
transactionReference, amountInCents, currency,
mobileWalletOperator, msisdn, description, transactionMetaData
} = req.body;
// Check idempotency - return existing result if already processed
const existing = await getExistingResult(transactionReference);
if (existing) {
return res.json(existing);
}
const internalRef = generateInternalReference();
await storeTransactionMapping(transactionReference, internalRef);
try {
const result = await creditMobileWallet({
operator: mobileWalletOperator,
msisdn,
amount: amountInCents,
currency,
description,
reference: internalRef
});
const fees = {};
if (result.processingFeeInCents > 0) {
fees['ProcessingFee'] = result.processingFeeInCents;
}
res.json({
transactionStatus: 'SUCCESS',
processorReference: result.providerReference,
processorMessage: 'Credit processed successfully',
fees: fees,
transactionMetaData: {
internal_ref: internalRef,
provider_transaction_id: result.providerTransactionId,
processing_time: result.processingTimeMs.toString()
}
});
} catch (error) {
if (error instanceof InsufficientFundsError) {
res.json({
transactionStatus: 'INSUFFICIENT_FUNDS',
processorMessage: 'Insufficient float balance'
});
} else if (error instanceof ProviderDeclinedError) {
res.json({
transactionStatus: 'DECLINED',
processorMessage: error.message
});
} else if (error instanceof ProviderPendingError) {
res.json({
transactionStatus: 'PENDING',
processorReference: error.reference,
processorMessage: 'Credit is being processed',
transactionMetaData: { internal_ref: internalRef }
});
} else {
res.json({
transactionStatus: 'FAILED',
processorMessage: error.message
});
}
}
});5. Transaction Status Check (Optional)
Endpoint: POST /v1/vantagepay/mobile-money/credit/status
Purpose: Checks the current status of a credit transaction. Called by VantagePay when the initial credit/process response had a Pending, TimedOut, or Indeterminate outcome, and continues to poll until a final status is returned.
Configuration: Enabled via TransactionStatusCheckEnabled setting.
Request Model: GetMobileWalletCreditTransactionStatusRequest
{
"transactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
"processorReference": "PROC_12345678",
"transactionMetaData": {
"providerTransactionId": "MTN_TXN_987654",
"processingTime": "1250",
"authorizationCode": "AUTH_789"
}
}| Field | Type | Description |
|---|---|---|
transactionReference | guid | Original transaction reference from credit/process |
processorReference | string? | Your internal/provider reference from credit/process, if it reached VantagePay |
transactionMetaData | Dict<string, string>? | Additional debugging/tracking data previously set on the transaction |
Response Model: GetMobileWalletCreditTransactionStatusResponse
{
"transactionStatus": "SUCCESS",
"processorReference": "PROC_12345678",
"processorMessage": "Credit confirmed by provider",
"transactionMetaData": {
"lookupId": "MTN_LKP_987654",
}
}| Field | Type | Description |
|---|---|---|
transactionStatus | TransactionStatus | Current status |
processorReference | string? | Your internal/provider reference |
processorMessage | string? | Human-readable status message |
transactionMetaData | Dict<string, string>? | Additional data to store and round-trip with future requests |
Implementation Examples
# FastAPI
@app.post("/v1/vantagepay/mobile-money/credit/status")
async def get_mobile-wallet-credit_transaction_status(
request: GetMobileWalletCreditTransactionStatusRequest
) -> GetMobileWalletCreditTransactionStatusResponse:
try:
# Use processorReference if available, fall back to transactionReference lookup
internal_ref = (
request.processorReference
or await get_internal_reference(request.transactionReference)
)
status = await check_credit_status(internal_ref)
return GetMobileWalletCreditTransactionStatusResponse(
transactionStatus=status.transaction_status,
processorReference=status.processor_reference,
processorMessage=status.message
)
except TransactionNotFound:
return GetMobileWalletCreditTransactionStatusResponse(
transactionStatus=TransactionStatus.FAILED,
processorMessage="Transaction not found"
)// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/credit/status")
public GetMobileWalletCreditTransactionStatusResponse getCreditTransactionStatus(
@RequestBody GetMobileWalletCreditTransactionStatusRequest request) {
try {
String internalRef = request.getProcessorReference() != null
? request.getProcessorReference()
: transactionMappingService.getInternalReference(request.getTransactionReference());
CreditStatus status = mobileWalletService.checkCreditStatus(internalRef);
return GetMobileWalletCreditTransactionStatusResponse.builder()
.transactionStatus(status.getTransactionStatus())
.processorReference(status.getProcessorReference())
.processorMessage(status.getMessage())
.build();
} catch (TransactionNotFoundException e) {
return GetMobileWalletCreditTransactionStatusResponse.builder()
.transactionStatus(TransactionStatus.FAILED)
.processorMessage("Transaction not found")
.build();
}
}// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/credit/status")]
public async Task<GetMobileWalletCreditTransactionStatusResponse> GetCreditTransactionStatus(
[FromBody] GetMobileWalletCreditTransactionStatusRequest request)
{
try
{
var internalRef = request.ProcessorReference
?? await _transactionMappingService.GetInternalReferenceAsync(
request.TransactionReference);
var status = await _mobileWalletService.CheckCreditStatusAsync(internalRef);
return new GetMobileWalletCreditTransactionStatusResponse
{
TransactionStatus = status.TransactionStatus,
ProcessorReference = status.ProcessorReference,
ProcessorMessage = status.Message
};
}
catch (TransactionNotFoundException)
{
return new GetMobileWalletCreditTransactionStatusResponse
{
TransactionStatus = TransactionStatus.Failed,
ProcessorMessage = "Transaction not found"
};
}
}// Express
app.post('/v1/vantagepay/mobile-money/credit/status', async (req, res) => {
const { transactionReference, processorReference } = req.body;
try {
const internalRef = processorReference
|| await getInternalReference(transactionReference);
const status = await checkCreditStatus(internalRef);
res.json({
transactionStatus: status.transactionStatus,
processorReference: status.processorReference,
processorMessage: status.message
});
} catch (error) {
if (error instanceof TransactionNotFoundError) {
res.json({
transactionStatus: 'FAILED',
processorMessage: 'Transaction not found'
});
} else {
throw error;
}
}
});6. Callbacks / Webhooks
If your implementation uses asynchronous operations that require callbacks or webhooks, you can use an externally facing URL that VantagePay will forward to your service on specific endpoints.
External callback URLs are in the format /v1/vantagepay/mobile-money/credit/{configName} and accept both GET and POST methods.
The path is rewritten by VantagePay, the query string, headers and request body are forwarded to your service on the internal URLs below.
Credit Endpoint: POST/GET /v1/vantagepay/mobile-money/credit/callback
Purpose: Receives the exact callback payload and query parameters forwarded from the external callback URL.
# FastAPI
@app.get("/v1/vantagepay/mobile-money/credit/callback")
@app.post("/v1/vantagepay/mobile-money/credit/callback")
async def mobile-wallet-credit_callback(request: Request):
body = await request.json() if request.method == "POST" else {}
params = dict(request.query_params)
# Handle callback - update transaction status, notify internal systems, etc.
transaction_ref = params.get("transactionReference")
await handle_provider_callback(transaction_ref, body)
return {"status": "callback received"}// Spring Boot
@GetMapping("/v1/vantagepay/mobile-money/credit/callback")
@PostMapping("/v1/vantagepay/mobile-money/credit/callback")
public ResponseEntity<Void> creditCallback(
@RequestParam Map<String, String> params,
@RequestBody(required = false) Map<String, Object> body) {
String transactionRef = params.get("transactionReference");
callbackHandlerService.handleCreditCallback(transactionRef, body, params);
return ResponseEntity.ok().build();
}// ASP.NET
[HttpGet("/v1/vantagepay/mobile-money/credit/callback")]
[HttpPost("/v1/vantagepay/mobile-money/credit/callback")]
public async Task CreditCallback()
{
var transactionRef = Request.Query["transactionReference"].ToString();
// Handle provider callback - update status, fire internal events, etc.
await _callbackHandlerService.HandleCreditCallbackAsync(transactionRef, Request);
}// Express
app.get('/v1/vantagepay/mobile-money/credit/callback', async (req, res) => {
const { transactionReference } = req.query;
await handleProviderCallback(transactionReference, {}, req.query);
res.json({ status: 'callback received' });
});
app.post('/v1/vantagepay/mobile-money/credit/callback', async (req, res) => {
const { transactionReference } = req.query;
await handleProviderCallback(transactionReference, req.body, req.query);
res.json({ status: 'callback received' });
});Configuration Reference
Your processor is configured by settings supplied to VantagePay. The framework reads these at startup to decide which optional endpoints to call and how to behave.
{
"Name": "my-credit-processor",
"BaseUrl": "http://my-credit-service:8080/",
"MinimumTransactionAmountInCents": 100,
"MaximumTransactionAmountInCents": 100000000,
"SupportedCountries": "GHA",
"SupportedCurrencies": "GHS",
"SupportedNetworks": "MTN,ATG,GMY",
"SupportsDelayedTransactions": true,
"RequestValidationCheckEnabled": true,
"AccountHolderLookupEnabled": true,
"TransactionStatusCheckEnabled": true,
"BalanceCheckEnabled": true,
"MaxAllowableDelayedLiabilityInCents": 10000000
}| Setting | Type | Default | Description |
|---|---|---|---|
Name | string | - | Unique processor name; must match the registered HTTP client name |
BaseUrl | string | - | Base URL of your third-party service |
MinimumTransactionAmountInCents | int | 1 | Minimum transaction amount this processor will accept |
MaximumTransactionAmountInCents | int | int.MaxValue | Maximum transaction amount this processor will accept |
SupportedCountries | string? | null | Comma-separated ISO 3166-1 alpha-3 country codes (null or empty = all) |
SupportedCurrencies | string? | null | Comma-separated ISO 4217 currency codes (null or empty = all) |
SupportedNetworks | string? | null | Comma-separated operator codes (null or empty = all). Use the canonical codes shown in Supported Mobile Wallet Operators |
SupportsDelayedTransactions | bool | false | Master switch for delayed-payment queuing |
RequestValidationCheckEnabled | bool | false | Enables the credit/validate call before processing |
AccountHolderLookupEnabled | bool | false | Enables the account-holder call before processing |
TransactionStatusCheckEnabled | bool | false | Enables credit/status polling when the initial response is Pending, TimedOut, or Indeterminate |
BalanceCheckEnabled | bool | false | Enables the credit/balance call before processing; requires SupportsDelayedTransactions: true |
MaxAllowableDelayedLiabilityInCents | int | 0 | Maximum total in-flight delayed liability in cents. 0 disables the cap (not recommended for production) |
Your own settings and secrets should not be baked into the Docker image. VantagePay stores these securely and exposes them to your service via environment variables.
Operational Notes
Polling Behaviour
When credit/process returns Pending, TimedOut, or Indeterminate (and TransactionStatusCheckEnabled is true), VantagePay:
- Schedules a
credit/statuscall approximately 10 seconds after the initial response. - Polls every ~2 seconds while the status remains
PendingorTimedOut. - Exits the polling loop and finalises the transaction once any other status is received, when the parent transaction batch completes/expires, or when the host shuts down.
- Forces the status to
Indeterminateif the loop exits while the status is stillPending(treated as a final failure).
If TransactionStatusCheckEnabled is false the framework cannot recover an indeterminate outcome and the transaction is finalised as Unrecoverable.
Delayed Payment Processing
- The framework checks
credit/balancebefore callingcredit/processwhen bothBalanceCheckEnabledandSupportsDelayedTransactionsaretrue. - If the balance is insufficient and the in-flight liability is within
MaxAllowableDelayedLiabilityInCents, the transaction is placed on a retry queue (30-minute interval). - Each retry calls
credit/balanceagain before attemptingcredit/process. - If the in-flight liability cap would be exceeded, the transaction is failed rather than queued, and a manual refund of the source funds is required.
Note
The in-flight liability counter is held in Redis and shared across all processor instances, so the cap is enforced consistently across pods and survives restarts. The counter is incremented when a transaction is queued and decremented after it has been dispatched from the queue.
Status After Timeout or Connection Failure
If your service does not respond within the HTTP timeout (4 minutes), VantagePay records the transaction as TimedOut and - if TransactionStatusCheckEnabled is true - immediately queues a credit/status poll. This poll will continue until your service confirms a final status or the batch expires.
If TransactionStatusCheckEnabled is false and a timeout occurs, the transaction is marked Unrecoverable and the batch is failed. It is strongly recommended to enable TransactionStatusCheckEnabled for all production processors to avoid false failures caused by provider latency.
Error Handling Best Practices
Returning the Right Status
Pick the most specific TransactionStatus value for the situation - VantagePay maps your status onto an internal transaction state and decides whether to settle, fail, or poll.
| Scenario | Return |
|---|---|
| Credit confirmed by provider | Success |
| Provider rejected the credit | Declined |
| Provider acknowledged but hasn't confirmed yet | Pending |
| MSISDN invalid / operator not supported | ValidationError |
| Your float is too low at the provider level | InsufficientFunds |
| Unknown outcome - network issue with provider | Pending (if you can poll) or Failed |
Metadata Best Practices
Include all information you might need to look up this transaction during a credit/status call:
metaData = {
"internal_reference": internal_ref,
"provider_correlation_id": provider_response.correlation_id,
"provider_response_code": provider_response.response_code,
"processing_time_ms": str(processing_time),
"api_version": "v2.1"
}Map<String, String> metaData = new HashMap<>();
metaData.put("internal_reference", internalRef);
metaData.put("provider_correlation_id", providerResponse.getCorrelationId());
metaData.put("provider_response_code", providerResponse.getResponseCode());
metaData.put("processing_time_ms", String.valueOf(processingTime));
metaData.put("api_version", "v2.1");var metaData = new Dictionary<string, string>
{
{ "internal_reference", internalRef },
{ "provider_correlation_id", providerResponse.CorrelationId },
{ "provider_response_code", providerResponse.ResponseCode },
{ "processing_time_ms", processingTime.ToString() },
{ "api_version", "v2.1" }
};const metaData = {
internal_reference: internalRef,
provider_correlation_id: providerResponse.correlationId,
provider_response_code: providerResponse.responseCode,
processing_time_ms: String(processingTime),
api_version: "v2.1"
};Metadata is round-tripped in all subsequent calls (credit/status, replayed credit/process after delay) so you can use it to resume state without a database lookup.
Fee Calculation
Properly calculate and return fees if necessary:
def calculate_fees(amount_in_cents, operator):
fees = {}
# Processing fee charged to VantagePay
fees[FeeType.PROCESSING_FEE] = calculate_processing_fee(amount_in_cents, operator)
# User fee added to debit amount
if has_user_fee(operator):
fees[FeeType.USER_FEE] = calculate_user_fee(amount_in_cents, operator)
# Commission subtracted from amount
if has_commission(operator):
fees[FeeType.COMMISSION] = calculate_commission(amount_in_cents, operator)
return feesTesting Your Integration
VantagePay provides a service-tester tool that issues each of the endpoint contracts described above with sample payloads. Coordinate with the VantagePay technical team to:
- Provision a test environment that points at your service
BaseUrl. - Run the tester against each endpoint your processor settings have enabled.
- Verify that your service handles repeat
credit/processcalls (idempotency).
Deployment Considerations
Docker Configuration Example
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Health Probes
Implement liveness and readiness check endpoints for monitoring:
@app.get("/health/liveness")
async def liveness_check():
return {"status": "healthy"}
@app.get("/health/ready")
async def readiness_check():
# Check provider connectivity and float balance
balance = await get_float_balance("GHS", None)
return {"status": "ready", "floatBalanceInCents": balance.balance_in_cents}@RestController
@RequestMapping("/health")
public class HealthController {
private final FloatBalanceService floatBalanceService;
public HealthController(FloatBalanceService floatBalanceService) {
this.floatBalanceService = floatBalanceService;
}
@GetMapping("/liveness")
public Map<String, Object> livenessCheck() {
return Map.of("status", "healthy");
}
@GetMapping("/ready")
public Map<String, Object> readinessCheck() {
FloatBalance balance = floatBalanceService.getBalance("GHS", null);
return Map.of(
"status", "ready",
"floatBalanceInCents", balance.getBalanceInCents()
);
}
}app.MapGet("/health/liveness", () => Results.Ok(new
{
status = "healthy"
}));
app.MapGet("/health/ready", async (IFloatBalanceService balanceService) =>
{
var balance = await balanceService.GetBalanceAsync("GHS", null);
return Results.Ok(new
{
status = "ready",
floatBalanceInCents = balance.BalanceInCents
});
});app.get("/health/liveness", (_req, res) => {
res.json({ status: "healthy" });
});
app.get("/health/ready", async (_req, res) => {
const balance = await floatBalanceService.getBalance("GHS", null);
res.json({
status: "ready",
floatBalanceInCents: balance.balanceInCents
});
});Secrets Management
Do not embed API keys or provider credentials in your Docker image. VantagePay will inject secrets via environment variables:
import os
PROVIDER_API_KEY = os.environ["PROVIDER_API_KEY"]
PROVIDER_BASE_URL = os.environ["PROVIDER_BASE_URL"]@Value("${PROVIDER_API_KEY}")
private String providerApiKey;
@Value("${PROVIDER_BASE_URL}")
private String providerBaseUrl;var providerApiKey = Environment.GetEnvironmentVariable("PROVIDER_API_KEY")
?? throw new InvalidOperationException("PROVIDER_API_KEY is not set.");
var providerBaseUrl = Environment.GetEnvironmentVariable("PROVIDER_BASE_URL")
?? throw new InvalidOperationException("PROVIDER_BASE_URL is not set.");const providerApiKey = process.env.PROVIDER_API_KEY;
const providerBaseUrl = process.env.PROVIDER_BASE_URL;
if (!providerApiKey || !providerBaseUrl) {
throw new Error("Required provider environment variables are missing.");
}Logging
Log structured data for every transaction to aid troubleshooting:
logger.info(
"Credit transaction processed",
extra={
"transactionReference": str(request.transactionReference),
"msisdn": request.msisdn,
"amountInCents": request.amountInCents,
"status": result.transactionStatus,
"processorReference": result.processorReference
}
)private static final Logger logger = LoggerFactory.getLogger(CreditController.class);
logger.info(
"Credit transaction processed transactionReference={} msisdn={} amountInCents={} status={} processorReference={}",
request.getTransactionReference(),
request.getMsisdn(),
request.getAmountInCents(),
result.getTransactionStatus(),
result.getProcessorReference()
);logger.LogInformation(
"Credit transaction processed {@TransactionLog}",
new
{
transactionReference = request.TransactionReference,
msisdn = request.Msisdn,
amountInCents = request.AmountInCents,
status = result.TransactionStatus,
processorReference = result.ProcessorReference
}
);logger.info("Credit transaction processed", {
transactionReference: request.transactionReference,
msisdn: request.msisdn,
amountInCents: request.amountInCents,
status: result.transactionStatus,
processorReference: result.processorReference
});Security
Never log full MSISDN digits in production if your compliance requirements restrict this. Mask the middle digits: 2332****7890.
Security Considerations
Your service runs in a private Kubernetes network accessible only by VantagePay internal services, so mutual TLS or API key authentication between VantagePay and your service is not required. However, apply standard internal security practices:
- Input validation - Validate all incoming request fields before forwarding to your provider.
- Secrets management - Use environment variables, not hardcoded values, for provider API keys.
- Audit logging - Log all requests and responses (excluding PINs and full card numbers) for audit trails.
- Provider security - Use HTTPS with valid certificates for all outbound calls to mobile wallet providers.
Support and Integration
For integration support and questions about the VantagePay Payment Service Framework, contact the VantagePay technical team with:
- Your service implementation details and the language/framework you are using
- Sample request/response logs for any failing calls
- The specific error messages or status codes you are receiving
- Your mobile wallet operator and country/currency configuration requirements
This documentation covers the Mobile Wallet Credit integration. Additional documentation is available for other payment types including credit cards, bank accounts, and digital wallets.
