Merchant API Documentation
Server-to-server API for card and mobile money payments
Base URL: https://gateway.pexipay.com
Authentication
Create API keys in the merchant portal at /m/api-keys. The plaintext key and secret are shown once at creation.
Include both headers on every request:
x-api-key: pk_...
x-api-secret: sk_...Try It – Card API
Sign in to your merchant account to use the interactive testing console.
Processing Modes
Card processing supports three modes: 2D, 3D, and AUTO.
- AUTO (recommended): Attempts 3D first, falls back to 2D if declined.
- 3D: Always requires 3D Secure authentication. Returns a
redirectUrl. - 2D: No redirect; status returned immediately.
⚠️ Critical: Capture Customer IP Address
When calling our payment API from your backend server, you MUST capture and pass your customer's actual IP address in the ipAddress field.
❌ Wrong:
Omitting ipAddress will cause the system to use your server's IP (e.g., 13.234.204.168), not your customer's location.
✅ Correct:
Capture the customer's IP on your frontend/checkout page and forward it in every API request.
Why This Matters:
- Fraud detection requires the actual cardholder's geographic location
- 3D Secure authentication uses IP for risk assessment
- Payment gateways may decline transactions with mismatched IPs
- Compliance requirements mandate accurate cardholder location data
Frontend Implementation:
<!-- Add to your checkout page -->
<script>
async function captureCustomerIP() {
try {
const response = await fetch('https://api.ipify.org?format=json');
const data = await response.json();
return data.ip;
} catch (error) {
console.error('Failed to get customer IP:', error);
return null;
}
}
// When customer clicks Pay button
async function processPayment() {
const customerIP = await captureCustomerIP();
// Send to your backend
await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 100.00,
ipAddress: customerIP, // ← REQUIRED
customer: { /* ... */ },
card: { /* ... */ }
})
});
}
</script>Backend Implementation:
// Your backend endpoint
app.post('/api/create-payment', async (req, res) => {
const { amount, ipAddress, customer, card } = req.body;
// Forward customer IP to PexiPay
const payment = await fetch('https://gateway.pexipay.com/api/v1/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.PEXIPAY_API_KEY,
'x-api-secret': process.env.PEXIPAY_API_SECRET,
},
body: JSON.stringify({
amount,
currency: 'USD',
ipAddress, // ← Customer's actual IP from frontend
customer,
billing: { /* ... */ },
card
})
});
res.json(await payment.json());
});💡 Alternative IP Services:
- •
https://api.ipify.org?format=json - •
https://api.my-ip.io/ip.json - •
https://ipapi.co/json/ - • Or extract from
X-Forwarded-Forheader if using a proxy
Card Payments
Create Payment
POST /api/v1/payments
Creates a new card payment. For 3DS flows, returns a redirectUrl.
Required fields
amount,currencycustomer: firstName, lastName, emailbilling: firstName, lastName, street, city, country, postCodecard: holder, number, expMonth, expYear, securityCode
Optional fields
returnUrl— Where to redirect after 3DSreference— Your order/invoice referencedescription— Payment descriptionipAddress— Customer IP address (used for fraud detection and 3DS)billing.state— State/province/region (recommended for some acquirers)
Example
curl -X POST http://localhost:3000/api/v1/payments \
-H 'Content-Type: application/json' \
-H 'x-api-key: pk_...' \
-H 'x-api-secret: sk_...' \
-d '{
"amount": 10.00,
"currency": "USD",
"reference": "order_123",
"returnUrl": "https://yoursite.com/payment/complete",
"ipAddress": "192.168.1.1",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com"
},
"billing": {
"firstName": "John",
"lastName": "Doe",
"street": "123 Main St",
"city": "New York",
"state": "NY",
"country": "US",
"postCode": "10001"
},
"card": {
"holder": "JOHN DOE",
"number": "4111111111111111",
"expMonth": "12",
"expYear": "30",
"securityCode": "123"
}
}'Response
{
"id": "tx_...",
"reference": "order_123",
"status": "INITIATED",
"redirectUrl": "https://gateway.example.com/3ds/...",
"returnUrl": "https://yoursite.com/payment/complete",
"createdAt": "2026-02-16T10:30:00.000Z"
}Force 3D Secure
POST /api/v1/payments/3d
Same request body as /api/v1/payments, but always forces 3DS authentication.
Mobile Money (APM) Payments
Accept mobile money collections across 12+ African currencies. Supported providers include Supported currencies include SLE, KES, NGN, GHS, TZS, UGX, XAF, XOF, ZAR, ZMW, EGP, RWF, and more.
Create APM Payment
POST /api/v1/apm/payments
Required fields
amount— Payment amount (number > 0)currency— ISO currency code (SLE, KES, NGN, GHS, XOF, TZS, etc.)
Optional fields
reference— Your order reference (auto-generated if omitted)description— Payment descriptionwebhookUrl— Per-transaction callback URL; overrides your account-level webhook URL for this transactioncustomerCode— Required for pre-OTP providers (e.g.orange-senegal)operatorormetadata.operator— Mobile money operator e.g.ORANGE,MPESA,MTNcountryCodeormetadata.countryCode— ISO2 country code e.g.SL,KE,NGmsisdn— Full phone number with country codecustomer.firstName,customer.lastName— Required by some providerscustomer.email— Required by some providerscustomer.phoneCode— Country calling code e.g.+232customer.phone— Phone numbermetadata.provider— Provider slug e.g.orange-sierra-leone,orange-senegal
Example — Sierra Leone (SLE)
curl -X POST https://gateway.pexipay.com/api/v1/apm/payments \
-H 'Content-Type: application/json' \
-H 'x-api-key: pk_...' \
-H 'x-api-secret: sk_...' \
-d '{
"amount": 100,
"currency": "SLE",
"reference": "order_sl_123",
"description": "Payment for order #123",
"webhookUrl": "https://your-server.com/callback",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneCode": "+232",
"phone": "23278666871"
},
"metadata": {
"provider": "orange-sierra-leone",
"network": "orange"
}
}'Example — Orange Senegal pre-OTP (XOF)
curl -X POST https://gateway.pexipay.com/api/v1/apm/payments \
-H 'Content-Type: application/json' \
-H 'x-api-key: pk_...' \
-H 'x-api-secret: sk_...' \
-d '{
"amount": 1500,
"currency": "XOF",
"reference": "order_sn_123",
"webhookUrl": "https://your-server.com/callback",
"customerCode": "123456",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneCode": "221",
"phone": "770000001"
},
"metadata": {
"provider": "orange-senegal"
}
}'orange-senegal), you must include customerCode in the request body.Response
{
"id": "apm_1781605937734_u0ousd2",
"pexiTransactionId": "PEXI1781605937728ZPQK",
"reference": "order_sl_123",
"amount": 100,
"currency": "SLE",
"status": "PENDING",
"chargeRequestId": "LDC20260616103218BONEN",
"paymentMethod": "PENDING",
"nextAction": "processing",
"createdAt": "2026-06-16T10:32:17.735Z",
"completedAt": null,
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "23278666871",
"phoneCode": "232"
},
"fraud": { "score": 0, "flags": [], "isFraudulent": false }
}Webhook Callback
When the collection reaches a final status, Pexipay POSTs to your webhookUrl (request body) or your account-level webhook URL:
{
"txId": "apm_1780394240274_kzarcxo",
"reference": "order_sl_123",
"status": "SUCCESS",
"gateway": {
"type": "async-response",
"paynet-order-id": "000155cc2fe6",
"merchant-order-id": "order_sl_123",
"status": "approved",
"amount": "100",
"currency": "SLE",
"customer-msisdn": "23278666871"
}
}Integration Flow
- For pre-OTP providers, collect the customer code first and pass it as
customerCode. - Call
POST /api/v1/apm/paymentswith all required fields. - If the response includes a
redirectUrl, send the customer there to complete payment. - Wait for the webhook callback to your
webhookUrl. - Fulfill on
status: "SUCCESS". Reject onFAILEDorCANCELLED.
Transaction Statuses
PENDING— Awaiting customer or network processingSUCCESS— Payment completed successfullyFAILED— Payment failed or declinedCANCELLED— Payment was cancelled
OTP (One-Time Password)
Some mobile money providers (e.g. Nexterpay for XOF) require a one-time password to be submitted alongside the payment request. Use these endpoints to generate and verify OTPs before initiating the payment.
OTP Payment Flow (Nexterpay / XOF)
- Call
POST /api/v1/apm/otp/request— delivers a 6-digit code to the customer - Customer enters the code in your checkout UI
- Call
POST /api/v1/apm/paymentsand include the code asmetadata.metaData1
Request OTP
POST /api/v1/apm/otp/request
Sends a 6-digit OTP to the customer via SMS, email, or both. The code expires after 10 minutes and each phone/email is limited to 5 requests per hour.
Required fields
deliveryMethod—sms,email, orbothpurpose—apm_payment,payout, ormerchant_authphoneNumber— Required whendeliveryMethodissmsorboth(E.164 format, e.g., +221761234567)email— Required whendeliveryMethodisemailorboth
Optional fields
reference— Your order/payment reference (recommended for correlation)
Example
curl -X POST https://gateway.pexipay.com/api/v1/apm/otp/request \
-H 'Content-Type: application/json' \
-H 'x-api-key: pk_...' \
-H 'x-api-secret: sk_...' \
-d '{
"phoneNumber": "+221761234567",
"deliveryMethod": "sms",
"purpose": "apm_payment",
"reference": "order_sn_123"
}'Response
{
"success": true,
"message": "OTP sent successfully",
"otpId": "otp_1746123456789_abc123",
"expiresIn": 600
}Verify OTP
POST /api/v1/apm/otp/verify
Verifies a 6-digit OTP code entered by the customer. Optional — you can skip this step and pass the code directly as metadata.metaData1 in the payment request. The provider will validate it server-side.
Required fields
code— 6-digit OTP entered by the customerphoneNumberoremail— Must match what was used in/otp/request
Example
curl -X POST https://gateway.pexipay.com/api/v1/apm/otp/verify \
-H 'Content-Type: application/json' \
-H 'x-api-key: pk_...' \
-H 'x-api-secret: sk_...' \
-d '{
"code": "847291",
"phoneNumber": "+221761234567"
}'Response
{
"success": true,
"message": "OTP verified successfully",
"reference": "order_sn_123"
}End-to-End Example (Nexterpay / XOF)
// Step 1 – Request OTP
const otpRes = await fetch('https://gateway.pexipay.com/api/v1/apm/otp/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.PEXIPAY_API_KEY,
'x-api-secret': process.env.PEXIPAY_API_SECRET,
},
body: JSON.stringify({
phoneNumber: '+221761234567',
deliveryMethod: 'sms',
purpose: 'apm_payment',
reference: 'order_sn_123',
}),
})
// { success: true, otpId: '...', expiresIn: 600 }
// Step 2 – Customer reads the SMS and enters the OTP in your UI
const otp = '847291' // user input
// Step 3 – Submit payment with OTP in metadata
const payRes = await fetch('https://gateway.pexipay.com/api/v1/apm/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.PEXIPAY_API_KEY,
'x-api-secret': process.env.PEXIPAY_API_SECRET,
},
body: JSON.stringify({
amount: 1000,
currency: 'XOF',
reference: 'order_sn_123',
operator: 'Orange',
countryCode: 'SN',
customer: {
firstName: 'Michael',
lastName: 'Marc',
email: 'michael@example.com',
phoneCode: '221',
phone: '761234567',
},
metadata: {
metaData1: otp, // ← OTP goes here
},
}),
})Hosted Checkout for Item Purchases
Use this flow when the customer clicks a buy button on your website and you want to redirect them to PexiPay hosted checkout.
Start Checkout
POST /api/merchant/checkout/start
Validates item and price on the server, creates a hosted payment link, and returns a redirect URL for the buyer.
Required fields
itemId— Server-known item identifierqty— Integer quantity (1-20)
Request Example
curl -X POST http://localhost:3000/api/merchant/checkout/start \
-H 'Content-Type: application/json' \
-H 'x-api-key: pk_...' \
-H 'x-api-secret: sk_...' \
-d '{
"itemId": "orange-weekly-bundle",
"qty": 2
}'Response
{
"itemId": "orange-weekly-bundle",
"qty": 2,
"unitPrice": 2500,
"amount": 5000,
"currency": "XOF",
"checkoutUrl": "https://your-pexipay-domain/pay/link_1707425123456_abc123"
}Frontend Redirect Example
async function startCheckout(itemId, qty) {
const res = await fetch('/api/merchant/checkout/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemId, qty })
})
const body = await res.json()
if (!res.ok || !body.checkoutUrl) {
throw new Error(body.message || 'Could not start checkout')
}
window.location.href = body.checkoutUrl
}End-to-End Flow
- Customer clicks buy button on merchant page
- Frontend calls
POST /api/merchant/checkout/startwithitemIdandqty - Merchant backend validates item and amount from server-side catalog
- Backend creates hosted payment link via merchant payment links API
- Frontend redirects to
checkoutUrlusingwindow.location.href - Customer pays on hosted page and is returned to configured merchant return/cancel URL
Security Note
- Never trust item prices from browser requests
- Always derive amount from server-side product catalog
- This endpoint accepts merchant API keys or an authenticated merchant session
- Merchant account must have active MERCHANT_APM access and active mobile money provider mapping
Mobile Money Payouts
Disburse funds to mobile money accounts across 12 African countries. Perfect for salary payments, vendor payments, and marketplaces.
Create Payout
POST /api/v1/apm/payouts
Initiates a payout to a beneficiary's mobile money account. Funds are typically received within 1-5 minutes.
Required fields
amount— Payout amount (number > 0)currency— ISO currency code (SLE, KES, NGN, GHS, XOF, TZS, etc.)beneficiary.firstName,beneficiary.lastNamebeneficiary.phone— Phone number (leading+is stripped automatically)
Optional fields
reference— Your payout reference (auto-generated if omitted)description— Payout descriptionwebhookUrl— Per-transaction callback URL; overrides your account-level webhook URL for this transactionbeneficiary.phoneCode— Country calling code e.g.+232— used for mobile money provider detectionbeneficiary.email— Beneficiary email addressbeneficiary.address— Beneficiary addressmetadata.countryCode— ISO2 country code e.g.SL,KE
Example — Sierra Leone (SLE)
curl -X POST https://gateway.pexipay.com/api/v1/apm/payouts \
-H 'Content-Type: application/json' \
-H 'x-api-key: pk_...' \
-H 'x-api-secret: sk_...' \
-d '{
"amount": 100,
"currency": "SLE",
"reference": "payout_sl_001",
"description": "Winnings withdrawal",
"webhookUrl": "https://your-server.com/payout-callback",
"beneficiary": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneCode": "+232",
"phone": "23278666871"
},
"metadata": { "countryCode": "SL" }
}'Response
{
"id": "payout_1780394242115_tzx0ldw",
"pexiTransactionId": "PAYOUT1780394242115HSCU",
"reference": "payout_sl_001",
"amount": 100,
"currency": "SLE",
"status": "PENDING",
"chargeRequestId": "566541948",
"createdAt": "2026-06-02T09:57:22.116Z",
"completedAt": null,
"customer": {
"firstName": "John",
"lastName": "Doe",
"phone": "23278666871",
"phoneCode": "232"
}
}Supported Countries
🇰🇪 Kenya (KES)
M-Pesa, Airtel Money
🇸🇳 Senegal (XOF)
Orange Money, Free Money
🇸🇱 Sierra Leone (SLE)
Orange Money, Africell
🇬🇭 Ghana (GHS)
MTN, Airtel, Vodafone
🇹🇿 Tanzania (TZS)
Vodacom, Airtel, Tigo
🇷🇼 Rwanda (RWF)
MTN, Airtel
🇿🇲 Zambia (ZMW)
MTN, Airtel, Zamtel
🇲🇼 Malawi (MWK)
Airtel, TNM
🇨🇲 Cameroon (XAF)
MTN, Orange
🇧🇯 Benin (XOF)
MTN, Moov
🇨🇮 Côte d'Ivoire (XOF)
MTN, Orange, Wave, Moov
🇨🇩 DR Congo (CDF)
Vodacom, Airtel, Orange
Payout Flow
- Call
POST /api/v1/apm/payoutswith beneficiary details and amount - Response includes
chargeRequestId— the provider's order ID - Beneficiary receives funds in their mobile money account (1–5 minutes)
- Pexipay forwards the final status to your
webhookUrl - Update payout records on
status: "SUCCESS"; handle"FAILED"accordingly
Payout Statuses
PENDING— Submitted, awaiting provider processingSUCCESS— Payout completed successfullyFAILED— Payout failed or was declined
Webhook Callback
When a payout completes or fails, Pexipay POSTs to your webhookUrl (request body) or your account-level webhook URL:
{
"txId": "payout_1780394242115_tzx0ldw",
"reference": "payout_sl_001",
"status": "SUCCESS",
"gateway": {
"type": "async-response",
"paynet-order-id": "566541948",
"merchant-order-id": "payout_sl_001",
"status": "approved",
"amount": "100",
"currency": "SLE"
}
}💡 Best Practices
- Always use unique references for each payout
- Validate phone numbers before submission (E.164 format)
- Use webhooks for final status confirmation
- Monitor your provider account balance before sending payouts
- Store payout records for reconciliation and audit trails
⚠️ Important Notes
- Payouts cannot be canceled once submitted
- Maximum payout amount varies by provider (e.g., KES 150,000 for M-Pesa)
- Ensure MERCHANT_APM role is enabled for your merchant account
- Some countries require service code configuration - contact support for setup
Transactions
List Card Transactions
GET /api/v1/transactions
Query params
limit— Default 20, max 100cursor— For paginationstatus— Filter by statusfrom,to— Date range (ISO format)
Get Card Transaction by ID
GET /api/v1/transactions/:id
Retrieve a transaction using its transaction ID. Add ?includeRaw=1 to include full gateway response.
Response
{
"id": "tx_1776396346622_9a97e7a",
"reference": "1194f606eb74e5e7212627f7afe628bd",
"amount": "10",
"currency": "USD",
"status": "PENDING",
"statusDescription": "Payment processing - awaiting confirmation",
"createdAt": "2026-04-17T03:25:46.623Z",
"acquirerId": "acq-borderpay-1776371782354",
"apiKeyId": "key_1776392320382_eivrvej"
}Example Status Descriptions:
Payment processed successfully- Successful paymentPayment processing - awaiting confirmation- Pending 3DSDo not honour- Card declined by issuerInsufficient funds- Not enough balanceCard expired- Expired cardBLOCKED- Transaction blocked
Note: Messages are automatically parsed to show the most user-friendly information from gateway responses.
Get Card Transaction by Reference
GET /api/v1/transactions/by-reference/:reference
Retrieve a transaction using its reference number (e.g., PEXI-ABC123 or your custom reference). If multiple transactions share the same reference, returns the most recent one. Add ?includeRaw=1 to include full gateway response.
When to Use
Use this endpoint when you have the reference number but not the transaction ID. Common use case: checking transaction status after webhook or return URL callback.
List APM Transactions
GET /api/v1/apm/transactions
Returns mobile money transactions with same query params as card transactions.
Get APM Transaction
GET /api/v1/apm/transactions/:id
Webhooks
PexiPay sends webhooks to notify your server about payment status changes in real-time.
Why Use Webhooks?
- Customers may close the browser before returning to your site
- Network issues can interrupt return URL redirects
- Enable automated order fulfillment and real-time updates
Configuration
Configure your webhook URL in the merchant dashboard at /m/settings.
Card Payment Webhook Payload
{
"txId": "tx_abc123...",
"status": "SUCCEEDED",
"amount": 10.00,
"currency": "USD",
"reference": "order_123",
"merchantId": "cm_...",
"timestamp": "2026-02-16T10:30:00.000Z",
"cardBrand": "VISA",
"cardLast4": "1111",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com"
}
}APM Payment Webhook Payload
{
"event": "successful_payment",
"amount": "1500",
"currency_code": "KES",
"payer_msisdn": "254712345678",
"payer_email": "john@example.com",
"payment_status": 700,
"charge_request_id": "123456789",
"external_reference": "order_123",
"payment_method_code": "MPESA_KEN",
"transaction_id": "2019",
"payer_transaction_id": "RBL35TKMUH",
"payment_date": "2026-02-16T10:30:00.000Z"
}Card Payment Statuses
- SUCCEEDED — Payment completed
- DECLINED — Declined by issuing bank
- FAILED — Processing error
- CANCELLED — Customer cancelled
- PENDING — Awaiting final status
Implementation Example
app.post('/webhooks/pexipay', async (req, res) => {
// Acknowledge receipt immediately
res.status(200).json({ received: true });
const { txId, status, reference } = req.body;
switch (status) {
case 'SUCCEEDED':
await fulfillOrder(reference);
break;
case 'DECLINED':
case 'FAILED':
await notifyPaymentFailed(reference);
break;
}
});Timeouts & Retries
PexiPay expects a 200 response within 5 seconds. Failed webhooks are retried with exponential backoff.
Hosted Payment Links
Create payment links in the dashboard and accept card payments via a hosted flow. No API key is required for paying a link.
Pay Link
POST /api/payment-links/:id/pay
Required fields
returnUrl— Redirect URL after 3DScustomer— firstName, lastName, emailbilling— Full billing addresscard— Card details
Error Codes
400— Invalid request parameters401— Missing or invalid API credentials403— Merchant not authorized for this payment type404— Resource not found502— Payment gateway error503— Service temporarily unavailable
Postman Collection
Download and import for quick testing.
Set environment variables apiKey and apiSecret from your merchant portal.