- 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:- An approved organisation (
org_id) — see the Organisation Onboarding flow - A valid API key or Bearer JWT — see Authentication
- 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 anorg_rate_id which locks in the rate and must be passed in the payment request.
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: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 abeneficiary_id. Fetch existing beneficiaries or create a new one:
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.
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):
Pattern C — Cross-border remittance (payin + payout)
Collect in the source currency and deliver in the destination currency. This requires anorg_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:
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.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 returns200 with a payment_id and transaction_id:
next_action values:
| Value | What to do |
|---|---|
instructions | Display any payment_instructions to the user. For M-PESA, the STK push has been sent — prompt the user to approve on their phone. |
redirect | Redirect the user to redirect_url to complete payment. |
wait | No user action needed — payment is processing in the background. |
Step 6 — Track payment status
Poll for the final outcome using thepayment_id returned in Step 5:
| Status | Meaning |
|---|---|
completed | Payment successful |
failed | Payment failed — check failure_reason |
canceled | Canceled before processing |
Step 7 — Receive webhook events
Register a webhook endpoint to receive real-time payment status updates instead of polling: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
| Field | Required | Description |
|---|---|---|
user_id | Yes | ID of the initiating user |
amount | Yes | Source amount |
currency | Yes | Source currency (ISO 4217) |
target_amount | Yes | Destination amount. Must equal amount for single-currency flows. |
target_currency | Yes | Destination currency. Must equal currency for single-currency flows. |
country | Yes | ISO alpha-2 country code of the payin origin |
payment_method | Yes | mobile_money, bank_transfer, or card |
direction | No | payin (default) or payout |
org_id | Yes | Your organisation ID |
org_rate_id | Cross-border only | Rate ID from the exchange rate lookup. Not required when currency == target_currency. |
client_reference | No | Your own idempotency reference for this transaction |
description | No | Human-readable description of the payment |
payer_details | Yes (payin) | Object with name, email, and optionally phone of the paying party |
mobile_money_details | Mobile money only | Object with provider (exact network name) and phone_number |
bank_details | Bank transfer only | Payer’s source bank account: account_number, account_name, bank_name, country, bank_code |
bank_account | Bank transfer only | Destination bank account: account_number, bank_code, bank_name |
beneficiary_id | Payout flows | ID of the registered beneficiary |
return_url | No | URL to redirect to after payment completion |
metadata | No | Arbitrary key-value pairs — included in webhook payloads |