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_...
Security: API keys are for server-to-server identification only. Never embed them in mobile apps or browser code.

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-For header 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, currency
  • customer: firstName, lastName, email
  • billing: firstName, lastName, street, city, country, postCode
  • card: holder, number, expMonth, expYear, securityCode

Optional fields

  • returnUrl — Where to redirect after 3DS
  • reference — Your order/invoice reference
  • description — Payment description
  • ipAddress — 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 description
  • webhookUrl — Per-transaction callback URL; overrides your account-level webhook URL for this transaction
  • customerCode — Required for pre-OTP providers (e.g. orange-senegal)
  • operator or metadata.operator — Mobile money operator e.g. ORANGE, MPESA, MTN
  • countryCode or metadata.countryCode — ISO2 country code e.g. SL, KE, NG
  • msisdn — Full phone number with country code
  • customer.firstName, customer.lastName — Required by some providers
  • customer.email — Required by some providers
  • customer.phoneCode — Country calling code e.g. +232
  • customer.phone — Phone number
  • metadata.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"
    }
  }'
Pre-OTP rule: For providers that require pre-OTP (e.g. 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

  1. For pre-OTP providers, collect the customer code first and pass it as customerCode.
  2. Call POST /api/v1/apm/payments with all required fields.
  3. If the response includes a redirectUrl, send the customer there to complete payment.
  4. Wait for the webhook callback to your webhookUrl.
  5. Fulfill on status: "SUCCESS". Reject on FAILED or CANCELLED.

Transaction Statuses

  • PENDING — Awaiting customer or network processing
  • SUCCESS — Payment completed successfully
  • FAILED — Payment failed or declined
  • CANCELLED — 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)

  1. Call POST /api/v1/apm/otp/request — delivers a 6-digit code to the customer
  2. Customer enters the code in your checkout UI
  3. Call POST /api/v1/apm/payments and include the code as metadata.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

  • deliveryMethodsms, email, or both
  • purposeapm_payment, payout, or merchant_auth
  • phoneNumber — Required when deliveryMethod is sms or both (E.164 format, e.g., +221761234567)
  • email — Required when deliveryMethod is email or both

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
}
Rate limits: Max 5 OTP requests per phone/email per hour. Each OTP allows up to 3 verification attempts before being invalidated.

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 customer
  • phoneNumber or email — 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 identifier
  • qty — 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

  1. Customer clicks buy button on merchant page
  2. Frontend calls POST /api/merchant/checkout/start with itemId and qty
  3. Merchant backend validates item and amount from server-side catalog
  4. Backend creates hosted payment link via merchant payment links API
  5. Frontend redirects to checkoutUrl using window.location.href
  6. 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.lastName
  • beneficiary.phone — Phone number (leading + is stripped automatically)

Optional fields

  • reference — Your payout reference (auto-generated if omitted)
  • description — Payout description
  • webhookUrl — Per-transaction callback URL; overrides your account-level webhook URL for this transaction
  • beneficiary.phoneCode — Country calling code e.g. +232 — used for mobile money provider detection
  • beneficiary.email — Beneficiary email address
  • beneficiary.address — Beneficiary address
  • metadata.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

  1. Call POST /api/v1/apm/payouts with beneficiary details and amount
  2. Response includes chargeRequestId — the provider's order ID
  3. Beneficiary receives funds in their mobile money account (1–5 minutes)
  4. Pexipay forwards the final status to your webhookUrl
  5. Update payout records on status: "SUCCESS"; handle "FAILED" accordingly

Payout Statuses

  • PENDING — Submitted, awaiting provider processing
  • SUCCESS — Payout completed successfully
  • FAILED — 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

Try It – APM API

Sign in to your merchant account to test mobile money payments.

Transactions

List Card Transactions

GET /api/v1/transactions

Query params

  • limit — Default 20, max 100
  • cursor — For pagination
  • status — Filter by status
  • from, 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 payment
  • Payment processing - awaiting confirmation - Pending 3DS
  • Do not honour - Card declined by issuer
  • Insufficient funds - Not enough balance
  • Card expired - Expired card
  • BLOCKED - 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.

Error Codes

  • 400 — Invalid request parameters
  • 401 — Missing or invalid API credentials
  • 403 — Merchant not authorized for this payment type
  • 404 — Resource not found
  • 502 — Payment gateway error
  • 503 — Service temporarily unavailable

Postman Collection

Download and import for quick testing.

Set environment variables apiKey and apiSecret from your merchant portal.