Skip to content

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.

OperatorCode your service receivesDescription
MTNMTNMTN Mobile Money (Ghana, South Africa, and other African markets)
VodafoneVDFVodafone Mobile Money (Ghana)
AirtelATLAirtel Mobile Money
TigoTGOTigo Mobile Money (Ghana)
AT (AirtelTigo)ATGAirtelTigo Mobile Money (Ghana)
G-MoneyGMYG-Money Wallet (GCB Bank, Ghana)
ZeepayZPYZeepay Wallet (Ghana)
GhanaPayGHPGhanaPay (interoperable scheme, Ghana)
TelecelTCGTelecel Mobile Money (formerly Vodafone Ghana)
VodacomVDCVodacom Mobile Money (South Africa)
Cell CCLCCell C Mobile Money (South Africa)
TelkomTKMTelkom Mobile Money (South Africa)
MoovMOVMoov Africa Mobile Money (Benin, Burkina Faso, Côte d'Ivoire, Niger, Togo)
OrangeORGOrange Money (Senegal, Mali, Côte d'Ivoire, Cameroon, Madagascar)
WaveWAVWave Mobile Money (Senegal, Côte d'Ivoire, Burkina Faso, Mali)
M-PesaMPEM-Pesa (Kenya, Tanzania, DRC, Mozambique, Lesotho, Ghana, Egypt)
TNMTNMTelekom Networks Malawi Mobile Money (Malawi)
MovitelMVTMovitel Mobile Money (Mozambique)
FreeFREFree / Sonatel Mobile Money (Senegal and West Africa)
HalotelHTLHalotel (Viettel Tanzania) Mobile Money (Tanzania)
ZamtelZTLZamtel 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.

mermaid
graph LR
    A[VantagePay Payment Services] --> B[Your Service API]
    B --> C[Mobile Wallet Providers]

    subgraph "Internal Infrastructure"
        B
    end

    subgraph "External Services"
        C
    end

Credit 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.

mermaid
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
    end

Delayed Payment Behaviour

The delayed-payment flow is entirely framework-driven and transparent to your service. When BalanceCheckEnabled is true and SupportsDelayedTransactions is true:

  1. The framework calls credit/balance before each credit attempt.
  2. If the returned balance is less than the transaction amount, the framework queues the transaction for later - your service never receives the credit/process call.
  3. The framework periodically replays the queued transaction, calling credit/balance again each time, until sufficient balance is available.
  4. 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.

ValueNumericDescription
Success1Transaction completed successfully - funds credited to wallet
Failed2Transaction failed; the provider rejected or could not complete the credit
Pending4Status unknown - VantagePay will poll credit/status until a final status is received
Declined6Transaction explicitly declined by the mobile wallet provider
ValidationError7Failed due to validation issues (invalid MSISDN, operator not supported, etc.)
InsufficientFunds11Credit 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.

ValueNumericDescription
ProcessingFee1Fee charged to VantagePay for processing (not passed to the consumer)
UserFee3Fee charged to the user (added to the credit amount sent to the wallet)
Commission4Commission 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

json
{
  "currency": "GHS",
  "mobileWalletOperator": "MTN",
  "country": "GHA"
}
FieldTypeRequiredDescription
currencyCurrency (ISO 4217)YesCurrency for which to check the float balance, e.g. "GHS"
mobileWalletOperatorMobileWalletOperator?NoOperator code (see Supported Mobile Wallet Operators) for which to check the balance, if per-operator floats are maintained
countryCountry? (ISO 3166-1 alpha-3)NoCountry of the consumer associated with the transaction, e.g. "GHA". May be null when the consumer's country is unknown

Response Model: GetMobileWalletFloatBalanceResponse

json
{
  "balanceInCents": 5000000,
  "currency": "GHS"
}
FieldTypeDescription
balanceInCentslongCurrent available float balance in cents
currencyCurrencyCurrency of the balance

Implementation Examples

python
# 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
        )
java
// 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();
    }
}
csharp
// 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
        };
    }
}
javascript
// 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

json
{
  "amountInCents": 50000,
  "currency": "GHS",
  "mobileWalletOperator": "MTN",
  "country": "GHA",
  "msisdn": "233241234567"
}
FieldTypeRequiredDescription
amountInCentsintegerYesTransaction amount in cents
currencyCurrency (ISO 4217)YesCurrency code, e.g. "GHS"
mobileWalletOperatorMobileWalletOperatorYesOperator code (see Supported Mobile Wallet Operators)
countryCountry? (ISO 3166-1 alpha-3)NoCountry of the consumer, e.g. "GHA". May be null when unknown
msisdnstringYesMobile number in international format (no leading + or 00), e.g. "233241234567"

Response Model: ValidateMobileWalletCreditResponse

json
{
  "canProcess": true,
  "reason": null
}
FieldTypeDescription
canProcessbooleanWhether the service can process this credit
reasonstring?Optional reason to log if the credit cannot be processed

Implementation Examples

python
# 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)
java
// 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);
}
csharp
// 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 };
}
javascript
// 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

json
// 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

json
{
  "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"
  }
}
FieldTypeRequiredDescription
transactionReferenceguidYesUnique transaction reference from VantagePay. Use this as your idempotency key
amountInCentsintegerYesTransaction amount in cents
currencyCurrency (ISO 4217)YesCurrency code, e.g. "GHS"
mobileWalletOperatorMobileWalletOperatorYesOperator code (see Supported Mobile Wallet Operators)
countryCountry? (ISO 3166-1 alpha-3)NoCountry of the consumer, e.g. "GHA". May be null when unknown
msisdnstringYesMobile number in international format (no leading + or 00)
accountHolderstring?NoAccount holder name if previously resolved via account-holder
descriptionstring?NoPayment description/reference, may appear on the wallet statement
transactionMetaDataDict<string, string>NoAdditional debugging/tracking data previously set on the transaction

Response Model: ProcessMobileWalletCreditTransactionResponse

json
{
  "transactionStatus": "SUCCESS",
  "processorReference": "PROC_12345678",
  "processorMessage": "Credit processed successfully",
  "fees": {
    "ProcessingFee": 100
  },
  "transactionMetaData": {
    "providerTransactionId": "MTN_TXN_987654",
    "processingTime": "1250",
    "authorizationCode": "AUTH_789"
  }
}
FieldTypeDescription
transactionStatusTransactionStatusFinal or intermediate status of the credit
processorReferencestring?Your internal/provider reference for this transaction
processorMessagestring?Human-readable message describing the outcome
feesDict<FeeType, int>?Fees in cents associated with this credit, by fee type
transactionMetaDataDict<string, string>?Additional data to store and round-trip with future requests

Implementation Examples

python
# 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)
        )
java
// 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();
    }
}
csharp
// 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 } }
        };
    }
}
javascript
// 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

json
{
  "transactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
  "processorReference": "PROC_12345678",
  "transactionMetaData": {
    "providerTransactionId": "MTN_TXN_987654",
    "processingTime": "1250",
    "authorizationCode": "AUTH_789"
  }
}
FieldTypeDescription
transactionReferenceguidOriginal transaction reference from credit/process
processorReferencestring?Your internal/provider reference from credit/process, if it reached VantagePay
transactionMetaDataDict<string, string>?Additional debugging/tracking data previously set on the transaction

Response Model: GetMobileWalletCreditTransactionStatusResponse

json
{
  "transactionStatus": "SUCCESS",
  "processorReference": "PROC_12345678",
  "processorMessage": "Credit confirmed by provider",
  "transactionMetaData": {
    "lookupId": "MTN_LKP_987654",
  }
}
FieldTypeDescription
transactionStatusTransactionStatusCurrent status
processorReferencestring?Your internal/provider reference
processorMessagestring?Human-readable status message
transactionMetaDataDict<string, string>?Additional data to store and round-trip with future requests

Implementation Examples

python
# 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"
        )
java
// 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();
    }
}
csharp
// 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"
        };
    }
}
javascript
// 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.

python
# 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"}
java
// 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();
}
csharp
// 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);
}
javascript
// 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.

json
{
"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
}
SettingTypeDefaultDescription
Namestring-Unique processor name; must match the registered HTTP client name
BaseUrlstring-Base URL of your third-party service
MinimumTransactionAmountInCentsint1Minimum transaction amount this processor will accept
MaximumTransactionAmountInCentsintint.MaxValueMaximum transaction amount this processor will accept
SupportedCountriesstring?nullComma-separated ISO 3166-1 alpha-3 country codes (null or empty = all)
SupportedCurrenciesstring?nullComma-separated ISO 4217 currency codes (null or empty = all)
SupportedNetworksstring?nullComma-separated operator codes (null or empty = all). Use the canonical codes shown in Supported Mobile Wallet Operators
SupportsDelayedTransactionsboolfalseMaster switch for delayed-payment queuing
RequestValidationCheckEnabledboolfalseEnables the credit/validate call before processing
AccountHolderLookupEnabledboolfalseEnables the account-holder call before processing
TransactionStatusCheckEnabledboolfalseEnables credit/status polling when the initial response is Pending, TimedOut, or Indeterminate
BalanceCheckEnabledboolfalseEnables the credit/balance call before processing; requires SupportsDelayedTransactions: true
MaxAllowableDelayedLiabilityInCentsint0Maximum 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:

  1. Schedules a credit/status call approximately 10 seconds after the initial response.
  2. Polls every ~2 seconds while the status remains Pending or TimedOut.
  3. 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.
  4. Forces the status to Indeterminate if the loop exits while the status is still Pending (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

  1. The framework checks credit/balance before calling credit/process when both BalanceCheckEnabled and SupportsDelayedTransactions are true.
  2. If the balance is insufficient and the in-flight liability is within MaxAllowableDelayedLiabilityInCents, the transaction is placed on a retry queue (30-minute interval).
  3. Each retry calls credit/balance again before attempting credit/process.
  4. 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.

ScenarioReturn
Credit confirmed by providerSuccess
Provider rejected the creditDeclined
Provider acknowledged but hasn't confirmed yetPending
MSISDN invalid / operator not supportedValidationError
Your float is too low at the provider levelInsufficientFunds
Unknown outcome - network issue with providerPending (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:

python
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"
}
java
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");
csharp
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" }
};
javascript
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:

python
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 fees

Testing 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:

  1. Provision a test environment that points at your service BaseUrl.
  2. Run the tester against each endpoint your processor settings have enabled.
  3. Verify that your service handles repeat credit/process calls (idempotency).

Deployment Considerations

Docker Configuration Example

dockerfile
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:

python
@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}
java
@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()
        );
    }
}
csharp
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
    });
});
javascript
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:

python
import os

PROVIDER_API_KEY = os.environ["PROVIDER_API_KEY"]
PROVIDER_BASE_URL = os.environ["PROVIDER_BASE_URL"]
java
@Value("${PROVIDER_API_KEY}")
private String providerApiKey;

@Value("${PROVIDER_BASE_URL}")
private String providerBaseUrl;
csharp
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.");
javascript
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:

python
logger.info(
    "Credit transaction processed",
    extra={
        "transactionReference": str(request.transactionReference),
        "msisdn": request.msisdn,
        "amountInCents": request.amountInCents,
        "status": result.transactionStatus,
        "processorReference": result.processorReference
    }
)
java
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()
);
csharp
logger.LogInformation(
    "Credit transaction processed {@TransactionLog}",
    new
    {
        transactionReference = request.TransactionReference,
        msisdn = request.Msisdn,
        amountInCents = request.AmountInCents,
        status = result.TransactionStatus,
        processorReference = result.ProcessorReference
    }
);
javascript
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:

  1. Input validation - Validate all incoming request fields before forwarding to your provider.
  2. Secrets management - Use environment variables, not hardcoded values, for provider API keys.
  3. Audit logging - Log all requests and responses (excluding PINs and full card numbers) for audit trails.
  4. 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:

  1. Your service implementation details and the language/framework you are using
  2. Sample request/response logs for any failing calls
  3. The specific error messages or status codes you are receiving
  4. 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.

Payments for Africa