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.
| Operator | Code your service receives | Description |
|---|---|---|
| MTN | MTN | MTN Mobile Money (Ghana, South Africa, and other African markets) |
| Vodafone | VDF | Vodafone Mobile Money (Ghana) |
| Airtel | ATL | Airtel Mobile Money |
| Tigo | TGO | Tigo Mobile Money (Ghana) |
| AT (AirtelTigo) | ATG | AirtelTigo Mobile Money (Ghana) |
| G-Money | GMY | G-Money Wallet (GCB Bank, Ghana) |
| Zeepay | ZPY | Zeepay Wallet (Ghana) |
| GhanaPay | GHP | GhanaPay (interoperable scheme, Ghana) |
| Telecel | TCG | Telecel Mobile Money (formerly Vodafone Ghana) |
| Vodacom | VDC | Vodacom Mobile Money (South Africa) |
| Cell C | CLC | Cell C Mobile Money (South Africa) |
| Telkom | TKM | Telkom Mobile Money (South Africa) |
| Moov | MOV | Moov Africa Mobile Money (Benin, Burkina Faso, Côte d'Ivoire, Niger, Togo) |
| Orange | ORG | Orange Money (Senegal, Mali, Côte d'Ivoire, Cameroon, Madagascar) |
| Wave | WAV | Wave Mobile Money (Senegal, Côte d'Ivoire, Burkina Faso, Mali) |
| M-Pesa | MPE | M-Pesa (Kenya, Tanzania, DRC, Mozambique, Lesotho, Ghana, Egypt) |
| TNM | TNM | Telekom Networks Malawi Mobile Money (Malawi) |
| Movitel | MVT | Movitel Mobile Money (Mozambique) |
| Free | FRE | Free / Sonatel Mobile Money (Senegal and West Africa) |
| Halotel | HTL | Halotel (Viettel Tanzania) Mobile Money (Tanzania) |
| Zamtel | ZTL | Zamtel Mobile Money (Zambia) |
Integration Architecture
Third-party processors implement a REST API service that VantagePay's payment services will call to process transactions. The service runs within a private network (typically as a pod in a Kubernetes cluster) and requires no external security as it's only accessible by VantagePay's internal payment services.
Refer to the diagrams below to understand the typical flows that will occur for mobile wallet payments and refunds.
graph LR
A[VantagePay Payment Services] --> B[Your Service API]
B --> C[Mobile Wallet Providers]
subgraph "Internal Infrastructure"
B
end
subgraph "External Services"
C
endDebit Mobile Wallet Transaction Flow
The framework orchestrates all steps. Your service only needs to respond to each API call - it never needs to drive the flow itself.
sequenceDiagram
participant VP as VantagePay Payment Services
participant Service as Your Service API
participant MW as Mobile Wallet Provider
Note over VP,MW: Optional: 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
endRefund Mobile Wallet Transaction Flow
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
endCore Data Models
Enumerations
TransactionStatus
Status of a debit transaction or refund. Used by debit/process, debit/status, and refund/process responses.
| Value | Numeric | Description |
|---|---|---|
Success | 1 | Transaction completed successfully - funds debited from wallet |
Failed | 2 | Transaction failed; the provider rejected or could not complete the debit |
Pending | 4 | Status unknown - VantagePay will poll debit/status (or refund/status) until a final status is received |
Declined | 6 | Transaction explicitly declined by the mobile wallet provider |
ValidationError | 7 | Failed due to validation issues (invalid MSISDN, operator not supported, etc.) |
RequiresPin | 9 | Provider 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 |
InsufficientFunds | 11 | Debit 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.
| Value | Numeric | Description |
|---|---|---|
Success | 1 | Refund completed successfully |
Failed | 2 | Refund failed |
Pending | 4 | Refund is still being processed - VantagePay will continue polling |
Declined | 6 | Refund explicitly declined by the provider |
ValidationError | 7 | Refund 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.
| Value | Numeric | Description |
|---|---|---|
ProcessingFee | 1 | Fee charged to VantagePay for processing (not passed to the consumer) |
UserFee | 3 | Fee charged to the consumer (added to the debit amount) |
Commission | 4 | Commission 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
{
"amountInCents": 50000,
"currency": "GHS",
"mobileWalletOperator": "MTN",
"country": "GHA",
"msisdn": "233241234567"
}| Field | Type | Required | Description |
|---|---|---|---|
amountInCents | integer | Yes | Transaction amount in cents |
currency | Currency (ISO 4217) | Yes | Currency code, e.g. "GHS" |
mobileWalletOperator | MobileWalletOperator | Yes | Operator code (see Supported Mobile Wallet Operators) |
country | Country? (ISO 3166-1 alpha-3) | No | Country of the consumer, e.g. "GHA". May be null when unknown |
msisdn | string | Yes | Mobile number in international format (no leading + or 00), e.g. "233241234567" |
Response Model: ValidateMobileWalletDebitResponse
{
"canProcess": true,
"reason": null
}| Field | Type | Description |
|---|---|---|
canProcess | boolean | Whether the service can process this transaction |
reason | string? | Optional reason to log if transaction cannot be processed |
Implementation Examples
# 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)// 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);
}// 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 };
}// 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
{
"mobileWalletOperator": "MTN",
"msisdn": "233241234567"
}| Field | Type | Description |
|---|---|---|
mobileWalletOperator | MobileWalletOperator | Wallet operator enum value |
msisdn | string | Mobile number in international format |
Response Model: GetMobileWalletAccountHolderResponse
{
"accountHolder": "John Doe"
}| Field | Type | Description |
|---|---|---|
accountHolder | string? | Account holder name if available |
Implementation Examples
# 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)// 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);
}
}// 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 };
}
}// 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
{
"transactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
"amountInCents": 50000,
"currency": "GHS",
"mobileWalletOperator": "MTN",
"country": "GHA",
"msisdn": "233241234567",
"pin": "1234",
"transactionMetaData": {
"correlationId": "CORR_ABC123"
}
}| Field | Type | Required | Description |
|---|---|---|---|
transactionReference | guid | Yes | Unique transaction reference from VantagePay. Use this as your idempotency key |
amountInCents | integer | Yes | Transaction amount in cents |
currency | Currency (ISO 4217) | Yes | Currency code, e.g. "GHS" |
mobileWalletOperator | MobileWalletOperator | Yes | Operator code (see Supported Mobile Wallet Operators) |
country | Country? (ISO 3166-1 alpha-3) | No | Country of the consumer, e.g. "GHA". May be null when unknown |
msisdn | string | Yes | Mobile number in international format (no leading + or 00) |
pin | string? | No | PIN supplied by the consumer when re-issuing a transaction that previously returned RequiresPin |
transactionMetaData | Dict<string, string> | No | Additional data to store and round-trip with future requests |
Response Model: ProcessMobileWalletDebitTransactionResponse
{
"transactionStatus": "SUCCESS",
"processorReference": "PROC_12345678",
"processorMessage": "Transaction processed successfully",
"fees": {
"processingFee": 100,
"userFee": 50
},
"transactionMetaData": {
"correlationId": "CORR_ABC123",
"authCode": "AUTH_456789",
"provider": "mtn_api"
}
}| Field | Type | Description |
|---|---|---|
transactionStatus | TransactionStatus | Final or intermediate status of the debit |
processorReference | string? | Your internal/provider reference for this transaction |
processorMessage | string? | Human-readable message describing the outcome |
fees | Dict<FeeType, int>? | Fees in cents associated with this debit, by fee type |
transactionMetaData | Dict<string, string>? | Additional data to store and round-trip with future requests |
Implementation Examples
# FastAPI
@app.post("/v1/vantagepay/mobile-money/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)
)// 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();
}
}// 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"
};
}
}// 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
{
"transactionReference": "9e5083c5-a2b0-4266-993f-d3576828a5cc",
"processorReference": "PROC_12345678",
"transactionMetaData": {
"correlationId": "CORR_ABC123",
"authCode": "AUTH_456789",
"provider": "mtn_api"
}
}| Field | Type | Description |
|---|---|---|
transactionReference | guid | Original transaction reference |
processorReference | string? | Your internal/processor reference if known |
transactionMetaData | Dict<string, string>? | Additional debugging/tracking data previously set on the transaction |
Response Model: GetMobileWalletDebitTransactionStatusResponse
{
"transactionStatus": "SUCCESS",
"processorReference": "PROC_12345678",
"processorMessage": "Transaction completed successfully",
"transactionMetaData": {
"provider": "mtn_api"
}
}| Field | Type | Description |
|---|---|---|
transactionStatus | TransactionStatus | Current status |
processorReference | string? | Your internal/provider reference |
processorMessage | string? | Human-readable status message |
transactionMetaData | Dict<string, string>? | Additional data to store and round-trip with future requests |
Implementation Examples
# FastAPI
@app.post("/v1/vantagepay/mobile-money/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"
)// 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();
}
}// 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"
};
}
}// 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
{
"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"
}| Field | Type | Description |
|---|---|---|
refundReference | guid | Unique refund reference from VantagePay |
originalTransactionReference | guid | Reference to the original transaction |
originalProcessorReference | string? | Your internal/processor reference to the original transaction |
refundAmountInCents | integer | Refund amount in cents |
currency | string | Currency code (e.g., "GHS", "USD") |
mobileWalletOperator | MobileWalletOperator | Wallet operator enum value |
msisdn | string | Mobile number in international format |
refundMetaData | Dict<string, string>? | Additional debugging/tracking data previously set on the refund |
transactionMetaData | Dict<string, string>? | Additional debugging/tracking data previously set on the source transaction |
Response Model: ProcessMobileWalletDebitRefundResponse
{
"refundStatus": "SUCCESS",
"processorReference": "REF_12345678",
"processorMessage": "Refund processed successfully",
"fees": {
"processingFee": 50
},
"refundMetaData": {
"refund_type": "direct_credit"
}
}| Field | Type | Description |
|---|---|---|
refundStatus | TransactionStatus | Final or intermediate status of the refund |
processorReference | string? | Your internal/provider refund reference |
processorMessage | string? | Human-readable message describing the outcome |
fees | Dict<FeeType, int>? | Fees in cents associated with this refund, by fee type |
refundMetaData | Dict<string, string>? | Additional data to store and round-trip with future refund-status requests |
Implementation Examples
# 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)
)// 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();
}
}// 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"
};
}
}// 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
{
"refundReference": "e3a5bba9-4d9f-4e68-905e-7be38ee85b76",
"processorReference": "REF_12345678",
"refundMetaData": {
"providerRefundId": "MTN_RFD_987654"
},
"transactionMetaData": {
"providerTransactionId": "MTN_TXN_987654",
"processingTime": "1250",
"authorizationCode": "AUTH_789"
}
}| Field | Type | Description |
|---|---|---|
refundReference | guid | Original refund reference |
processorReference | string? | Your internal/processor refund reference if known |
refundMetaData | Dict<string, string>? | Additional debugging/tracking data previously set on the refund transaction |
transactionMetaData | Dict<string, string>? | Additional debugging/tracking data previously set on the source transaction |
Response Model: GetMobileWalletDebitRefundStatusResponse
{
"refundStatus": "SUCCESS",
"processorReference": "REF_12345678",
"processorMessage": "Refund completed successfully",
"refundMetaData": {
"providerRefundId": "MTN_RFD_987654",
"processingTime": "1440"
}
}| Field | Type | Description |
|---|---|---|
refundStatus | MobileWalletDebitRefundStatus | Current refund status |
processorReference | string? | Your internal/provider refund reference |
processorMessage | string? | Human-readable status message |
refundMetaData | Dict<string, string>? | Additional data to store and round-trip with future refund-status requests |
Implementation Examples
# 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"
)// 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();
}
}// 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"
};
}
}// 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.
# 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"}// 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();
}// 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);
}// 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.
{
"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
}| Setting | Type | Default | Description |
|---|---|---|---|
Name | string | - | Unique processor name; must match the registered HTTP client name |
BaseUrl | string | - | Base URL of your third-party service |
MinimumTransactionAmountInCents | int | 1 | Minimum transaction amount this processor will accept |
MaximumTransactionAmountInCents | int | int.MaxValue | Maximum transaction amount this processor will accept |
SupportedCountries | string? | null | Comma-separated ISO 3166-1 alpha-3 country codes (null or empty = all) |
SupportedCurrencies | string? | null | Comma-separated ISO 4217 currency codes (null or empty = all) |
SupportedNetworks | string? | null | Comma-separated operator codes (null or empty = all). Use the canonical codes shown in Supported Mobile Wallet Operators |
RequestValidationCheckEnabled | bool | false | Enables the debit/validate call before processing |
AccountHolderLookupEnabled | bool | false | Enables the account-holder call before processing |
TransactionStatusCheckEnabled | bool | false | Enables debit/status polling when the initial response is Pending, TimedOut, or Indeterminate |
SupportedRefundType | enum | None | Refund capability: None, FullRefundsOnly, or PartialRefundsAllowed |
RefundStatusCheckEnabled | bool | false | Enables 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:
- Schedules a
debit/status(orrefund/status) call approximately 10 seconds after the initial response. - Polls every ~2 seconds while the status remains
PendingorTimedOut. - Exits the polling loop and finalises the transaction once any other status is received, when the parent transaction batch completes/expires, or when the host shuts down. Refund polling additionally has a 4-minute hard ceiling.
- Forces the status to
Indeterminateif the loop exits while the status is stillPending(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.
| Scenario | Return |
|---|---|
| Debit confirmed by provider | Success |
| Provider rejected the debit (final) | Declined |
| Provider acknowledged but has not confirmed yet | Pending |
| Consumer must enter their wallet PIN before the debit can be authorised | RequiresPin |
| MSISDN invalid / operator not supported / amount out of range | ValidationError |
| Consumer wallet has insufficient balance | InsufficientFunds |
| Unknown outcome - network issue with provider | Pending (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:
metaData = {
"internal_reference": internal_ref,
"provider_correlation_id": provider_response.correlation_id,
"provider_response_code": provider_response.response_code,
"processing_time_ms": str(processing_time),
"api_version": "v2.1"
}Map<String, String> metaData = new HashMap<>();
metaData.put("internal_reference", internalRef);
metaData.put("provider_correlation_id", providerResponse.getCorrelationId());
metaData.put("provider_response_code", providerResponse.getResponseCode());
metaData.put("processing_time_ms", String.valueOf(processingTime));
metaData.put("api_version", "v2.1");var metaData = new Dictionary<string, string>
{
{ "internal_reference", internalRef },
{ "provider_correlation_id", providerResponse.CorrelationId },
{ "provider_response_code", providerResponse.ResponseCode },
{ "processing_time_ms", processingTime.ToString() },
{ "api_version", "v2.1" }
};const metaData = {
internal_reference: internalRef,
provider_correlation_id: providerResponse.correlationId,
provider_response_code: providerResponse.responseCode,
processing_time_ms: String(processingTime),
api_version: "v2.1"
};Metadata is round-tripped in all subsequent calls (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:
def calculate_fees(amount_in_cents, operator):
fees = {}
# Processing fee charged to VantagePay
fees[FeeType.PROCESSING_FEE] = calculate_processing_fee(amount_in_cents, operator)
# User fee added to debit amount
if has_user_fee(operator):
fees[FeeType.USER_FEE] = calculate_user_fee(amount_in_cents, operator)
# Commission subtracted from amount
if has_commission(operator):
fees[FeeType.COMMISSION] = calculate_commission(amount_in_cents, operator)
return feesTesting Your Integration
VantagePay provides a service-tester tool that issues each of the endpoint contracts described above with sample payloads. Coordinate with the VantagePay technical team to:
- Provision a test environment that points at your service
BaseUrl. - Run the tester against each endpoint your processor settings have enabled.
- Verify that your service handles repeat
debit/processcalls (idempotency), theRequiresPinre-entry flow, and thePendingpolling cycle correctly.
Deployment Considerations
Docker Configuration
Your service should be containerized and expose the API on the configured port:
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Health Probes
Implement liveness and readiness check endpoints for monitoring:
@app.get("/health/liveness")
async def liveness_check():
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
@app.get("/health/ready")
async def readiness_check():
# Check database connectivity, external service availability, etc.
return {"status": "ready"}@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");
}
}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" });
});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:
import os
PROVIDER_API_KEY = os.environ["PROVIDER_API_KEY"]
PROVIDER_BASE_URL = os.environ["PROVIDER_BASE_URL"]@Value("${PROVIDER_API_KEY}")
private String providerApiKey;
@Value("${PROVIDER_BASE_URL}")
private String providerBaseUrl;var providerApiKey = Environment.GetEnvironmentVariable("PROVIDER_API_KEY")
?? throw new InvalidOperationException("PROVIDER_API_KEY is not set.");
var providerBaseUrl = Environment.GetEnvironmentVariable("PROVIDER_BASE_URL")
?? throw new InvalidOperationException("PROVIDER_BASE_URL is not set.");const providerApiKey = process.env.PROVIDER_API_KEY;
const providerBaseUrl = process.env.PROVIDER_BASE_URL;
if (!providerApiKey || !providerBaseUrl) {
throw new Error("Required provider environment variables are missing.");
}Logging
Implement comprehensive logging for troubleshooting:
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)}")
raiseprivate 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;
}
}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;
}
});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:
- Input validation - Validate all incoming request fields before forwarding to your provider.
- Secrets management - Use environment variables, not hardcoded values, for provider API keys.
- Audit logging - Log all requests and responses (excluding PINs and full card numbers) for audit trails.
- Provider security - Use HTTPS with valid certificates for all outbound calls to mobile wallet providers.
Support and Integration
For integration support and questions about the VantagePay Payment Service Framework, contact the VantagePay technical team with:
- Your service implementation details and the language/framework you are using
- Sample request/response logs for any failing calls
- The specific error messages or status codes you are receiving
- Your mobile wallet operator and country/currency configuration requirements
This documentation covers the Mobile Wallet Debit integration. Additional documentation is available for other payment types including credit cards, bank accounts, and digital wallets.
