Skip to main content
This guide walks through integrating crypto conversion flows via the PCX API. PCX acts as the orchestrator — your users send or receive fiat, and PCX handles the crypto side and settlement automatically. Two directions are supported:
  • On-ramp — customer pays in fiat, receives crypto directly to their wallet
  • Off-ramp — customer sends crypto, receives fiat to a bank or mobile money account

Prerequisites

Before initiating a ramp, your integration must have:
  1. A valid API key — see Authentication
  2. An approved organisation (org_id)
  3. A registered beneficiary (beneficiary_id) for off-ramp flows — see Beneficiaries
The ramp endpoint uses API key authentication. Pass your key in the X-Api-Key header.

Step 1 — Fetch the exchange rate

Fetch the current rate for the corridor before initiating the ramp. This gives you a rate to present to the user and an org_rate_id that you must pass in the ramp request.
GET /organizations/admin/exchange-rate?orgId={org_id}&fromCurrency={from_currency}&toCurrency={to_currency}
The direction of the corridor — which currency to send as fromCurrency vs toCurrency — depends on the ramp direction you intend to initiate. The rate must be fetched in the same direction the funds flow:
Ramp directionfromCurrencytoCurrency
on_ramp (fiat → crypto)Fiat currency (e.g. NGN)Crypto’s fiat equivalent (e.g. USD)
off_ramp (crypto → fiat)Crypto’s fiat equivalent (e.g. USD)Fiat currency (e.g. NGN)
The crypto’s fiat equivalent is mapped as follows:
Crypto assetFiat equivalent
USDCUSD
USDTUSD
EURCEUR
BTC, ETH, etc.Use the fiat currency directly
If you fetch the rate in the wrong direction for the ramp you are initiating, the request fails with RATE_STALE. Always align the rate fetch direction with the ramp direction.
Example — on-ramp NGN → USDC:
GET /organizations/admin/exchange-rate?orgId=k257f15d-3a59-4b9f-afdc-087fc2903eer&fromCurrency=NGN&toCurrency=USD
Example — off-ramp USDC → NGN:
GET /organizations/admin/exchange-rate?orgId=k257f15d-3a59-4b9f-afdc-087fc2903eer&fromCurrency=USD&toCurrency=NGN
Response:
{
  "org_rate_id": "258b95aa-a76f-4144-ade1-23814a32900d",
  "from_currency": "NGN",
  "to_currency": "USD",
  "rate": 0.00000645,
  "expires_at": "2026-06-04T12:00:00.000Z"
}
Store the rate (to show to your user as client_rate) and the org_rate_id. Both are required in the ramp request. Rates are spread-adjusted and validated against the live market on every fetch — always use the latest value before presenting it to a user.

Step 2 — Initiate the ramp

On-ramp — fiat → crypto

The customer pays in fiat. PCX settles the crypto directly to the wallet_address you provide. No further action is needed on your end after a successful initiation.
POST /payments-init/ramp
X-Api-Key: {your-api-key}
Example — NGN mobile money payin → USDC on Ethereum:
{
  "direction": "on_ramp",
  "user_id": "16a20294-a031-700c-e47a-74613d6596cb",
  "org_id": "k257f15d-3a59-4b9f-afdc-087fc2903eer",
  "fiat_currency": "NGN",
  "fiat_amount": 10000.00,
  "client_rate": 0.00000645,
  "crypto_currency": "USDC",
  "crypto_network": "ethereum",
  "payment_method": "mobile_money",
  "country": "NG",
  "payer_details": {
    "name": "Jane Kamau",
    "email": "jane@example.com",
    "phone": "+2348122603628"
  },
  "wallet_address": "0xABC123...your-wallet-address",
  "mobile_money_details": {
    "provider": "MTN",
    "phone_number": "+2348122603628"
  },
  "metadata": {
    "reason": "crypto_purchase"
  }
}
PCX handles the crypto payout directly. Once the fiat collection is confirmed, the crypto is settled to wallet_address automatically — you do not need to initiate a separate payout.

Off-ramp — crypto → fiat

The customer sends crypto; PCX arranges for fiat to be paid out to their bank or mobile money account. The beneficiary must be registered before initiating. Example — USDC → NGN bank payout:
{
  "direction": "off_ramp",
  "user_id": "16a20294-a031-700c-e47a-74613d6596cb",
  "org_id": "k257f15d-3a59-4b9f-afdc-087fc2903eer",
  "fiat_currency": "NGN",
  "fiat_amount": 10000.00,
  "client_rate": 0.00000645,
  "crypto_currency": "USDC",
  "crypto_network": "ethereum",
  "payment_method": "bank_transfer",
  "country": "NG",
  "payer_details": {
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "+2348000000000"
  },
  "beneficiary_id": "a07f1e7e-6546-4143-993e-2c9f07151634",
  "beneficiary_name": "John Doe",
  "destination": {
    "accountNumber": "0123456789",
    "bankCode": "058",
    "bankName": "GTBank"
  },
  "metadata": {
    "reason": "crypto_sale"
  }
}

Step 3 — Handle the response

A successful ramp initiation returns 200. The response now includes next_action and payment_instructions so you can drive the customer to the right next step without a second API call. On-ramp — bank transfer (next_action: make_transfer):
{
  "statusCode": 200,
  "response": {
    "success": true,
    "payment_id": "ea5965e4-1432-40ef-bccf-cb51455a935a",
    "transaction_id": "2a679c96-69bf-424c-89cb-c2f4a90a2738",
    "direction": "on_ramp",
    "status": "pending",
    "fiat_currency": "NGN",
    "fiat_amount": 10000.00,
    "crypto_currency": "USDC",
    "crypto_network": "ethereum",
    "crypto_amount": 6.45,
    "expires_at": "2026-06-04T11:34:34.128120Z",
    "next_action": "make_transfer",
    "payment_instructions": {
      "type": "bank_transfer",
      "account_name": "PCX Collections",
      "account_number": "0123456789",
      "bank_name": "GTBank",
      "amount": 10000.00,
      "currency": "NGN",
      "reference": "PCX-ea5965e4"
    }
  }
}
On-ramp — mobile money (next_action: approve_prompt):
{
  "next_action": "approve_prompt",
  "payment_instructions": {
    "type": "mobile_money",
    "message": "An M-PESA STK push has been sent to +254700000000. Approve on your phone to complete payment.",
    "amount": 10000.00,
    "currency": "KES"
  }
}
Off-ramp (next_action: send_crypto):
{
  "next_action": "send_crypto",
  "payment_instructions": {
    "type": "crypto_transfer",
    "wallet_address": "0xDEF456...pcx-deposit-address",
    "crypto_amount": 6.45,
    "crypto_currency": "USDC",
    "crypto_network": "ethereum",
    "expires_at": "2026-06-04T11:34:34.128120Z"
  }
}
Send the exact crypto_amount to wallet_address on crypto_network before expires_at. Once the deposit is confirmed on-chain, PCX disburses the fiat to the registered beneficiary automatically.
The deposit must match crypto_amount, crypto_currency, and crypto_network exactly. Sending the wrong asset, wrong network, or a different amount can delay or fail the payout.

next_action values

ValueWhenWhat to do
make_transferOn-ramp bank transferDisplay payment_instructions (account name, number, bank, reference) so the customer can send funds. The reference is mandatory — payments without it may not reconcile.
approve_promptOn-ramp mobile moneyThe provider has pushed a prompt (e.g. M-PESA STK push) to the customer’s phone. Show payment_instructions.message and prompt them to approve.
send_cryptoOff-rampDisplay payment_instructions (wallet address, crypto amount, currency, network, expiry) so the customer can send crypto to the PCX-provided wallet. Fiat payout to the beneficiary starts once the deposit confirms.
waitAny flow with no client action (e.g. off-ramp when the provider did not return settlement details)PCX continues processing in the background. Wait for the webhook or poll for status.
nullProvider data missingThe initiation succeeded but instructions could not be built (for example, an on-ramp bank transfer where bankInfo was missing). Fall back to polling and surface a generic “processing” state.
The crypto_amount reflects what the customer will receive (or send, for off-ramp) at the locked spread-adjusted rate. The expires_at field indicates when the collection window closes — prompt your user to complete payment before then.

Rate validation and the RATE_STALE error

PCX validates the client_rate you submit against the current live rate. If it has drifted more than 0.5%, the request is rejected:
{
  "statusCode": 422,
  "error": "RATE_STALE",
  "message": "Rate has drifted. Please re-fetch the exchange rate."
}
When you receive RATE_STALE, re-fetch the rate from GET /organizations/admin/exchange-rate, present the updated rate to the user, and re-submit with the new client_rate and org_rate_id.
Always fetch a fresh rate immediately before presenting it to the user and before submitting the ramp request. Do not cache rates across sessions or reuse org_rate_id values.

Step 4 — Track ramp status

Poll for the final outcome using the payment_id returned in Step 3:
GET /payments/{payment_id}
Or look up by transaction ID:
GET /payments/transaction/{transaction_id}
Terminal statuses:
StatusMeaning
completedRamp successful — crypto settled (on-ramp) or fiat paid out (off-ramp)
failedRamp failed — check failure_reason
canceledCanceled before processing

Step 5 — Receive webhook events

Register a webhook endpoint to receive real-time 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 ramp status changes. Respond with 2xx to confirm receipt. Failed deliveries are retried with exponential backoff. See Webhooks for the full event schema.

Field reference

Common fields (both directions)

FieldRequiredDescription
directionYeson_ramp (fiat → crypto) or off_ramp (crypto → fiat)
user_idYesID of the initiating user
org_idYesYour organisation ID
fiat_currencyYesISO 4217 fiat currency code (e.g. NGN, KES)
fiat_amountYesAmount in fiat currency — what the customer pays (on-ramp) or receives (off-ramp)
client_rateYesThe rate you fetched and displayed to the user. Must be within 0.5% of the current live rate.
crypto_currencyYesCrypto asset code (e.g. USDC, USDT, EURC, BTC)
crypto_networkYesBlockchain network (e.g. ethereum, polygon, tron)
payment_methodYesmobile_money or bank_transfer
countryYesISO alpha-2 country code of the fiat side
payer_detailsYesObject with name, email, and optionally phone of the paying party
customer_typeNoretail (default) or institution
business_idInstitution onlyRequired when customer_type is institution
business_nameInstitution onlyRequired when customer_type is institution
metadataNoArbitrary key-value pairs included in webhook payloads
Retail customers (customer_type = "retail") cannot currently be processed on some 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.

On-ramp fields

FieldRequiredDescription
wallet_addressYesDestination crypto wallet address. PCX settles crypto here directly.
mobile_money_detailsMobile money onlyObject with provider and phone_number
bank_detailsBank transfer onlyPayer’s source bank account details

Off-ramp fields

FieldRequiredDescription
beneficiary_idYesID of the registered beneficiary receiving the fiat payout
beneficiary_nameYesFull name of the beneficiary
destinationYesDestination routing details — bank account number, bank code, and bank name