Skip to main content
This guide walks through how to initiate payments via the PCX API. It covers three integration patterns:
  • Payin only — collect funds from a user in a single currency
  • Payout only — disburse funds to a beneficiary in a single currency
  • Cross-border remittance — collect in one currency and pay out in another

Prerequisites

Before initiating any payment, your integration must have:
  1. An approved organisation (org_id) — see the Organisation Onboarding flow
  2. A valid API key or Bearer JWT — see Authentication
  3. A registered beneficiary (beneficiary_id) for payout flows — see Beneficiaries


Step 1 — Fetch the exchange rate (cross-border only)

Skip this step if you are doing a single-currency payin or payout. For cross-border flows, fetch the rate for the corridor before initiating the payment. This returns an org_rate_id which locks in the rate and must be passed in the payment request.
GET /organizations/admin/exchange-rate?orgId={org_id}&fromCurrency={from}&toCurrency={to}
Example:
GET /organizations/admin/exchange-rate?orgId=k257f15d-3a59-4b9f-afdc-087fc2903eer&fromCurrency=NGN&toCurrency=KES
Response:
{
  "org_rate_id": "258b95aa-a76f-4144-ade1-23814a32900d",
  "from_currency": "NGN",
  "to_currency": "KES",
  "rate": 0.09127444176,
  "expires_at": "2026-06-04T12:00:00.000Z"
}
Store the org_rate_id and initiate the payment promptly — rates expire.

Step 2 — Fetch available networks or banks (mobile money / bank payout)

Fetch the available payment networks (mobile money operators and banks) for the destination country:
POST /externals/networks
{ "country_code": "{country_code}" }
The response returns each network’s name, code, and type (momo or bank). The upstream provider is selected automatically based on the country code. Present the list to the user. For mobile money payouts, pass the network name as provider inside mobile_money_details. For bank-transfer beneficiaries, use the code as bank_code.

Step 3 — Fetch or create a beneficiary

All payments require a beneficiary_id. Fetch existing beneficiaries or create a new one:
GET /beneficiaries?user_id={user_id}
POST /beneficiaries
See Beneficiaries for the full schema.

Step 4 — Initiate the payment

Pattern A — Payin only (single currency)

Collect funds from a user. No FX conversion. currency and target_currency must be identical, and amount must equal target_amount. org_rate_id is not required.
POST /payments-init/initiate/public-payin
Mobile money payin example (KES):
{
  "user_id": "16a20294-a031-700c-e47a-74613d6596cb",
  "amount": 1500.0,
  "target_amount": 1500.0,
  "currency": "KES",
  "target_currency": "KES",
  "country": "KE",
  "payment_method": "mobile_money",
  "direction": "payin",
  "org_id": "k257f15d-3a59-4b9f-afdc-087fc2903eer",
  "client_reference": "TX-your-reference",
  "payer_details": {
    "name": "Jane Kamau",
    "email": "jane@example.com",
    "phone": "+254712345678"
  },
  "mobile_money_details": {
    "provider": "Mobile Wallet (M-PESA)",
    "phone_number": "+254712345678"
  },
  "metadata": {
    "reason": "deposit"
  }
}
The provider value in mobile_money_details must match the network name exactly as returned by POST /externals/networks. For mobile_money flows you must also pass networkId — the id value from the same response — alongside mobile_money_details. Without it, the payment will fail to route to the correct mobile money operator.

Pattern B — Payout only (single currency)

Disburse funds to a beneficiary. Same rules apply — currency and target_currency must match, as must amount and target_amount. No org_rate_id. Mobile money payout example (RWF):
{
  "user_id": "16a20294-a031-700c-e47a-74613d6596cb",
  "amount": 1500.0,
  "target_amount": 1500.0,
  "currency": "RWF",
  "target_currency": "RWF",
  "country": "RW",
  "payment_method": "mobile_money",
  "direction": "payout",
  "org_id": "k257f15d-3a59-4b9f-afdc-087fc2903eer",
  "client_reference": "TX-your-reference",
  "payer_details": {
    "name": "Sender Name",
    "email": "sender@example.com",
    "phone": "+2348122603628"
  },
  "mobile_money_details": {
    "provider": "MTN",
    "phone_number": "+250786632360"
  },
  "beneficiary_id": "a07f1e7e-6546-4143-993e-2c9f07151634",
  "metadata": {
    "reason": "gift"
  }
}

Pattern C — Cross-border remittance (payin + payout)

Collect in the source currency and deliver in the destination currency. This requires an org_rate_id from Step 1. currency and target_currency will differ, and target_amount is the converted amount at the locked rate. Example: NGN payin → KES payout via bank transfer:
{
  "user_id": "16a20294-a031-700c-e47a-74613d6596cb",
  "amount": 10000.00,
  "target_amount": 912.74,
  "currency": "NGN",
  "target_currency": "KES",
  "org_rate_id": "258b95aa-a76f-4144-ade1-23814a32900d",
  "country": "NG",
  "payment_method": "bank_transfer",
  "direction": "payin",
  "org_id": "k257f15d-3a59-4b9f-afdc-087fc2903eer",
  "client_reference": "TX-1780587197122-mtadahxl",
  "description": "Transfer to Erick Adikah in Kenya",
  "payer_details": {
    "email": "sender@example.com",
    "name": "Sender Name",
    "phone": "+2348000000000"
  },
  "bank_details": {
    "account_name": "Sender Name",
    "account_number": "4322789043216789",
    "bank_name": "other",
    "country": "NG",
    "bank_code": null
  },
  "bank_account": {
    "account_number": "4322789043216789",
    "bank_code": null,
    "bank_name": "other"
  },
  "beneficiary_id": "a07f1e7e-6546-4143-993e-2c9f07151634",
  "return_url": "https://your-app.com/transaction-success",
  "metadata": {
    "beneficiary_name": "Erick Adikah",
    "exchange_rate": "0.09127444176",
    "target_amount": "912.74",
    "target_currency": "KES",
    "reason": "gift"
  }
}
In cross-border flows, direction refers to the payin leg. PCX automatically initiates the payout to the beneficiary once the payin is confirmed.
Both bank_details and bank_account are required for bank transfer flows. bank_details carries the payer’s source account; bank_account carries the destination account details used to route the payout. The payer_details you submit must correlate with the actual sender of the funds — if they do not match the account holder on bank_details, the payment will be reversed and the funds returned to the sender.
Retail payments (customer_type = "retail") cannot currently be processed on some destination corridors due to upstream KYC data processing constraints. For affected corridors, submit the request with customer_type = "institution" and include business_id and business_name.

Step 5 — Handle the response

A successful initiation returns 200 with a payment_id and transaction_id:
{
  "statusCode": 200,
  "response": {
    "success": true,
    "provider_payment_id": "fc90546e-ba5d-59fa-9a8c-1bb41f1f41dc",
    "payment_id": "ea5965e4-1432-40ef-bccf-cb51455a935a",
    "status": "process",
    "next_action": "instructions",
    "redirect_url": "",
    "payment_instructions": null,
    "expires_at": "2026-06-04T11:34:34.128120Z"
  },
  "transaction_id": "2a679c96-69bf-424c-89cb-c2f4a90a2738"
}
next_action values:
ValueWhat to do
instructionsDisplay any payment_instructions to the user. For M-PESA, the STK push has been sent — prompt the user to approve on their phone.
redirectRedirect the user to redirect_url to complete payment.
waitNo user action needed — payment is processing in the background.

Step 6 — Track payment status

Poll for the final outcome using the payment_id returned in Step 5:
GET /payments/{payment_id}
Or look up by transaction ID:
GET /payments/transaction/{transaction_id}
Terminal statuses:
StatusMeaning
completedPayment successful
failedPayment failed — check failure_reason
canceledCanceled before processing

Step 7 — Receive webhook events

Register a webhook endpoint to receive real-time payment status updates instead of polling:
POST /public/webhooks
{
  "endpoint_url": "https://your-app.com/webhooks/pcx"
}
PCX delivers a POST to your endpoint when the payment status changes. Respond with 2xx to confirm receipt. Failed deliveries are retried with exponential backoff. See Webhooks for the full event schema and retry policy.

Field reference

FieldRequiredDescription
user_idYesID of the initiating user
amountYesSource amount
currencyYesSource currency (ISO 4217)
target_amountYesDestination amount. Must equal amount for single-currency flows.
target_currencyYesDestination currency. Must equal currency for single-currency flows.
countryYesISO alpha-2 country code of the payin origin
payment_methodYesmobile_money, bank_transfer, or card
directionNopayin (default) or payout
org_idYesYour organisation ID
org_rate_idCross-border onlyRate ID from the exchange rate lookup. Not required when currency == target_currency.
client_referenceNoYour own idempotency reference for this transaction
descriptionNoHuman-readable description of the payment
payer_detailsYes (payin)Object with name, email, and optionally phone of the paying party
mobile_money_detailsMobile money onlyObject with provider (exact network name) and phone_number
bank_detailsBank transfer onlyPayer’s source bank account: account_number, account_name, bank_name, country, bank_code
bank_accountBank transfer onlyDestination bank account: account_number, bank_code, bank_name
beneficiary_idPayout flowsID of the registered beneficiary
return_urlNoURL to redirect to after payment completion
metadataNoArbitrary key-value pairs — included in webhook payloads