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.
Alternative Payment Methods (APM)
Accept mobile money collections across 12+ African currencies, plus card / Instant EFT / wallet collections in South Africa via a hosted checkout. 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,south-africa(PayFast hosted checkout)successUrl/failUrl— Where hosted-redirect providers (e.g. South Africa) return the customer after payment
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.Example — Egypt Wallet (EGP)
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": 20,
"currency": "EGP",
"reference": "order_eg_123",
"description": "Payment for order #123",
"webhookUrl": "https://your-server.com/callback",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phoneCode": "20",
"phone": "1157477115"
},
"metadata": {
"provider": "wallet-egypt",
"network": "wallet",
"operator": "WALLET",
"countryCode": "EG"
}
}'WALLET or CASH (Fawry) — set via operator/metadata.operator. Payout is currently supported on CASH only; contact your account manager for other payout methods. See Centiwise's integration guide.Example — South Africa hosted checkout (ZAR)
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": 150,
"currency": "ZAR",
"reference": "order_za_123",
"description": "Payment for order #123",
"webhookUrl": "https://your-server.com/callback",
"successUrl": "https://your-site.com/thank-you",
"failUrl": "https://your-site.com/checkout",
"customer": {
"firstName": "Thabo",
"lastName": "Nkosi",
"email": "thabo@example.com",
"phoneCode": "27",
"phone": "821234567"
},
"metadata": {
"provider": "south-africa",
"countryCode": "ZA"
}
}' redirectUrl — send the customer there to pay by card, Instant EFT, Capitec Pay, SnapScan, Apple/Google Pay and more. There is no phone push and no customerCode. Final status arrives on your webhookUrl once PayFast confirms the payment. To skip the method picker, add metadata.paymentMethod (card, eft, capitec, snapscan, apple-pay, …). EFT / Capitec / SnapScan / Zapper are SA-only.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 }
}Supported Countries
🇰🇪 Kenya (KES)
M-Pesa, Airtel Money
🇸🇳 Senegal (XOF)
Orange Money, Wave, 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
🇪🇬 Egypt (EGP)
Wallet, Cash (Fawry) — payout on Cash only
🇿🇦 South Africa (ZAR)
Card, Instant EFT, Capitec Pay, SnapScan, Apple/Google Pay — hosted redirect
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)
The endpoints below generate and verify a Pexipay-issued OTP (sent by Pexipay via SMS/email). This is a standalone confirmation step you can use for your own flows (e.g. merchant_auth). It is not the code that mobile money providers like Nexterpay/Orange Money validate — see the note below.
Nexterpay / Orange Money (XOF) payments: the OTP comes from Orange, not from Pexipay
For pre-OTP mobile money providers (e.g. Orange Money Senegal/Côte d'Ivoire via Nexterpay), the customer obtains their confirmation code directly from the mobile money operator — typically by dialing the operator's USSD code or confirming in the operator's own app — not by calling POST /api/v1/apm/otp/request. Calling that endpoint will return a success response and a code, but it is a Pexipay-generated code the operator will never recognize — submitting it as metaData1 will always be rejected as an invalid/expired OTP.
- Direct the customer to confirm/authorize the payment with the operator on their own phone and retrieve the code the operator sends them
- Customer enters that operator-issued code in your checkout UI
- Call
POST /api/v1/apm/paymentsand include the code asmetadata.metaData1— Nexterpay forwards it to the operator for validation
The code is generally only valid for a few minutes from when the operator issues it, so submit the payment request promptly after the customer enters it.
Request OTP
POST /api/v1/apm/otp/request
Sends a 6-digit, Pexipay-generated 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. Use this for your own confirmation steps — do not use it to obtain the code for a Nexterpay/Orange Money metaData1 submission (see note above).
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 code that was generated by POST /api/v1/apm/otp/request above. This only validates Pexipay-issued codes — it has no relationship to an operator-issued Nexterpay/Orange Money code. For Orange Money XOF payments, skip this endpoint entirely and pass the operator-issued code directly as metadata.metaData1 in the payment request; Nexterpay validates it server-side with the operator.
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)
No call to /api/v1/apm/otp/request here — the code comes from the operator, not Pexipay.
// Step 1 – Customer confirms/authorizes the payment with the mobile money
// operator on their own phone (e.g. Orange Money USSD/app) and receives a
// confirmation code directly from the operator.
// Step 2 – Customer enters that operator-issued code in your checkout UI
const otp = '847291' // user input, issued by the operator (not Pexipay)
// 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, Wave, 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
🇪🇬 Egypt (EGP)
Cash (Fawry) only — contact your account manager for Wallet payouts
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.