Skip to content

Mobile Wallet Debit Integration Guide

Sample Project Download

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 and refunds.

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

Debit 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: Pre-transaction Validation
    VP->>Service: POST /v1/vantagepay/mobile-money/debit/validate
    Service->>Service: Validate request data
    Service->>MW: Check operator availability
    MW-->>Service: Availability response
    Service-->>VP: Can process debit response

    Note over VP,MW: Optional: Account Holder Lookup
    VP->>Service: POST /v1/vantagepay/mobile-money/account-holder
    Service->>MW: Lookup account holder
    MW-->>Service: Account holder name
    Service-->>VP: Account holder response

    Note over VP,MW: Main Transaction Processing
    VP->>Service: POST /v1/vantagepay/mobile-money/debit/process
    Service->>MW: Process mobile wallet debit transaction
    MW-->>Service: Transaction result
    Service-->>VP: Debit 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/debit/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

Refund Mobile Wallet Transaction Flow

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: Refund Processing
    VP->>Service: POST /v1/vantagepay/mobile-money/refund/process
    Service->>MW: Process mobile wallet refund
    MW-->>Service: Refund result
    Service-->>VP: Refund response
    
    Note over VP,MW: Optional: Refund Status Polling (if PENDING returned)
    alt Refund Status is PENDING
        loop Poll until non-PENDING status
            VP->>Service: POST /v1/vantagepay/mobile-money/refund/status
            Service->>MW: Check refund status
            MW-->>Service: Current status
            Service-->>VP: Refund 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

Core Data Models

Enumerations

TransactionStatus

Status of a debit transaction or refund. Used by debit/process, debit/status, and refund/process responses.

ValueNumericDescription
Success1Transaction completed successfully - funds debited from wallet
Failed2Transaction failed; the provider rejected or could not complete the debit
Pending4Status unknown - VantagePay will poll debit/status (or refund/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.)
RequiresPin9Provider requires the consumer to authorise the debit with a PIN. VantagePay will collect the PIN out-of-band and re-issue debit/process with the pin field populated
InsufficientFunds11Debit could not be completed because the consumer's wallet balance is too low

The numeric values are stable across API versions and safe to use as integer keys if your language does not support string enums.

RefundStatus

Status of a refund returned by refund/status. Note that RequiresPin and InsufficientFunds are not valid refund statuses.

ValueNumericDescription
Success1Refund completed successfully
Failed2Refund failed
Pending4Refund is still being processed - VantagePay will continue polling
Declined6Refund explicitly declined by the provider
ValidationError7Refund could not be processed because of validation problems

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 debit transaction or its refund. Returned in the fees map on debit/process and refund/process responses.

ValueNumericDescription
ProcessingFee1Fee charged to VantagePay for processing (not passed to the consumer)
UserFee3Fee charged to the consumer (added to the debit amount)
Commission4Commission fee deducted from the debit 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. Transaction Validation (Optional)

Endpoint: POST /v1/vantagepay/mobile-money/debit/validate

Purpose: Pre-validates whether a debit 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: ValidateMobileWalletDebitRequest

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

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

Implementation Examples

python
# FastAPI
@app.post("/v1/vantagepay/mobile-money/debit/validate")
async def can_process_mobile_wallet_source(
    request: ValidateMobileWalletDebitRequest
) -> ValidateMobileWalletDebitResponse:
    # 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 ValidateMobileWalletDebitResponse(
            canProcess=False,
            reason="Operator temporarily unavailable"
        )
    
    if not valid_msisdn:
        return ValidateMobileWalletDebitResponse(
            canProcess=False,
            reason="Invalid MSISDN format"
        )
    
    return ValidateMobileWalletDebitResponse(canProcess=True)
java
// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/debit/validate")
public ValidateMobileWalletDebitResponse ValidateMobileWalletDebit(
    @RequestBody ValidateMobileWalletDebitRequest request) {
    
    boolean operatorAvailable = checkOperatorStatus(request.getMobileWalletOperator());
    boolean validMsisdn = validateMsisdn(request.getMsisdn());
    
    if (!operatorAvailable) {
        return new ValidateMobileWalletDebitResponse(false, "Operator temporarily unavailable");
    }
    
    if (!validMsisdn) {
        return new ValidateMobileWalletDebitResponse(false, "Invalid MSISDN format");
    }
    
    return new ValidateMobileWalletDebitResponse(true, null);
}
csharp
// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/debit/validate")]
public async Task<ValidateMobileWalletDebitResponse> ValidateMobileWalletDebit(
    [FromBody] ValidateMobileWalletDebitRequest request)
{
    var operatorAvailable = await CheckOperatorStatusAsync(request.MobileWalletOperator);
    var validMsisdn = ValidateMsisdn(request.Msisdn);
    
    if (!operatorAvailable)
    {
        return new ValidateMobileWalletDebitResponse 
        { 
            CanProcess = false, 
            Reason = "Operator temporarily unavailable" 
        };
    }
    
    if (!validMsisdn)
    {
        return new ValidateMobileWalletDebitResponse 
        { 
            CanProcess = false, 
            Reason = "Invalid MSISDN format" 
        };
    }
    
    return new ValidateMobileWalletDebitResponse { CanProcess = true };
}
javascript
// Express
app.post('/v1/vantagepay/mobile-money/debit/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 });
});

2. 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 Credit integration. If you implement both debit and credit, a single implementation serves both - VantagePay calls the same path on the same BaseUrl.

Request Model: GetMobileWalletAccountHolderRequest

json
{
  "mobileWalletOperator": "MTN",
  "msisdn": "233241234567"
}
FieldTypeDescription
mobileWalletOperatorMobileWalletOperatorWallet operator enum value
msisdnstringMobile number in international format

Response Model: GetMobileWalletAccountHolderResponse

json
{
  "accountHolder": "John Doe"
}
FieldTypeDescription
accountHolderstring?Account holder name if available

Implementation Examples

python
# FastAPI
@app.post("/v1/vantagepay/mobile-money/account-holder")
async def get_mobile_wallet_account_holder(
    request: GetMobileWalletAccountHolderRequest
) -> GetMobileWalletAccountHolderResponse:
    try:
        account_holder = await lookup_account_holder(
            request.mobileWalletOperator, 
            request.msisdn
        )
        return GetMobileWalletAccountHolderResponse(accountHolder=account_holder)
    except AccountNotFound:
        return GetMobileWalletAccountHolderResponse(accountHolder=None)
    except Exception as e:
        logger.warning(f"Account holder lookup failed: {str(e)}")
        return GetMobileWalletAccountHolderResponse(accountHolder=None)
java
// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/account-holder")
public GetMobileWalletAccountHolderResponse getMobileWalletAccountHolder(
    @RequestBody GetMobileWalletAccountHolderRequest request) {
    
    try {
        String accountHolder = accountHolderService.lookup(
            request.getMobileWalletOperator(),
            request.getMsisdn()
        );
        return new GetMobileWalletAccountHolderResponse(accountHolder);
    } catch (AccountNotFoundException e) {
        return new GetMobileWalletAccountHolderResponse(null);
    } catch (Exception e) {
        logger.warn("Account holder lookup failed: " + e.getMessage());
        return new GetMobileWalletAccountHolderResponse(null);
    }
}
csharp
// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/account-holder")]
public async Task<GetMobileWalletAccountHolderResponse> GetMobileWalletAccountHolder(
    [FromBody] GetMobileWalletAccountHolderRequest request)
{
    try
    {
        var accountHolder = await _accountHolderService.LookupAsync(
            request.MobileWalletOperator,
            request.Msisdn
        );
        return new GetMobileWalletAccountHolderResponse { AccountHolder = accountHolder };
    }
    catch (AccountNotFoundException)
    {
        return new GetMobileWalletAccountHolderResponse { AccountHolder = null };
    }
    catch (Exception ex)
    {
        _logger.LogWarning("Account holder lookup failed: {Message}", ex.Message);
        return new GetMobileWalletAccountHolderResponse { AccountHolder = null };
    }
}
javascript
// Express
app.post('/v1/vantagepay/mobile-money/account-holder', async (req, res) => {
    const { mobileWalletOperator, msisdn } = req.body;
    
    try {
        const accountHolder = await lookupAccountHolder(mobileWalletOperator, msisdn);
        res.json({ accountHolder });
    } catch (error) {
        if (error instanceof AccountNotFoundError) {
            res.json({ accountHolder: null });
        } else {
            console.warn('Account holder lookup failed:', error.message);
            res.json({ accountHolder: null });
        }
    }
});

3. Process Transaction (Required)

Endpoint: POST /v1/vantagepay/mobile-money/debit/process

Purpose: Main endpoint for processing a mobile wallet debit transaction. This is the only required endpoint for the debit flow.

⚠️ 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 debit/process request for a transactionReference you have already successfully processed, return the original successful response. Do not debit the wallet twice.

Request Model: ProcessMobileWalletDebitTransactionRequest

json
{
  "transactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
  "amountInCents": 50000,
  "currency": "GHS",
  "mobileWalletOperator": "MTN",
  "country": "GHA",
  "msisdn": "233241234567",
  "pin": "1234",
  "transactionMetaData": {
    "correlationId": "CORR_ABC123"
  }
}
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)
pinstring?NoPIN supplied by the consumer when re-issuing a transaction that previously returned RequiresPin
transactionMetaDataDict<string, string>NoAdditional data to store and round-trip with future requests

Response Model: ProcessMobileWalletDebitTransactionResponse

json
{
  "transactionStatus": "SUCCESS",
  "processorReference": "PROC_12345678",
  "processorMessage": "Transaction processed successfully",
  "fees": {
    "processingFee": 100,
    "userFee": 50
  },
  "transactionMetaData": {
    "correlationId": "CORR_ABC123",
    "authCode": "AUTH_456789",
    "provider": "mtn_api"
  }
}
FieldTypeDescription
transactionStatusTransactionStatusFinal or intermediate status of the debit
processorReferencestring?Your internal/provider reference for this transaction
processorMessagestring?Human-readable message describing the outcome
feesDict<FeeType, int>?Fees in cents associated with this debit, 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/debit/process")
async def process_mobile_wallet_source_transaction(
    request: ProcessMobileWalletDebitTransactionRequest
) -> ProcessMobileWalletDebitTransactionResponse:
    # Store transaction reference mapping
    internal_ref = generate_internal_reference()
    store_transaction_mapping(request.transactionReference, internal_ref)
    
    try:
        # Process with mobile wallet provider
        result = await process_debit_with_provider(
            operator=request.mobileWalletOperator,
            msisdn=request.msisdn,
            amount=request.amountInCents,
            currency=request.currency,
            pin=request.pin,
            reference=internal_ref
        )
        
        return ProcessMobileWalletDebitTransactionResponse(
            transactionStatus=TransactionStatus.SUCCESS,
            processorReference=result.provider_reference,
            processorMessage="Transaction completed successfully",
            fees={FeeType.PROCESSING_FEE: calculate_processing_fee(request.amountInCents)},
            transactionMetaData={
                "internal_ref": internal_ref,
                "provider_correlation_id": result.correlation_id,
                "processing_time": str(result.processing_time)
            }
        )
    except InsufficientFundsError:
        return ProcessMobileWalletDebitTransactionResponse(
            transactionStatus=TransactionStatus.INSUFFICIENT_FUNDS,
            processorMessage="Insufficient funds in mobile wallet"
        )
    except RequiresPinError:
        return ProcessMobileWalletDebitTransactionResponse(
            transactionStatus=TransactionStatus.REQUIRES_PIN,
            processorMessage="PIN required for transaction authorization"
        )
    except PendingError as e:
        return ProcessMobileWalletDebitTransactionResponse(
            transactionStatus=TransactionStatus.PENDING,
            processorReference=e.reference,
            processorMessage="Transaction is being processed"
        )
    except Exception as e:
        return ProcessMobileWalletDebitTransactionResponse(
            transactionStatus=TransactionStatus.FAILED,
            processorMessage=str(e)
        )
java
// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/debit/process")
public ProcessMobileWalletDebitTransactionResponse processTransaction(
    @RequestBody ProcessMobileWalletDebitTransactionRequest request) {
    
    try {
        // Store transaction reference mapping
        String internalRef = generateInternalReference();
        transactionMappingService.store(request.getTransactionReference(), internalRef);
        
        // Process with provider
        ProviderResult result = mobileWalletService.processDebit(
            request.getMobileWalletOperator(),
            request.getMsisdn(),
            request.getAmountInCents(),
            request.getCurrency(),
            request.getPin(),
            internalRef
        );
        
        Map<FeeType, Integer> fees = new HashMap<>();
        fees.put(FeeType.PROCESSING_FEE, calculateProcessingFee(request.getAmountInCents()));
        
        Map<String, String> metaData = new HashMap<>();
        metaData.put("internal_ref", internalRef);
        metaData.put("provider_correlation_id", result.getCorrelationId());
        
        return ProcessMobileWalletDebitTransactionResponse.builder()
            .transactionStatus(TransactionStatus.SUCCESS)
            .processorReference(result.getProviderReference())
            .processorMessage("Transaction completed successfully")
            .fees(fees)
            .transactionMetaData(metaData)
            .build();
            
    } catch (InsufficientFundsException e) {
        return ProcessMobileWalletDebitTransactionResponse.builder()
            .transactionStatus(TransactionStatus.INSUFFICIENT_FUNDS)
            .processorMessage("Insufficient funds in mobile wallet")
            .build();
    } catch (RequiresPinException e) {
        return ProcessMobileWalletDebitTransactionResponse.builder()
            .transactionStatus(TransactionStatus.REQUIRES_PIN)
            .processorMessage("PIN required for transaction authorization")
            .build();
    } catch (PendingException e) {
        return ProcessMobileWalletDebitTransactionResponse.builder()
            .transactionStatus(TransactionStatus.PENDING)
            .processorReference(e.getReference())
            .processorMessage("Transaction is being processed")
            .build();
    }
}
csharp
// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/debit/process")]
public async Task<ProcessMobileWalletDebitTransactionResponse> ProcessTransaction(
    [FromBody] ProcessMobileWalletDebitTransactionRequest request)
{
    try
    {
        // Store transaction reference mapping
        var internalRef = GenerateInternalReference();
        await _transactionMappingService.StoreAsync(request.TransactionReference, internalRef);
        
        // Process with provider
        var result = await _mobileWalletService.ProcessDebitAsync(
            request.MobileWalletOperator,
            request.Msisdn,
            request.AmountInCents,
            request.Currency,
            request.Pin,
            internalRef
        );
        
        var fees = new Dictionary<FeeType, int>
        {
            { FeeType.ProcessingFee, CalculateProcessingFee(request.AmountInCents) }
        };
        
        var metaData = new Dictionary<string, string>
        {
            { "internal_ref", internalRef },
            { "provider_correlation_id", result.CorrelationId }
        };
        
        return new ProcessMobileWalletDebitTransactionResponse
        {
            TransactionStatus = TransactionStatus.Success,
            ProcessorReference = result.ProviderReference,
            ProcessorMessage = "Transaction completed successfully",
            Fees = fees,
            TransactionMetaData = metaData
        };
    }
    catch (InsufficientFundsException)
    {
        return new ProcessMobileWalletDebitTransactionResponse
        {
            TransactionStatus = TransactionStatus.InsufficientFunds,
            ProcessorMessage = "Insufficient funds in mobile wallet"
        };
    }
    catch (RequiresPinException)
    {
        return new ProcessMobileWalletDebitTransactionResponse
        {
            TransactionStatus = TransactionStatus.RequiresPin,
            ProcessorMessage = "PIN required for transaction authorization"
        };
    }
    catch (PendingException ex)
    {
        return new ProcessMobileWalletDebitTransactionResponse
        {
            TransactionStatus = TransactionStatus.Pending,
            ProcessorReference = ex.Reference,
            ProcessorMessage = "Transaction is being processed"
        };
    }
}
javascript
// Express
app.post('/v1/vantagepay/mobile-money/debit/process', async (req, res) => {
    const { transactionReference, amountInCents, currency, mobileWalletOperator, msisdn, pin } = req.body;
    
    try {
        // Store transaction reference mapping
        const internalRef = generateInternalReference();
        await storeTransactionMapping(transactionReference, internalRef);
        
        // Process with provider
        const result = await processDebitWithProvider({
            operator: mobileWalletOperator,
            msisdn,
            amount: amountInCents,
            currency,
            pin,
            reference: internalRef
        });
        
        const fees = {
            ProcessingFee: calculateProcessingFee(amountInCents)
        };
        
        const metaData = {
            internal_ref: internalRef,
            provider_correlation_id: result.correlationId,
            processing_time: result.processingTime
        };
        
        res.json({
            transactionStatus: 'SUCCESS',
            processorReference: result.providerReference,
            processorMessage: 'Transaction completed successfully',
            fees: fees,
            transactionMetaData: metaData
        });
    } catch (error) {
        if (error instanceof InsufficientFundsError) {
            res.json({
                transactionStatus: 'INSUFFICIENT_FUNDS',
                processorMessage: 'Insufficient funds in mobile wallet'
            });
        } else if (error instanceof RequiresPinError) {
            res.json({
                transactionStatus: 'REQUIRES_PIN',
                processorMessage: 'PIN required for transaction authorization'
            });
        } else if (error instanceof PendingError) {
            res.json({
                transactionStatus: 'PENDING',
                processorReference: error.reference,
                processorMessage: 'Transaction is being processed'
            });
        } else {
            res.json({
                transactionStatus: 'FAILED',
                processorMessage: error.message
            });
        }
    }
});

4. Transaction Status Check (Optional)

Endpoint: POST /v1/vantagepay/mobile-money/debit/status

Purpose: Checks current status of a transaction, typically used when initial response was PENDING.

Configuration: Enabled via TransactionStatusCheckEnabled setting

Request Model: GetMobileWalletDebitTransactionStatusRequest

json
{
  "transactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
  "processorReference": "PROC_12345678",
  "transactionMetaData": {
    "correlationId": "CORR_ABC123",
    "authCode": "AUTH_456789",
    "provider": "mtn_api"
  }
}
FieldTypeDescription
transactionReferenceguidOriginal transaction reference
processorReferencestring?Your internal/processor reference if known
transactionMetaDataDict<string, string>?Additional debugging/tracking data previously set on the transaction

Response Model: GetMobileWalletDebitTransactionStatusResponse

json
{
  "transactionStatus": "SUCCESS",
  "processorReference": "PROC_12345678",
  "processorMessage": "Transaction completed successfully",
  "transactionMetaData": {
    "provider": "mtn_api"
  }
}
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/debit/status")
async def get_mobile_wallet_source_transaction_status(
    request: GetMobileWalletDebitTransactionStatusRequest
) -> GetMobileWalletDebitTransactionStatusResponse:
    try:
        # Get internal reference from mapping
        internal_ref = get_internal_reference(request.transactionReference)
        
        # Check status with provider
        status = await check_transaction_status(internal_ref)
        
        return GetMobileWalletDebitTransactionStatusResponse(
            transactionStatus=status.transaction_status,
            processorReference=status.processor_reference,
            processorMessage=status.message
        )
    except TransactionNotFound:
        return GetMobileWalletDebitTransactionStatusResponse(
            transactionStatus=TransactionStatus.FAILED,
            processorMessage="Transaction not found"
        )
java
// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/debit/status")
public GetMobileWalletDebitTransactionStatusResponse getTransactionStatus(
    @RequestBody GetMobileWalletDebitTransactionStatusRequest request) {
    
    try {
        String internalRef = transactionMappingService.getInternalReference(
            request.getTransactionReference()
        );
        
        TransactionStatus status = mobileWalletService.checkTransactionStatus(internalRef);
        
        return GetMobileWalletDebitTransactionStatusResponse.builder()
            .transactionStatus(status.getTransactionStatus())
            .processorReference(status.getProcessorReference())
            .processorMessage(status.getMessage())
            .build();
    } catch (TransactionNotFoundException e) {
        return GetMobileWalletDebitTransactionStatusResponse.builder()
            .transactionStatus(TransactionStatus.FAILED)
            .processorMessage("Transaction not found")
            .build();
    }
}
csharp
// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/debit/status")]
public async Task<GetMobileWalletDebitTransactionStatusResponse> GetTransactionStatus(
    [FromBody] GetMobileWalletDebitTransactionStatusRequest request)
{
    try
    {
        var internalRef = await _transactionMappingService.GetInternalReferenceAsync(
            request.TransactionReference
        );
        
        var status = await _mobileWalletService.CheckTransactionStatusAsync(internalRef);
        
        return new GetMobileWalletDebitTransactionStatusResponse
        {
            TransactionStatus = status.TransactionStatus,
            ProcessorReference = status.ProcessorReference,
            ProcessorMessage = status.Message
        };
    }
    catch (TransactionNotFoundException)
    {
        return new GetMobileWalletDebitTransactionStatusResponse
        {
            TransactionStatus = TransactionStatus.Failed,
            ProcessorMessage = "Transaction not found"
        };
    }
}
javascript
// Express
app.post('/v1/vantagepay/mobile-money/debit/status', async (req, res) => {
    const { transactionReference } = req.body;
    
    try {
        const internalRef = await getInternalReference(transactionReference);
        const status = await checkTransactionStatus(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;
        }
    }
});

5. Process Refund (Optional)

Endpoint: POST /v1/vantagepay/mobile-money/refund/process

Purpose: Processes refunds for previously successful transactions.

Configuration: Enabled via SupportedRefundType setting

⚠️ Important: Store the refundReference and map it to your internal reference. Use originalTransactionReference to locate the original transaction if required.

Request Model: ProcessMobileWalletDebitRefundRequest

json
{
  "refundReference": "e3a5bba9-4d9f-4e68-905e-7be38ee85b76",
  "originalTransactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
  "originalProcessorReference": "PROC_12345678",
  "refundAmountInCents": 25000,
  "currency": "GHS",
  "country": "GHA",
  "mobileWalletOperator": "MTN",
  "msisdn": "233241234567"
}
FieldTypeDescription
refundReferenceguidUnique refund reference from VantagePay
originalTransactionReferenceguidReference to the original transaction
originalProcessorReferencestring?Your internal/processor reference to the original transaction
refundAmountInCentsintegerRefund amount in cents
currencystringCurrency code (e.g., "GHS", "USD")
mobileWalletOperatorMobileWalletOperatorWallet operator enum value
msisdnstringMobile number in international format
refundMetaDataDict<string, string>?Additional debugging/tracking data previously set on the refund
transactionMetaDataDict<string, string>?Additional debugging/tracking data previously set on the source transaction

Response Model: ProcessMobileWalletDebitRefundResponse

json
{
  "refundStatus": "SUCCESS",
  "processorReference": "REF_12345678",
  "processorMessage": "Refund processed successfully",
  "fees": {
    "processingFee": 50
  },
  "refundMetaData": {
    "refund_type": "direct_credit"
  }
}
FieldTypeDescription
refundStatusTransactionStatusFinal or intermediate status of the refund
processorReferencestring?Your internal/provider refund reference
processorMessagestring?Human-readable message describing the outcome
feesDict<FeeType, int>?Fees in cents associated with this refund, by fee type
refundMetaDataDict<string, string>?Additional data to store and round-trip with future refund-status requests

Implementation Examples

python
# FastAPI
@app.post("/v1/vantagepay/mobile-money/refund/process")
async def process_mobile_wallet_source_refund(
    request: ProcessMobileWalletDebitRefundRequest
) -> ProcessMobileWalletDebitRefundResponse:
    # Store refund reference mapping
    internal_refund_ref = generate_internal_reference()
    store_refund_mapping(request.refundReference, internal_refund_ref)
    
    try:
        # Use originalProcessorReference if provided, otherwise look up the
        # internal reference we stored when the original debit was processed.
        original_internal_ref = (
            request.originalProcessorReference
            or await get_internal_reference(request.originalTransactionReference)
        )

        # Process refund with mobile wallet provider
        result = await process_refund_with_provider(
            operator=request.mobileWalletOperator,
            msisdn=request.msisdn,
            amount=request.refundAmountInCents,
            currency=request.currency,
            country=request.country,
            original_reference=original_internal_ref,
            refund_reference=internal_refund_ref
        )
        
        return ProcessMobileWalletDebitRefundResponse(
            refundStatus=TransactionStatus.SUCCESS,
            processorReference=result.refund_reference,
            processorMessage="Refund processed successfully",
            fees={FeeType.PROCESSING_FEE: calculate_refund_fee(request.refundAmountInCents)},
            refundMetaData={
                "internal_refund_ref": internal_refund_ref,
                "original_transaction": request.originalTransactionReference,
                "refund_type": "direct_credit"
            }
        )
    except OriginalTransactionNotFound:
        return ProcessMobileWalletDebitRefundResponse(
            refundStatus=TransactionStatus.FAILED,
            processorMessage="Original transaction not found"
        )
    except RefundNotSupported:
        return ProcessMobileWalletDebitRefundResponse(
            refundStatus=TransactionStatus.DECLINED,
            processorMessage="Refunds not supported by operator"
        )
    except Exception as e:
        return ProcessMobileWalletDebitRefundResponse(
            refundStatus=TransactionStatus.FAILED,
            processorMessage=str(e)
        )
java
// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/refund/process")
public ProcessMobileWalletDebitRefundResponse processRefund(
    @RequestBody ProcessMobileWalletDebitRefundRequest request) {
    
    try {
        String internalRefundRef = generateInternalReference();
        refundMappingService.store(request.getRefundReference(), internalRefundRef);
        
        String originalInternalRef = transactionMappingService.getInternalReference(
            request.getOriginalTransactionReference()
        );
        
        RefundResult result = mobileWalletService.processRefund(
            request.getMobileWalletOperator(),
            request.getMsisdn(),
            request.getRefundAmountInCents(),
            request.getCurrency(),
            request.getCountry(),
            originalInternalRef,
            internalRefundRef
        );
        
        Map<FeeType, Integer> fees = new HashMap<>();
        fees.put(FeeType.PROCESSING_FEE, calculateRefundFee(request.getRefundAmountInCents()));
        
        Map<String, String> metaData = new HashMap<>();
        metaData.put("internal_refund_ref", internalRefundRef);
        metaData.put("original_transaction", request.getOriginalTransactionReference());
        metaData.put("refund_type", "direct_credit");
        
        return ProcessMobileWalletDebitRefundResponse.builder()
            .transactionStatus(TransactionStatus.SUCCESS)
            .processorReference(result.getRefundReference())
            .processorMessage("Refund processed successfully")
            .fees(fees)
            .refundMetaData(metaData)
            .build();
            
    } catch (OriginalTransactionNotFoundException e) {
        return ProcessMobileWalletDebitRefundResponse.builder()
            .refundStatus(TransactionStatus.FAILED)
            .processorMessage("Original transaction not found")
            .build();
    } catch (RefundNotSupportedException e) {
        return ProcessMobileWalletDebitRefundResponse.builder()
            .refundStatus(TransactionStatus.DECLINED)
            .processorMessage("Refunds not supported by operator")
            .build();
    }
}
csharp
// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/refund/process")]
public async Task<ProcessMobileWalletDebitRefundResponse> ProcessRefund(
    [FromBody] ProcessMobileWalletDebitRefundRequest request)
{
    try
    {
        var internalRefundRef = GenerateInternalReference();
        await _refundMappingService.StoreAsync(request.RefundReference, internalRefundRef);
        
        var originalInternalRef = request.OriginalProcessorReference;
        if (string.IsNullOrEmpty(originalInternalRef))
        {
            originalInternalRef = await _transactionMappingService.GetInternalReferenceAsync(
                request.OriginalTransactionReference
            );
        }
        
        var result = await _mobileWalletService.ProcessRefundAsync(
            request.MobileWalletOperator,
            request.Msisdn,
            request.RefundAmountInCents,
            request.Currency,
            request.Country,
            originalInternalRef,
            internalRefundRef
        );
        
        var fees = new Dictionary<FeeType, int>
        {
            { FeeType.ProcessingFee, CalculateRefundFee(request.RefundAmountInCents) }
        };
        
        var metaData = new Dictionary<string, string>
        {
            { "internal_refund_ref", internalRefundRef },
            { "original_transaction", request.OriginalTransactionReference },
            { "refund_type", "direct_credit" }
        };
        
        return new ProcessMobileWalletDebitRefundResponse
        {
            refundStatus = TransactionStatus.Success,
            ProcessorReference = result.RefundReference,
            ProcessorMessage = "Refund processed successfully",
            Fees = fees,
            RefundMetaData = metaData
        };
    }
    catch (OriginalTransactionNotFoundException)
    {
        return new ProcessMobileWalletDebitRefundResponse
        {
            refundStatus = TransactionStatus.Failed,
            ProcessorMessage = "Original transaction not found"
        };
    }
    catch (RefundNotSupportedException)
    {
        return new ProcessMobileWalletDebitRefundResponse
        {
            refundStatus = TransactionStatus.Declined,
            ProcessorMessage = "Refunds not supported by operator"
        };
    }
}
javascript
// Express
app.post('/v1/vantagepay/mobile-money/refund/process', async (req, res) => {
    const { refundReference, originalTransactionReference, originalProcessorReference, refundAmountInCents, currency, mobileWalletOperator, msisdn } = req.body;
    
    try {
        const internalRefundRef = generateInternalReference();
        await storeRefundMapping(refundReference, internalRefundRef);
        
        let originalInternalRef = originalProcessorReference;
        if (!originalInternalRef) {
            originalInternalRef = await getInternalReference(originalTransactionReference);
        }
        
        const result = await processRefundWithProvider({
            operator: mobileWalletOperator,
            msisdn,
            amount: refundAmountInCents,
            currency,
            originalReference: originalInternalRef,
            refundReference: internalRefundRef
        });
        
        const fees = {
            ProcessingFee: calculateRefundFee(refundAmountInCents)
        };
        
        const metaData = {
            internal_refund_ref: internalRefundRef,
            original_transaction: originalTransactionReference,
            refund_type: 'direct_credit'
        };
        
        res.json({
            refundStatus: 'SUCCESS',
            processorReference: result.refundReference,
            processorMessage: 'Refund processed successfully',
            fees: fees,
            refundMetaData: metaData
        });
    } catch (error) {
        if (error instanceof OriginalTransactionNotFoundError) {
            res.json({
                refundStatus: 'FAILED',
                processorMessage: 'Original transaction not found'
            });
        } else if (error instanceof RefundNotSupportedError) {
            res.json({
                refundStatus: 'DECLINED',
                processorMessage: 'Refunds not supported by operator'
            });
        } else {
            res.json({
                refundStatus: 'FAILED',
                processorMessage: error.message
            });
        }
    }
});

6. Refund Status Check (Optional)

Endpoint: POST /v1/vantagepay/mobile-money/refund/status

Purpose: Checks current status of a refund transaction.

Configuration: Enabled via RefundStatusCheckEnabled setting

Request Model: GetMobileWalletDebitRefundStatusRequest

json
{
  "refundReference": "e3a5bba9-4d9f-4e68-905e-7be38ee85b76",
  "processorReference": "REF_12345678",
  "refundMetaData": {
    "providerRefundId": "MTN_RFD_987654"
  },
  "transactionMetaData": {
    "providerTransactionId": "MTN_TXN_987654",
    "processingTime": "1250",
    "authorizationCode": "AUTH_789"
  }
}
FieldTypeDescription
refundReferenceguidOriginal refund reference
processorReferencestring?Your internal/processor refund reference if known
refundMetaDataDict<string, string>?Additional debugging/tracking data previously set on the refund transaction
transactionMetaDataDict<string, string>?Additional debugging/tracking data previously set on the source transaction

Response Model: GetMobileWalletDebitRefundStatusResponse

json
{
  "refundStatus": "SUCCESS",
  "processorReference": "REF_12345678",
  "processorMessage": "Refund completed successfully",
  "refundMetaData": {
    "providerRefundId": "MTN_RFD_987654",
    "processingTime": "1440"
  }
}
FieldTypeDescription
refundStatusMobileWalletDebitRefundStatusCurrent refund status
processorReferencestring?Your internal/provider refund reference
processorMessagestring?Human-readable status message
refundMetaDataDict<string, string>?Additional data to store and round-trip with future refund-status requests

Implementation Examples

python
# FastAPI
@app.post("/v1/vantagepay/mobile-money/refund/status")
async def get_mobile_wallet_source_refund_status(
    request: GetMobileWalletDebitRefundStatusRequest
) -> GetMobileWalletDebitRefundStatusResponse:
    try:
        # Get internal refund reference from mapping
        internal_refund_ref = get_internal_refund_reference(request.refundReference)
        
        # Check refund status with provider
        status = await check_refund_status(internal_refund_ref)
        
        return GetMobileWalletDebitRefundStatusResponse(
            refundStatus=status.refund_status,
            processorReference=status.processor_reference,
            processorMessage=status.message
        )
    except RefundNotFound:
        return GetMobileWalletDebitRefundStatusResponse(
            refundStatus=MobileWalletDebitRefundStatus.FAILED,
            processorMessage="Refund not found"
        )
java
// Spring Boot
@PostMapping("/v1/vantagepay/mobile-money/refund/status")
public GetMobileWalletDebitRefundStatusResponse getRefundStatus(
    @RequestBody GetMobileWalletDebitRefundStatusRequest request) {
    
    try {
        String internalRefundRef = refundMappingService.getInternalReference(
            request.getRefundReference()
        );
        
        RefundStatus status = mobileWalletService.checkRefundStatus(internalRefundRef);
        
        return GetMobileWalletDebitRefundStatusResponse.builder()
            .refundStatus(status.getRefundStatus())
            .processorReference(status.getProcessorReference())
            .processorMessage(status.getMessage())
            .build();
    } catch (RefundNotFoundException e) {
        return GetMobileWalletDebitRefundStatusResponse.builder()
            .refundStatus(MobileWalletDebitRefundStatus.FAILED)
            .processorMessage("Refund not found")
            .build();
    }
}
csharp
// ASP.NET
[HttpPost("/v1/vantagepay/mobile-money/refund/status")]
public async Task<GetMobileWalletDebitRefundStatusResponse> GetRefundStatus(
    [FromBody] GetMobileWalletDebitRefundStatusRequest request)
{
    try
    {
        var internalRefundRef = await _refundMappingService.GetInternalReferenceAsync(
            request.RefundReference
        );
        
        var status = await _mobileWalletService.CheckRefundStatusAsync(internalRefundRef);
        
        return new GetMobileWalletDebitRefundStatusResponse
        {
            RefundStatus = status.RefundStatus,
            ProcessorReference = status.ProcessorReference,
            ProcessorMessage = status.Message
        };
    }
    catch (RefundNotFoundException)
    {
        return new GetMobileWalletDebitRefundStatusResponse
        {
            RefundStatus = MobileWalletDebitRefundStatus.Failed,
            ProcessorMessage = "Refund not found"
        };
    }
}
javascript
// Express
app.post('/v1/vantagepay/mobile-money/refund/status', async (req, res) => {
    const { refundReference, processorReference } = req.body;

    try {
        const internalRefundRef = processorReference
            || await getInternalRefundReference(refundReference);
        const status = await checkRefundStatus(internalRefundRef);
        
        res.json({
            refundStatus: status.refundStatus,
            processorReference: status.processorReference,
            processorMessage: status.message
        });
    } catch (error) {
        if (error instanceof RefundNotFoundError) {
            res.json({
                refundStatus: 'FAILED',
                processorMessage: 'Refund not found'
            });
        } else {
            throw error;
        }
    }
});

7. 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 one of the following formats and accept both GET and POST:

  • /v1/vantagepay/mobile-money/debit/callback/{configName} - debit transaction callbacks
  • /v1/vantagepay/mobile-money/refund/callback/{configName} - refund callbacks

The path is rewritten by VantagePay, the query string, headers and request body are forwarded to your service on the internal URLs below.

Debit Endpoint: POST/GET /v1/vantagepay/mobile-money/debit/callback

Refund Endpoint: POST/GET /v1/vantagepay/mobile-money/refund/callback

Purpose: Receives the exact callback payload and query parameters forwarded from the external callback URL.

python
# FastAPI
@app.get("/v1/vantagepay/mobile-money/debit/callback")
@app.post("/v1/vantagepay/mobile-money/debit/callback")
async def mobile-wallet-debit_transaction_callback(request: Request):
    body = await request.json() if request.method == "POST" else {}
    params = dict(request.query_params)
    transaction_ref = params.get("transactionReference")
    await handle_provider_callback(transaction_ref, body, params)
    return {"status": "callback received"}
java
// Spring Boot
@RequestMapping(
    value = "/v1/vantagepay/mobile-money/debit/callback",
    method = { RequestMethod.GET, RequestMethod.POST })
public ResponseEntity<Void> debitCallback(
    @RequestParam Map<String, String> params,
    @RequestBody(required = false) Map<String, Object> body) {

    String transactionRef = params.get("transactionReference");
    callbackHandlerService.handleDebitCallback(transactionRef, body, params);
    return ResponseEntity.ok().build();
}
csharp
// ASP.NET
[HttpGet("/v1/vantagepay/mobile-money/debit/callback")]
[HttpPost("/v1/vantagepay/mobile-money/debit/callback")]
public async Task DebitCallback()
{
    var transactionRef = Request.Query["transactionReference"].ToString();
    await _callbackHandlerService.HandleDebitCallbackAsync(transactionRef, Request);
}
javascript
// Express
app.get("/v1/vantagepay/mobile-money/debit/callback", async (req, res) => {
    const { transactionReference } = req.query;
    await handleProviderCallback(transactionReference, {}, req.query);
    res.json({ status: "callback received" });
});

app.post("/v1/vantagepay/mobile-money/debit/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-debit-processor",
"BaseUrl": "http://my-debit-service:8080/",
"MinimumTransactionAmountInCents": 100,
"MaximumTransactionAmountInCents": 100000000,
"SupportedCountries": "GHA",
"SupportedCurrencies": "GHS",
"SupportedNetworks": "MTN,ATG,GMY",
"RequestValidationCheckEnabled": true,
"AccountHolderLookupEnabled": true,
"TransactionStatusCheckEnabled": true,
"SupportedRefundType": "PartialRefundsAllowed",
"RefundStatusCheckEnabled": true
}
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
RequestValidationCheckEnabledboolfalseEnables the debit/validate call before processing
AccountHolderLookupEnabledboolfalseEnables the account-holder call before processing
TransactionStatusCheckEnabledboolfalseEnables debit/status polling when the initial response is Pending, TimedOut, or Indeterminate
SupportedRefundTypeenumNoneRefund capability: None, FullRefundsOnly, or PartialRefundsAllowed
RefundStatusCheckEnabledboolfalseEnables refund/status polling when the initial refund response is non-final

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 debit/process (or refund/process) returns Pending, TimedOut, or Indeterminate (and the corresponding status-check setting is enabled), VantagePay:

  1. Schedules a debit/status (or refund/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. Refund polling additionally has a 4-minute hard ceiling.
  4. Forces the status to Indeterminate if the loop exits while the status is still Pending (treated as a final failure).

If TransactionStatusCheckEnabled (or RefundStatusCheckEnabled for refunds) is false the framework cannot recover an indeterminate outcome and the transaction is finalised as Unrecoverable.

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 debit/status poll. This poll continues 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
Debit confirmed by providerSuccess
Provider rejected the debit (final)Declined
Provider acknowledged but has not confirmed yetPending
Consumer must enter their wallet PIN before the debit can be authorisedRequiresPin
MSISDN invalid / operator not supported / amount out of rangeValidationError
Consumer wallet has insufficient balanceInsufficientFunds
Unknown outcome - network issue with providerPending (if TransactionStatusCheckEnabled is on) or Failed

Metadata Best Practices

Include all information you might need to look up this transaction during a debit/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 (debit/status, replayed debit/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 debit/process calls (idempotency), the RequiresPin re-entry flow, and the Pending polling cycle correctly.

Deployment Considerations

Docker Configuration

Your service should be containerized and expose the API on the configured port:

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", "timestamp": datetime.utcnow().isoformat()}

@app.get("/health/ready")
async def readiness_check():
    # Check database connectivity, external service availability, etc.
    return {"status": "ready"}
java
@RestController
@RequestMapping("/health")
public class HealthController {

    @GetMapping("/liveness")
    public Map<String, Object> livenessCheck() {
        return Map.of(
            "status", "healthy",
            "timestamp", Instant.now().toString()
        );
    }

    @GetMapping("/ready")
    public Map<String, Object> readinessCheck() {
        // Check database connectivity, external service availability, etc.
        return Map.of("status", "ready");
    }
}
csharp
app.MapGet("/health/liveness", () => Results.Ok(new
{
    status = "healthy",
    timestamp = DateTime.UtcNow.ToString("o")
}));

app.MapGet("/health/ready", () =>
{
    // Check database connectivity, external service availability, etc.
    return Results.Ok(new { status = "ready" });
});
javascript
app.get("/health/liveness", (_req, res) => {
  res.json({
    status: "healthy",
    timestamp: new Date().toISOString()
  });
});

app.get("/health/ready", (_req, res) => {
  // Check database connectivity, external service availability, etc.
  res.json({ status: "ready" });
});

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

Implement comprehensive logging for troubleshooting:

python
import logging

logger = logging.getLogger(__name__)

@app.post("/v1/vantagepay/mobile-money/debit/process")
async def process_mobile_wallet_source_transaction(request):
    logger.info(f"Processing transaction {request.transactionReference} for {request.msisdn}")
    
    try:
        result = await process_transaction(request)
        logger.info(f"Transaction {request.transactionReference} completed with status {result.transactionStatus}")
        return result
    except Exception as e:
        logger.error(f"Transaction {request.transactionReference} failed: {str(e)}")
        raise
java
private static final Logger logger = LoggerFactory.getLogger(DebitController.class);

@PostMapping("/v1/vantagepay/mobile-money/debit/process")
public ProcessMobileWalletDebitTransactionResponse processMobileWalletDebit(
    @RequestBody ProcessMobileWalletDebitTransactionRequest request) {

    logger.info(
        "Processing transaction {} for {}",
        request.getTransactionReference(),
        request.getMsisdn()
    );

    try {
        var result = debitService.processTransaction(request);
        logger.info(
            "Transaction {} completed with status {}",
            request.getTransactionReference(),
            result.getTransactionStatus()
        );
        return result;
    } catch (Exception ex) {
        logger.error(
            "Transaction {} failed: {}",
            request.getTransactionReference(),
            ex.getMessage(),
            ex
        );
        throw ex;
    }
}
csharp
app.MapPost("/v1/vantagepay/mobile-money/debit/process",
    async (
        ProcessMobileWalletDebitTransactionRequest request,
        IDebitService debitService,
        ILogger<Program> logger) =>
    {
        logger.LogInformation(
            "Processing transaction {TransactionReference} for {Msisdn}",
            request.TransactionReference,
            request.Msisdn
        );

        try
        {
            var result = await debitService.ProcessTransactionAsync(request);
            logger.LogInformation(
                "Transaction {TransactionReference} completed with status {TransactionStatus}",
                request.TransactionReference,
                result.TransactionStatus
            );
            return Results.Ok(result);
        }
        catch (Exception ex)
        {
            logger.LogError(
                ex,
                "Transaction {TransactionReference} failed",
                request.TransactionReference
            );
            throw;
        }
    });
javascript
app.post("/v1/vantagepay/mobile-money/debit/process", async (req, res, next) => {
  const request = req.body;
  logger.info(
    `Processing transaction ${request.transactionReference} for ${request.msisdn}`
  );

  try {
    const result = await debitService.processTransaction(request);
    logger.info(
      `Transaction ${request.transactionReference} completed with status ${result.transactionStatus}`
    );
    res.json(result);
  } catch (error) {
    logger.error(
      `Transaction ${request.transactionReference} failed: ${error.message}`
    );
    next(error);
  }
});

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 Debit integration. Additional documentation is available for other payment types including credit cards, bank accounts, and digital wallets.

Payments for Africa