# Create API key Source: https://docs.pcxpay.com/api-reference/api-keys/create-api-key /api-reference/external/openapi.yaml post /public/api-keys/create Creates a new API key. The full key value is returned once at creation and cannot be retrieved again — store it securely. # Delete API key Source: https://docs.pcxpay.com/api-reference/api-keys/delete-api-key /api-reference/external/openapi.yaml delete /public/api-keys/{prefix} Permanently deletes an API key. Any integration using this key will immediately lose access. # List API keys Source: https://docs.pcxpay.com/api-reference/api-keys/list-api-keys /api-reference/external/openapi.yaml get /public/api-keys Returns a paginated list of API keys for an organisation. # Revoke API key Source: https://docs.pcxpay.com/api-reference/api-keys/revoke-api-key /api-reference/external/openapi.yaml patch /public/api-keys/{prefix}/revoke Disables an API key without deleting it. The key is preserved for audit purposes but rejected on all subsequent requests. # Validate API key Source: https://docs.pcxpay.com/api-reference/api-keys/validate-api-key /api-reference/external/openapi.yaml post /public/api-keys/validate Checks whether a given API key is valid and active. Returns the associated `user_id`, `org_id`, and key status. No authentication is required on this endpoint — pass `NONE` for both headers. # Exchange TrueLayer auth code Source: https://docs.pcxpay.com/api-reference/bank-validation/exchange-truelayer-auth-code /api-reference/external/openapi.yaml post /payments/bank-validation/truelayer/callback Called after the TrueLayer OAuth redirect. Exchanges the authorization code for access and refresh tokens. Use the returned `access_token` for account holder verification. # Initiate TrueLayer OAuth flow Source: https://docs.pcxpay.com/api-reference/bank-validation/initiate-truelayer-oauth-flow /api-reference/external/openapi.yaml get /payments/bank-validation/truelayer/auth/init Returns an `auth_url` that the frontend must redirect the user to in order to grant TrueLayer access to their bank account for identity verification. # List supported Nigerian banks Source: https://docs.pcxpay.com/api-reference/bank-validation/list-supported-nigerian-banks /api-reference/external/openapi.yaml get /payments/bank-validation/nigerian-banks Returns the list of Nigerian banks with their bank codes. # List supported UK banks Source: https://docs.pcxpay.com/api-reference/bank-validation/list-supported-uk-banks /api-reference/external/openapi.yaml get /payments/bank-validation/uk-banks Returns the list of UK bank provider IDs accepted by the TrueLayer OAuth flow. # Validate Nigerian bank account Source: https://docs.pcxpay.com/api-reference/bank-validation/validate-nigerian-bank-account /api-reference/external/openapi.yaml post /payments/bank-validation/validate-nigerian-account Resolves an account number against its bank. # Validate UK account format Source: https://docs.pcxpay.com/api-reference/bank-validation/validate-uk-account-format /api-reference/external/openapi.yaml post /payments/bank-validation/validate-uk-account Performs basic format validation on a UK sort code and account number. # Validate UK account via modulus check Source: https://docs.pcxpay.com/api-reference/bank-validation/validate-uk-account-via-modulus-check /api-reference/external/openapi.yaml post /payments/bank-validation/validate-uk-account-checker Performs full modulus checking on a UK sort code and account number. # Verify UK account holder name Source: https://docs.pcxpay.com/api-reference/bank-validation/verify-uk-account-holder-name /api-reference/external/openapi.yaml post /payments/bank-validation/verify-uk-account-holder Verifies a name matches the account holder on record via TrueLayer. Requires an `accessToken` from the TrueLayer OAuth flow. # Create a beneficiary Source: https://docs.pcxpay.com/api-reference/beneficiaries/create-a-beneficiary /api-reference/external/openapi.yaml post /beneficiaries Creates a new payment beneficiary. The `beneficiary_of` field is resolved automatically from the authenticated user's credentials. For loan-client beneficiaries on behalf of an organisation, pass `org_id` in the body. # Delete a beneficiary Source: https://docs.pcxpay.com/api-reference/beneficiaries/delete-a-beneficiary /api-reference/external/openapi.yaml delete /beneficiaries/{beneficiary_id} # Get a beneficiary Source: https://docs.pcxpay.com/api-reference/beneficiaries/get-a-beneficiary /api-reference/external/openapi.yaml get /beneficiaries/{beneficiary_id} # List beneficiaries Source: https://docs.pcxpay.com/api-reference/beneficiaries/list-beneficiaries /api-reference/external/openapi.yaml get /beneficiaries Returns a paginated list of beneficiaries for the authenticated user. # Update a beneficiary Source: https://docs.pcxpay.com/api-reference/beneficiaries/update-a-beneficiary /api-reference/external/openapi.yaml put /beneficiaries/{beneficiary_id} Updates mutable fields on an existing beneficiary. Only fields present in the request body are changed. `account_type` and `beneficiary_of` cannot be changed after creation. # Add a milestone Source: https://docs.pcxpay.com/api-reference/escrow/add-a-milestone /api-reference/external/openapi.yaml post /virtual-accounts/escrow/{escrow_id}/milestones Adds a milestone to an existing escrow project. Requires `org-admin` OrgMember role and the `escrow` feature. # Assign parties to a milestone Source: https://docs.pcxpay.com/api-reference/escrow/assign-parties-to-a-milestone /api-reference/external/openapi.yaml post /virtual-accounts/escrow/{escrow_id}/milestones/{milestone_id}/parties Assigns one or more registered parties to this milestone with a payout split. Each `party_id` must already be registered on the escrow project via `POST /virtual-accounts/escrow/{escrow_id}/parties`. The body also accepts a single assignment object or a bare array instead of the `parties` wrapper. Requires `org-admin` OrgMember role and the `escrow` feature. # Cancel an escrow project Source: https://docs.pcxpay.com/api-reference/escrow/cancel-an-escrow-project /api-reference/external/openapi.yaml delete /virtual-accounts/escrow/{escrow_id} Cancels the escrow entity. Only permitted while the escrow is unfunded — returns a conflict once funds have been deposited. Requires `org-admin` OrgMember role and the `escrow` feature. # Create an escrow payment link Source: https://docs.pcxpay.com/api-reference/escrow/create-an-escrow-payment-link /api-reference/external/openapi.yaml post /virtual-accounts/escrow/{escrow_id}/payment-link Mints a hosted payment link for the escrow project — e.g. an agency sends it to their client to deposit a milestone payment. Returns the `link_ref` (deposit-match key) and the hosted URL to share. Incoming deposits are matched to the escrow on `link_ref`. Requires the `escrow` feature. # Create an escrow project Source: https://docs.pcxpay.com/api-reference/escrow/create-an-escrow-project /api-reference/external/openapi.yaml post /virtual-accounts/escrow Creates a new escrow entity (project) for the caller's organisation, optionally with milestones and parties inline. Requires the `escrow` feature to be enabled for the organisation. # Get a milestone's contract Source: https://docs.pcxpay.com/api-reference/escrow/get-a-milestones-contract /api-reference/external/openapi.yaml get /virtual-accounts/escrow/{escrow_id}/milestones/{milestone_id}/contract Returns the current contract record for the milestone. Exactly one contract exists per milestone, created implicitly on the first confirmation event. Requires the `escrow` feature. # Get an escrow project Source: https://docs.pcxpay.com/api-reference/escrow/get-an-escrow-project /api-reference/external/openapi.yaml get /virtual-accounts/escrow/{escrow_id} Fetches the full escrow entity, including its milestones, parties, contracts, and payment links. Org-scoped — the escrow must belong to the caller's organisation. Requires the `escrow` feature. # List escrow disbursements Source: https://docs.pcxpay.com/api-reference/escrow/list-escrow-disbursements /api-reference/external/openapi.yaml get /virtual-accounts/escrow/{escrow_id}/transactions Returns the escrow's disbursement history — the transaction records of type `escrow_disbursement` (one per party payout), newest first. Requires the `escrow` feature. # List escrow projects Source: https://docs.pcxpay.com/api-reference/escrow/list-escrow-projects /api-reference/external/openapi.yaml get /virtual-accounts/escrow Paginated list of escrow entities belonging to the caller's organisation. Requires the `escrow` feature to be enabled for the organisation. # Register parties Source: https://docs.pcxpay.com/api-reference/escrow/register-parties /api-reference/external/openapi.yaml post /virtual-accounts/escrow/{escrow_id}/parties Registers one or more parties (contractor, agency commission, or expense) on an escrow project. A party's payout participation and split are assigned per milestone via the milestone-parties endpoint. The body also accepts a single party object or a bare array instead of the `parties` wrapper. Requires the `escrow` feature. # Remove a party Source: https://docs.pcxpay.com/api-reference/escrow/remove-a-party /api-reference/external/openapi.yaml delete /virtual-accounts/escrow/{escrow_id}/parties/{party_id} Removes a party from the escrow project. Requires `org-admin` OrgMember role and the `escrow` feature. # Remove a party from a milestone Source: https://docs.pcxpay.com/api-reference/escrow/remove-a-party-from-a-milestone /api-reference/external/openapi.yaml delete /virtual-accounts/escrow/{escrow_id}/milestones/{milestone_id}/parties/{party_id} Removes a party's assignment (participation and split) from this milestone. The party's entity-level identity is kept — use `DELETE /virtual-accounts/escrow/{escrow_id}/parties/{party_id}` to remove it from the project entirely. Requires `org-admin` OrgMember role and the `escrow` feature. # Submit a contract confirmation Source: https://docs.pcxpay.com/api-reference/escrow/submit-a-contract-confirmation /api-reference/external/openapi.yaml post /virtual-accounts/escrow/{escrow_id}/milestones/{milestone_id}/contract Records a contract event for the milestone — exactly one action per call. Send `party_id` to record a contractor's completion confirmation (the contract record is created implicitly on the first event). Send `client_confirmed` to relay the client's final decision: `true` checks that every contractor assigned to the milestone has confirmed and, if complete, disburses the milestone to each party per its split — if confirmations are missing, the contract is rejected and flagged for PCX review; `false` rejects the contract immediately. `agency_notes` is only accepted alongside `client_confirmed`. Requires the `escrow` feature. # Update a milestone Source: https://docs.pcxpay.com/api-reference/escrow/update-a-milestone /api-reference/external/openapi.yaml put /virtual-accounts/escrow/{escrow_id}/milestones/{milestone_id} Updates a milestone's `label` or `expected_disbursement`. Requires `org-admin` OrgMember role and the `escrow` feature. # Update a party Source: https://docs.pcxpay.com/api-reference/escrow/update-a-party /api-reference/external/openapi.yaml put /virtual-accounts/escrow/{escrow_id}/parties/{party_id} Updates a single registered party — the `party_id` comes from the URL and the body carries the fields to change. Requires `org-admin` OrgMember role and the `escrow` feature. # Update an escrow project Source: https://docs.pcxpay.com/api-reference/escrow/update-an-escrow-project /api-reference/external/openapi.yaml put /virtual-accounts/escrow/{escrow_id} Updates the entity's metadata — `label`, `org_ref`, or `total_amount`. Requires `org-admin` OrgMember role and the `escrow` feature. # Update milestone party splits Source: https://docs.pcxpay.com/api-reference/escrow/update-milestone-party-splits /api-reference/external/openapi.yaml put /virtual-accounts/escrow/{escrow_id}/milestones/{milestone_id}/parties Updates one or more party splits on this milestone at once. The body also accepts a single update object or a bare array instead of the `parties` wrapper. Requires `org-admin` OrgMember role and the `escrow` feature. # Update parties Source: https://docs.pcxpay.com/api-reference/escrow/update-parties /api-reference/external/openapi.yaml put /virtual-accounts/escrow/{escrow_id}/parties Updates one or more registered parties at once — each item carries the `party_id` to change plus the fields to update. The body also accepts a single update object or a bare array instead of the `parties` wrapper. Requires `org-admin` OrgMember role and the `escrow` feature. # PCX API Reference Source: https://docs.pcxpay.com/api-reference/external/introduction Integration guide for direct API access ## Overview This reference covers every endpoint you need to integrate with PCX as an external organisation. It includes authentication, payments, transactions, virtual accounts, payment links, beneficiaries, and bank validation utilities. If you need access to any endpoint not listed here, contact [info@pcxpay.com](mailto:info@pcxpay.com). *** ## Authentication Every request requires two headers: | Header | Value | | - | - | | `X-Api-Key` | Your API key — the only supported credential | | `Authorization` | Always the literal string `NONE` | Both headers must always be present, even though `Authorization` never carries a credential. **Base URL:** `https://prod-api.pcxpay.com/v1` *** ## Getting your API key Use the [Create API key](/api-reference/external/introduction#authentication) endpoint to generate a key programmatically, or create one from your PCX dashboard. The full key value is shown **once at creation only** — store it securely. *** ## Pagination All list endpoints use cursor-based pagination. Pass the `last_evaluated_key` value from the previous response's `meta` block as a query parameter on the next request. When `last_evaluated_key` is `null`, you have reached the last page. *** ## Errors All errors follow a consistent shape: ```json theme={null} { "message": "A description of what went wrong", "status": "error" } ``` | Status code | Meaning | | - | - | | `400` | Invalid request — check your request body or parameters | | `401` | Missing or invalid credentials | | `403` | Insufficient role for this operation | | `404` | Resource not found | | `500` | Internal server error — contact support if this persists | # Browser callback after hosted checkout Source: https://docs.pcxpay.com/api-reference/payment-initiation/browser-callback-after-hosted-checkout /api-reference/external/openapi.yaml get /payments-init/callback Browser redirect endpoint that payment providers send the customer to after hosted checkout finishes. You do not call this endpoint yourself — PCX registers it with the provider during redirect checkout flows (`next_action: redirect`). It requires no authentication. PCX looks up the payment from the query parameters, then issues a `302` redirect to the `return_url` you supplied at initiation, appending `status` and `reference` query parameters. `status` is `completed` or `failed` where the provider outcome maps to a final state; otherwise the raw provider status is passed through. `reference` is your `client_reference` from initiation, or the first 10 characters of the payment ID when no `client_reference` was supplied. If no `return_url` was stored for the payment, or the stored URL is invalid, the customer is redirected to the PCX remittances page instead. # Initiate a crypto ramp Source: https://docs.pcxpay.com/api-reference/payment-initiation/initiate-a-crypto-ramp /api-reference/external/openapi.yaml post /payments-init/ramp Initiate an on-ramp (fiat → crypto) or off-ramp (crypto → fiat) conversion. PCX acts as the orchestrator — your users send or receive fiat, and PCX handles the crypto settlement automatically. **On-ramp**: the customer pays in fiat using their preferred payment method. Once the fiat collection is confirmed, the crypto is settled directly to the `wallet_address` you provide. No further action is needed after a successful initiation. **Off-ramp**: the customer sends crypto and receives fiat to a registered beneficiary's bank or mobile money account. You must fetch a fresh rate from `GET /organizations/admin/exchange-rate` immediately before presenting it to the user and before submitting this request. The rate must be fetched in the same direction the funds flow — `fromCurrency=fiat&toCurrency=crypto` for `on_ramp`, and `fromCurrency=crypto&toCurrency=fiat` for `off_ramp`. Use the crypto asset code itself (e.g. `USDC`), not its fiat equivalent. The `client_rate` you send is validated against the current live rate for that direction — if it has drifted more than 0.5% (or was fetched in the wrong direction), the request is rejected with `RATE_MISMATCH` and you must re-fetch. # Initiate a payment Source: https://docs.pcxpay.com/api-reference/payment-initiation/initiate-a-payment /api-reference/external/openapi.yaml post /payments-init/initiate Initiate a payin or payout. `org_id` and `user_id` are injected automatically from the authenticated API key, so you do not need to include them in the request body. Same-currency and cross-currency flows are both supported — for cross-currency flows, lock a rate first and pass its `org_rate_id`. # Initiate a payment (deprecated) Source: https://docs.pcxpay.com/api-reference/payment-initiation/initiate-a-payment-deprecated /api-reference/external/openapi.yaml post /payments-init/initiate/public-payin **Deprecated.** This endpoint has been consolidated into `POST /payments-init/initiate`, which accepts the same API-key authentication and also supports cross-currency flows. Use `/payments-init/initiate` instead. # List available payment networks Source: https://docs.pcxpay.com/api-reference/payment-initiation/list-available-payment-networks /api-reference/external/openapi.yaml post /externals/networks Returns the available payment networks (banks and mobile money operators) for a given country. The upstream provider is selected automatically based on the country code. Use the `code` value returned here as the `provider` field inside `mobile_money_details`, or as the `bank_code` for bank-transfer beneficiaries, when initiating a payment. # Submit a payment action Source: https://docs.pcxpay.com/api-reference/payment-initiation/submit-a-payment-action /api-reference/external/openapi.yaml post /payments-init/{payment_id}/action Continue a pending payment that is awaiting a customer action — for example when the initiation response returned `next_action: collect_otp`. Supported actions are `submit_otp`, `verify`, and `cancel`. The payment must be in `pending` status. # Confirm VA payment Source: https://docs.pcxpay.com/api-reference/payment-links/confirm-va-payment /api-reference/external/openapi.yaml post /payment-links/va/{link_ref}/confirm Payer confirms the payment. PCX processes the transfer into the virtual account. # Create a payment link Source: https://docs.pcxpay.com/api-reference/payment-links/create-a-payment-link /api-reference/external/openapi.yaml post /payment-links Creates a new payment link. Requires the `org-admin` or `staff` role. # Create a VA payment link Source: https://docs.pcxpay.com/api-reference/payment-links/create-a-va-payment-link /api-reference/external/openapi.yaml post /payment-links/va Creates a virtual account payment link for accepting deposits. # Get a payment link Source: https://docs.pcxpay.com/api-reference/payment-links/get-a-payment-link /api-reference/external/openapi.yaml get /payment-links/{link_id} # Initiate VA payment Source: https://docs.pcxpay.com/api-reference/payment-links/initiate-va-payment /api-reference/external/openapi.yaml post /payment-links/va/{link_ref}/initiate Payer provides their currency and amount. PCX returns a payment quote with the calculated exchange rate. # List org payment links Source: https://docs.pcxpay.com/api-reference/payment-links/list-org-payment-links /api-reference/external/openapi.yaml get /orgs/{org_id}/payment-links Returns all payment links for an organisation. Requires `org-admin` or `staff`. # Revoke a payment link Source: https://docs.pcxpay.com/api-reference/payment-links/revoke-a-payment-link /api-reference/external/openapi.yaml delete /payment-links/{link_id} Revokes a payment link. Optionally pass `email` in the body to revoke all active links for that recipient at once. # Send OTP Source: https://docs.pcxpay.com/api-reference/payment-links/send-otp /api-reference/external/openapi.yaml post /payment-links/otp Sends a one-time password to the customer's email. No authentication required. # Verify a payment link Source: https://docs.pcxpay.com/api-reference/payment-links/verify-a-payment-link /api-reference/external/openapi.yaml post /payment-links/verify Checks whether a link is valid (not expired or revoked). No authentication required. # Verify OTP Source: https://docs.pcxpay.com/api-reference/payment-links/verify-otp /api-reference/external/openapi.yaml put /payment-links/otp Verifies the OTP and returns a short-lived JWT (`type: payment_link`, 24-hour expiry) for accessing the protected payment link. # Create a payment Source: https://docs.pcxpay.com/api-reference/payments/create-a-payment /api-reference/external/openapi.yaml post /payments Initiates a new payment. # Get a payment Source: https://docs.pcxpay.com/api-reference/payments/get-a-payment /api-reference/external/openapi.yaml get /payments/{payment_id} # Get payments by transaction ID Source: https://docs.pcxpay.com/api-reference/payments/get-payments-by-transaction-id /api-reference/external/openapi.yaml get /payments/transaction/{transaction_id} Returns all payments associated with a given transaction ID. # List my payments Source: https://docs.pcxpay.com/api-reference/payments/list-my-payments /api-reference/external/openapi.yaml get /payments Returns a paginated list of payments for the authenticated user. # Update a payment Source: https://docs.pcxpay.com/api-reference/payments/update-a-payment /api-reference/external/openapi.yaml patch /payments/{payment_id} # Filter transactions Source: https://docs.pcxpay.com/api-reference/transactions/filter-transactions /api-reference/external/openapi.yaml post /transactions/filter Returns transactions matching the supplied criteria. # Get a transaction Source: https://docs.pcxpay.com/api-reference/transactions/get-a-transaction /api-reference/external/openapi.yaml get /transactions/{transaction_id} # Get transaction by payment ID Source: https://docs.pcxpay.com/api-reference/transactions/get-transaction-by-payment-id /api-reference/external/openapi.yaml get /transactions/payment/{payment_id} # List my transactions Source: https://docs.pcxpay.com/api-reference/transactions/list-my-transactions /api-reference/external/openapi.yaml get /transactions Returns a paginated list of transactions for the authenticated user. # Create fiat currency account Source: https://docs.pcxpay.com/api-reference/virtual-accounts/create-fiat-currency-account /api-reference/external/openapi.yaml post /virtual-accounts/currency Creates a fiat virtual account in any supported currency for an organisation. # Crypto deposit Source: https://docs.pcxpay.com/api-reference/virtual-accounts/crypto-deposit /api-reference/external/openapi.yaml post /virtual-accounts/fund/crypto Credits crypto funds to a virtual account. # Crypto withdrawal Source: https://docs.pcxpay.com/api-reference/virtual-accounts/crypto-withdrawal /api-reference/external/openapi.yaml post /virtual-accounts/withdraw/crypto Withdraws crypto funds from a virtual account to an external wallet address. # Get a VA transaction Source: https://docs.pcxpay.com/api-reference/virtual-accounts/get-a-va-transaction /api-reference/external/openapi.yaml get /virtual-accounts/transactions/{transaction_id} # Internal transfer Source: https://docs.pcxpay.com/api-reference/virtual-accounts/internal-transfer /api-reference/external/openapi.yaml post /virtual-accounts/transfer Moves funds between two virtual accounts on the platform. # Link external bank account Source: https://docs.pcxpay.com/api-reference/virtual-accounts/link-external-bank-account /api-reference/external/openapi.yaml post /virtual-accounts/link Links an external bank account as a withdrawal destination. # List account activity logs Source: https://docs.pcxpay.com/api-reference/virtual-accounts/list-account-activity-logs /api-reference/external/openapi.yaml get /virtual-accounts/logs Immutable activity log. Filter by org, account, event type, and date range. # List account transactions Source: https://docs.pcxpay.com/api-reference/virtual-accounts/list-account-transactions /api-reference/external/openapi.yaml get /virtual-accounts/{account_id}/transactions # List accounts by organisation Source: https://docs.pcxpay.com/api-reference/virtual-accounts/list-accounts-by-organisation /api-reference/external/openapi.yaml get /virtual-accounts/org/{org_id} # List accounts by user Source: https://docs.pcxpay.com/api-reference/virtual-accounts/list-accounts-by-user /api-reference/external/openapi.yaml get /virtual-accounts/user/{user_id} # List linked external accounts Source: https://docs.pcxpay.com/api-reference/virtual-accounts/list-linked-external-accounts /api-reference/external/openapi.yaml get /virtual-accounts/link # List org VA transactions Source: https://docs.pcxpay.com/api-reference/virtual-accounts/list-org-va-transactions /api-reference/external/openapi.yaml get /virtual-accounts/org/{org_id}/transactions Paginated list of all virtual account transactions across the organisation. # NGN bank account lookup Source: https://docs.pcxpay.com/api-reference/virtual-accounts/ngn-bank-account-lookup /api-reference/external/openapi.yaml post /virtual-accounts/ngn/account-lookup Verifies a destination NGN bank account before initiating a withdrawal. Returns the verified `account_name` for user confirmation. # Onboard a customer Source: https://docs.pcxpay.com/api-reference/virtual-accounts/onboard-a-customer /api-reference/external/openapi.yaml post /virtual-accounts/onboard Second step of customer onboarding. Submit the full customer profile once the customer has accepted the Terms of Service. # Org account registry Source: https://docs.pcxpay.com/api-reference/virtual-accounts/org-account-registry /api-reference/external/openapi.yaml get /virtual-accounts/org/{org_id}/registry Full registry of all virtual accounts for the organisation, with balances and status. # Org dashboard metrics Source: https://docs.pcxpay.com/api-reference/virtual-accounts/org-dashboard-metrics /api-reference/external/openapi.yaml get /virtual-accounts/org/{org_id}/dashboard/metrics # Org transaction summary Source: https://docs.pcxpay.com/api-reference/virtual-accounts/org-transaction-summary /api-reference/external/openapi.yaml get /virtual-accounts/org/{org_id}/transactions/summary Aggregated transaction totals by status and currency for the organisation. # Preview transaction fee Source: https://docs.pcxpay.com/api-reference/virtual-accounts/preview-transaction-fee /api-reference/external/openapi.yaml post /virtual-accounts/fees/preview Calculates the fee for a specific transaction type before executing it. Useful for showing users their total cost in pre-confirmation UIs. # Submit customer KYC Source: https://docs.pcxpay.com/api-reference/virtual-accounts/submit-customer-kyc /api-reference/external/openapi.yaml post /virtual-accounts/kyc First step of customer onboarding. Provide the customer's email and type to receive a `customer_id` and a Terms of Service link they must accept before their account is created. # Unlink external bank account Source: https://docs.pcxpay.com/api-reference/virtual-accounts/unlink-external-bank-account /api-reference/external/openapi.yaml delete /virtual-accounts/link # Withdraw funds Source: https://docs.pcxpay.com/api-reference/virtual-accounts/withdraw-funds /api-reference/external/openapi.yaml post /virtual-accounts/withdraw Initiates a withdrawal from a virtual account. NGN withdrawals are placed in `pending` status and require an admin approval step before the bank transfer fires. # Get webhook configuration Source: https://docs.pcxpay.com/api-reference/webhooks/get-webhook-configuration /api-reference/external/openapi.yaml get /public/webhooks Returns your organisation's current webhook configuration — the endpoint URL and the list of subscribed event types. The signing secret is not returned; it is only shown once at registration. # List subscribable event types Source: https://docs.pcxpay.com/api-reference/webhooks/list-subscribable-event-types /api-reference/external/openapi.yaml get /public/webhooks/events/catalogue Returns every event type you can subscribe a webhook to. # Notify transaction update Source: https://docs.pcxpay.com/api-reference/webhooks/notify-transaction-update /api-reference/external/openapi.yaml post /public/webhooks/notify Manually dispatches a transaction update notification to all registered webhook endpoints for the specified organisation. # Register webhook Source: https://docs.pcxpay.com/api-reference/webhooks/register-webhook /api-reference/external/openapi.yaml post /public/webhooks Registers an HTTPS endpoint to receive event notifications. PCX will POST events to this URL as they occur. The organisation identity is derived from your API key. The response includes the `secret_key` used to sign every delivery — it is returned only once, so store it securely. # Send test event Source: https://docs.pcxpay.com/api-reference/webhooks/send-test-event /api-reference/external/openapi.yaml post /public/webhooks/test Fires a synthetic event at your registered endpoint so you can verify signature validation and processing end to end. The test delivery is signed with your real webhook secret. # Update event subscriptions Source: https://docs.pcxpay.com/api-reference/webhooks/update-event-subscriptions /api-reference/external/openapi.yaml put /public/webhooks/events Replaces the list of event types your webhook is subscribed to. Send an empty array (or omit `events`) to subscribe to all event types. Use `GET /public/webhooks/events/catalogue` for the list of valid types. # Authentication Source: https://docs.pcxpay.com/guides/essentials/authentication How to authenticate with the PCX API using an API key ## Overview PCX authenticates server-to-server requests using **API keys**. **Both headers must always be present.** Send your API key in `X-Api-Key`. The `Authorization` header must always be sent with the literal string value `NONE`. | Header | Required value | | - | - | | `X-Api-Key` | `` | | `Authorization` | `NONE` (literal string) | > [!WARNING] > > Sending only one header, or omitting the unused header entirely, will result in a `401 Unauthorized` response. *** ## API key authentication Send your API key in `X-Api-Key` and set `Authorization` to the literal string `NONE`. ```bash theme={null} curl https://prod-api.pcxpay.com/v1/organizations/{org_id} \ -H "X-Api-Key: your_api_key_here" \ -H "Authorization: NONE" ``` Generate keys from **Dashboard → Settings → API Keys**. > [!WARNING] > > Never expose API keys in client-side code or public repositories. Rotate immediately if compromised. *** ## Errors | Status | Meaning | | - | - | | `401` | Missing, invalid, or incomplete headers | | `403` | Valid credentials but insufficient permissions | ```json theme={null} { "status": "error", "message": "Unauthorized" } ``` # Errors Source: https://docs.pcxpay.com/guides/essentials/errors Standard error responses, HTTP status codes, decimal handling, and idempotency across all PCX services ## Error response format All PCX services return errors in a consistent JSON envelope: ```json theme={null} { "status": "error", "message": "Human-readable description of what went wrong" } ``` Some services include an additional `error` field with a technical detail string (present mainly on 500 responses): ```json theme={null} { "status": "error", "message": "An internal error occurred", "error": "NullPointerException at use_case line 42" } ``` *** ## HTTP status codes | Code | Meaning | When it occurs | | - | - | - | | `200` | OK | Successful GET, PUT, PATCH, DELETE | | `201` | Created | Successful POST that creates a resource | | `204` | No Content | Successful DELETE with no response body | | `207` | Multi-Status | Partial success (e.g. `target_type: both` notifications where one fails) | | `400` | Bad Request | Missing required fields, invalid values, validation errors | | `401` | Unauthorized | Missing, malformed, or expired auth credentials | | `403` | Forbidden | Valid credentials but insufficient role/permission | | `404` | Not Found | Resource does not exist | | `409` | Conflict | Operation conflicts with current state (e.g. duplicate invite, deletion conflict) | | `500` | Internal Server Error | Unhandled exception in the service | *** ## Success response format Successful responses follow one of two shapes depending on the service: **Standard envelope (most services):** ```json theme={null} { "status": "success", "message": "Operation description", "data": { ... } } ``` **Direct object (some older endpoints):** ```json theme={null} { "user_id": "...", "email": "...", ... } ``` *** ## Decimal handling Monetary values are stored as DynamoDB `Decimal` types and serialized to JSON as floats. Always treat amounts as `number` in your client — never as integers. # Pagination Source: https://docs.pcxpay.com/guides/essentials/pagination Cursor-based pagination across all PCX services ## Pagination PCX uses **cursor-based pagination** throughout. There is no offset-based (`page=2`) pagination. ### How it works 1. Make your first request — optionally pass `limit` to control page size 2. If there are more results, the response includes a `last_evaluated_key` (or `lastEvaluatedKey`) field 3. Pass that value back as a query parameter on your next request to get the next page 4. When `last_evaluated_key` is `null` (or absent), you have reached the last page ### Request ```bash theme={null} GET /v1/users/list?limit=20 GET /v1/users/list?limit=20&last_evaluated_key= ``` ### Response ```json theme={null} { "status": "success", "data": [ ... ], "count": 20, "last_evaluated_key": "eyJwa...", "meta": { "last_evaluated_key": "eyJwa..." } } ``` > [!WARNING] > > Pagination cursors are opaque tokens. Do not attempt to parse, modify, or construct them manually — they are base64-encoded and will cause a `400` if malformed. ### Key naming inconsistency Different services use slightly different field names for the pagination cursor — this is a known inconsistency: | Service | Request param | Response field | | - | - | - | | Organizations | `lastEvaluatedKey` | `lastEvaluatedKey` | | Users | `last_evaluated_key` | `last_evaluated_key` | | Virtual Accounts | `last_evaluated_key` | `last_evaluated_key` | | Beneficiary | `last_evaluated_key` | `last_evaluated_key` | | Transaction | `last_evaluated_key` | `last_evaluated_key` | # Webhooks Source: https://docs.pcxpay.com/guides/essentials/webhooks Receive real-time payment and platform events at your HTTPS endpoint Webhooks let PCX push event notifications to your server the moment something changes — a payment settles, a virtual account receives a deposit, or a transfer completes. This eliminates the need to poll for status updates. *** ## Register your endpoint Add your webhook URL to your organization using the Public service: ```bash theme={null} curl -X POST https://prod-api.pcxpay.com/v1/public/webhooks \ -H "X-Api-Key: your_api_key" \ -H "Authorization: NONE" \ -H "Content-Type: application/json" \ -d '{ "endpoint_url": "https://yourapp.com/webhooks/pcx" }' ``` The response includes a `secret_key`. Store it securely — PCX uses it to sign every delivery, and it is returned only once at registration. Your endpoint must be reachable over HTTPS. PCX starts delivering events immediately after registration. By default you receive all event types. To subscribe to specific events only, pass an `events` array at registration or replace the list later with `PUT /public/webhooks/events`. Fetch the full list of subscribable types from `GET /public/webhooks/events/catalogue`. *** ## Event payload structure Every event follows the same envelope: ```json theme={null} { "event_id": "9f0a1c2e-...", "event_type": "payment.settled", "event_category": "payment", "event_object_id": "ea5965e4-1432-40ef-bccf-cb51455a935a", "event_object_status": "completed", "occurred_at": "2026-09-04T10:00:00+00:00", "event_object": { ... }, "metadata": { ... } } ``` * `event_object` carries the object the event is about; `event_object_id` and `event_object_status` lift its identifier and status to the envelope. * `occurred_at` is when the event happened; a `timestamp` field records when the delivery was sent. * A legacy `data` field duplicates `event_object` for existing integrations. Use `event_id` to deduplicate — it stays the same across retries, and PCX may deliver the same event more than once. Process each `event_id` exactly once. *** ## Verify the signature PCX signs every delivery with HMAC-SHA256 using your `secret_key`. Two signature headers are sent: | Header | Contents | | - | - | | `X-Webhook-Signature` | `sha256=` + HMAC-SHA256 of the raw request body | | `X-Webhook-Signature-V2` | `sha256=` + HMAC-SHA256 of `.`, where `` is the value of `X-Webhook-Timestamp` (Unix seconds) | Deliveries also include `X-Event-Type` and `X-Event-ID` headers. Prefer the v2 signature — including the timestamp protects against replay. The timestamp is set per delivery attempt, so also reject timestamps older than a window you choose (for example five minutes). Always compute the HMAC over the exact raw bytes of the request body — do not re-serialize the parsed JSON. ```python theme={null} import hmac import hashlib def is_valid_v2(raw_body: bytes, timestamp: str, signature_header: str, secret: str) -> bool: message = f"{timestamp}.".encode() + raw_body expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header) ``` > [!WARNING] > > Reject any webhook event where the signature does not match. Never process unverified payloads — they may be spoofed. *** ## Acknowledge receipt Return a `2xx` HTTP status within **30 seconds** (the default delivery timeout). PCX does not inspect the response body — any `2xx` counts as a successful acknowledgement. If your processing logic takes longer, acknowledge immediately and handle the event asynchronously: ```python theme={null} @app.post("/webhooks/pcx") async def handle_webhook(request): body = await request.body() is_valid_v2(body, request.headers["X-Webhook-Timestamp"], request.headers["X-Webhook-Signature-V2"], WEBHOOK_SECRET) background_tasks.add_task(process_event, body) return {"ok": True} # Return 200 immediately ``` *** ## Retry schedule PCX attempts delivery up to 3 times in quick succession. If all fail, the event is retried on a backoff schedule: | Retry | Delay after previous attempt | | - | - | | 1 | 1 minute | | 2 | 5 minutes | | 3 | 15 minutes | | 4 | 1 hour | | 5 | 6 hours | | 6 | 24 hours | | 7 | 48 hours | Events still failing after 11 total attempts are marked permanently failed. Contact support to redeliver a permanently failed event. *** ## Event types | Event | When it fires | | - | - | | `payment.initiated` | Payment created and accepted for processing | | `payment.settled` | Payment successfully settled to the beneficiary | | `payment.failed` | Payment rejected or failed at the provider | | `virtual_account.created` | Virtual account provisioned | | `virtual_account.deposit_received` | Funds credited to a virtual account | | `virtual_account.transfer_initiated` | Transfer from a virtual account started | | `virtual_account.transfer_completed` | Transfer from a virtual account completed | | `virtual_account.transfer_failed` | Transfer from a virtual account failed | | `virtual_account.transfer_refunded` | Transfer refunded to the virtual account | | `virtual_account.transfer_cancelled` | Transfer cancelled before completion | See [Webhooks](/guides/concepts/webhooks) in Concepts for the payment event payload schema. *** ## Test your integration Fire a synthetic event at your registered endpoint — signed with your real secret — to verify signature validation and processing end to end: ```bash theme={null} curl -X POST https://prod-api.pcxpay.com/v1/public/webhooks/test \ -H "X-Api-Key: your_api_key" \ -H "Authorization: NONE" \ -H "Content-Type: application/json" \ -d '{ "event_type": "payment.settled" }' ``` `event_type` defaults to `virtual_account.deposit_received` when omitted. The response reports whether your endpoint acknowledged the delivery. # Error Handling Source: https://docs.pcxpay.com/guides/h2h/error-handling Common errors and how to handle them ## Error response format All error responses follow this structure: ```json theme={null} { "success": false, "error": "Error description", "details": "Additional error context" } ``` *** ## Error codes | Code | Description | Resolution | | - | - | - | | `INVALID_AMOUNT` | Amount validation failed | Ensure amount is greater than 0 | | `INVALID_CURRENCY` | Unsupported currency | Use GBP, EUR, or USD | | `PAYMENT_LINK_EXPIRED` | Link has expired | Create a new payment request | | `PROVIDER_ERROR` | Payment processing error | Retry the request or contact support | *** ## Best practices Never surface raw error responses to end users — map error codes to user-friendly messages in your application. For general API error codes and HTTP status meanings, see [Errors](/guides/essentials/errors). For persistent errors, contact [info@pcxpay.com](mailto:info@pcxpay.com). # Initiating a Payment Source: https://docs.pcxpay.com/guides/h2h/initiating-a-payment How to create and share a payment link via the PCX API ## Endpoint ``` POST https://prod-api.pcxpay.com/v1/payments-init/initiate ``` Both auth headers are always required. Set whichever you are not using to the literal string `NONE`. | Header | Value | | - | - | | `X-Api-Key` | Your API key | | `Authorization` | `NONE` (for API key flows) | *** ## Request The `pay_by_link: true` flag in the `metadata` object is required for H2H. Without it the request defaults to iFrame integration. ```json theme={null} { "amount": 100.00, "target_amount": 100.00, "target_currency": "GBP", "currency": "GBP", "country": "GB", "payment_method": "bank_transfer", "org_id": "your-org-id", "payer_details": { "email": "customer@example.com", "name": "John Doe" }, "metadata": { "beneficiary_name": "Recipient Name", "user_id": "user-uuid", "transaction_type": "bank", "reason": "Payment for services", "pay_by_link": true } } ``` *** ## Response ```json theme={null} { "success": true, "status": "initiated", "next_action": "pay_by_link", "payment_link": "https://pay.pcxpay.com/pay/abc123xyz...", "payment_intent_id": "pi_xyz789", "expiry_time": "2024-01-15T12:00:00Z", "provider_payment_id": "pay_abc123", "message": "Payment link created. Share link with customer to complete payment." } ``` ### Response fields | Field | Type | Description | | - | - | - | | `success` | boolean | Whether the request was successful | | `status` | string | Payment status — always `initiated` on creation | | `next_action` | string | Always `pay_by_link` for H2H integration | | `payment_link` | string | Direct payment link to share with the customer | | `payment_intent_id` | string | Unique identifier for this payment intent — store this for tracking | | `expiry_time` | string | ISO 8601 timestamp when the link expires (typically 24 hours) | | `provider_payment_id` | string | PCX payment identifier | | `message` | string | Human-readable status message | *** ## Sharing the link Once you receive `payment_link`, deliver it to the customer via SMS, email, in-app message, or push notification. * **Expiry** — the link expires after 24 hours. Check `expiry_time` in the response. If it expires, create a new payment request. * **Tracking** — store `payment_intent_id` against the order in your system for status tracking. * **Confirmation** — do not rely on the customer returning to your app to confirm payment. Always use webhooks for confirmation. See [Payment Status & Webhooks](/guides/h2h/payment-status). * **Expired links** — if the customer tries to pay after expiry, surface a clear message and allow them to request a new link. # Host to Host (H2H) Source: https://docs.pcxpay.com/guides/h2h/overview Direct payment link integration for bank transfer payments via PCX H2H integration returns a direct payment link that can be shared with the customer via SMS, email, or embedded in your application. The customer opens the link and completes the payment at their bank. No hosted UI redirect is required on your side. Use H2H when you want full control over how and where the payment link is delivered — mobile apps, SMS campaigns, email flows, or any channel where a redirect is not practical. *** ## How it works ``` 1. Your server calls POST /v1/payments-init/initiate with pay_by_link: true in metadata 2. PCX returns a payment_link URL 3. You deliver the link to the customer via your chosen channel (SMS, email, push, in-app) 4. The customer opens the link and completes the payment at their bank 5. PCX sends a webhook to your server confirming the payment status ``` *** ## When to use H2H | Scenario | Recommended integration | | - | - | | Mobile app with custom payment flow | H2H | | SMS or email payment campaigns | H2H | | Channels where a browser redirect is impractical | H2H | | Embedded hosted payment UI | [iFrame Integration](/guides/iframe/overview) | *** ## Integration steps **1. Initiate the payment** Call the PCX payment initiation endpoint with `pay_by_link: true` in the metadata. PCX returns a `payment_link` URL. See [Initiating a Payment](/guides/h2h/initiating-a-payment). **2. Deliver the link** Send the `payment_link` to the customer via SMS, email, push notification, or in-app message. The link expires after 24 hours. **3. Receive confirmation** PCX sends a server-to-server webhook to your registered endpoint when the payment status changes. See [Payment Status & Webhooks](/guides/h2h/payment-status). # Payment Status and Webhooks Source: https://docs.pcxpay.com/guides/h2h/payment-status How to track payment status and receive real-time notifications ## Status lifecycle ``` initiated → pending → processing → completed ↘ failed ``` | Status | Description | | - | - | | `initiated` | Payment request created, awaiting customer action | | `pending` | Customer has opened the link and started the payment | | `processing` | Payment is being processed by the bank | | `completed` | Payment successfully settled | | `failed` | Payment failed or was cancelled | *** ## Checking status manually Do not poll for status — use webhooks to receive updates in real time. If you need to retrieve the current status of a payment: ``` GET /payments/{payment_id} GET /payments/transaction/{transaction_id} ``` *** ## Webhooks > [!NOTE] > > To receive webhook notifications, send your webhook endpoint URL to [info@pcxpay.com](mailto:info@pcxpay.com). Our team will register it on your account before you go live. PCX sends a `POST` request to your registered webhook endpoint on every payment status change. Register your webhook endpoint in the PCX Dashboard before going live. ### Events | Event | Description | | - | - | | `payment.initiated` | Payment created and accepted for processing | | `payment.settled` | Payment successfully settled | | `payment.failed` | Payment failed or cancelled | ### Example payload ```json theme={null} { "event_id": "9f0a1c2e-...", "event_type": "payment.settled", "event_category": "payment", "event_object_id": "pay_abc123", "event_object_status": "completed", "occurred_at": "2026-09-04T10:30:00+00:00", "event_object": { "reference": "pay_abc123", "transaction_id": "txn_xyz789", "narration": "Payment", "source": { "amount": { "value": "100.00", "currency": "GBP" } }, "destination": { "amount": { "value": "100.00", "currency": "GBP" }, "method": "transfer", "beneficiary": { "name": "Recipient Name" } } }, "metadata": {} } ``` ### Webhook requirements * **Validate signatures** — always verify webhook signatures before processing events. See [Webhooks](/guides/essentials/webhooks). * **Respond quickly** — your endpoint must return a `2xx` status within 30 seconds. * **Idempotency** — PCX retries failed deliveries. Implement idempotency using `event_id` to avoid processing the same event twice. # Embedding Source: https://docs.pcxpay.com/guides/iframe/embedding Render the PCX-hosted payment form inside your page using a standard HTML iFrame The PCX payment UI is a hosted web application served from `remittances.pcxpay.com`. You embed it by rendering a standard ` ``` Replace `REDIRECT_URL_FROM_INITIATE_RESPONSE` with the `response.redirect_url` value from the [payment initiation response](/guides/iframe/session-initialisation). Do not modify the URL or append additional query parameters. *** ## React example ```jsx theme={null} function PaymentIframe({ redirectUrl }) { return ( ``` Do not add `sandbox` attributes to the iFrame. PCX's payment form requires access to browser features (redirects, cookie handling) that sandbox restrictions would block. *** ## Rendering without a full-page reload You can show or hide the frame dynamically without reloading the page: ```javascript theme={null} // Show the payment form after receiving the redirect_url from your server function showPaymentForm(redirectUrl) { const frame = document.getElementById("pcxpay-payment"); frame.src = redirectUrl; frame.style.display = "block"; } // Hide after receiving payment confirmation via webhook function hidePaymentForm() { const frame = document.getElementById("pcxpay-payment"); frame.style.display = "none"; } ``` # iFrame Integration Source: https://docs.pcxpay.com/guides/iframe/overview Embed a PCX-hosted payment UI into your platform using a redirect URL — no custom payment UI required PCX iFrame integration lets you embed a fully hosted payment form directly inside your web application. Your server initiates a payment via the PCX API and receives a `redirect_url` that you embed in an `