xPayStart integrating

Developers

Integrate once. Never pick a rail.

You call one API. xPay decides between the customer’s bank mandate and their card, per payment, and puts the instrument it used on the transaction record so your reconciliation has an answer.

# One call. No instrument named — that is the point.
POST /v1/charges
Authorization: Bearer xpb_test_…
Idempotency-Key: order_88213

{
  "customer_id": "cus_8fd21a",
  "amount_kobo": 4500000,
  "narration": "Order 88213"
}

Amounts are in kobo, always — an integer, never a decimal. An API that accepts 45000.00 accepts 45000.001 too, and somebody eventually sends it.

The idempotency key is a header so a retry can set it without rewriting the body. Retrying with the same key is safe and returns the original charge; inventing a new one is how a customer gets charged twice.

Integration methods

Five ways in, all against one API.

API integrationServer to server, for custom checkout and platform flows.
Hosted payment pagesxPay-hosted checkout with minimal work on your side.
Pay-by-linkLinks generated per invoice, for remote payment.
QR terminalsStatic or dynamic codes for a physical point of sale.
Recurring collections APIPlans, schedules and event-driven collection.

Every endpoint, field and error is in the API reference, generated from the service’s own spec rather than written alongside it.

Credentials and environments

Two keys, two environments, and no crossing over.

Public key

Identifies you and initialises client-side or hosted checkout. Safe to embed in front-end code — it is an identifier, not a secret.

Private key

Server-to-server authentication and request signing. It lives on your backend, is never sent to a client and is never logged in plaintext. Recurring collections can only be initiated with it.

Base URL

Per environment. Sandbox and production keys are not interchangeable: each works only against its own URL, and a key pointed at the wrong one fails in a way that looks like a bug in your code.

You never receive or handle our provider’s credentials. Keys can be rotated or revoked from the dashboard, and every key and instrument action is audit-logged.

Webhooks

One event says accepted. A different one says paid.

This is the distinction the whole integration turns on, and getting it wrong is the most common way a merchant ships a bug that only appears under load.

Authorisation

The customer approved it

Arrives immediately. It means the charge was accepted and is on its way. It is not money. Show the customer a confirmation, keep the order pending.

Settlement

The money arrived

Arrives when the charge resolves — seconds on a card, longer on a mandate. This is the one to release goods on.

An unrecognised status is pending, never failed

If you build your own state machine over ours, carry this rule into it. A status you have not seen before, a timeout and a dropped connection all mean the same thing — you could not ask — and none of them means the bank said no. Treating an unknown as a failure is how an order gets cancelled on money that actually moved, and how the retry takes it twice. Verify every delivery’s signature against the raw bytes you received, before parsing.

Sandbox

Every failure, on demand.

The sandbox is a shipped surface you certify against, not a test double we keep to ourselves. It is fully isolated and moves no real funds, and it can produce every outcome on both rails — including the ones you would otherwise first meet in production, at the worst possible moment.

Request sandbox credentials
  • Card enrolment and tokenisation
  • Card decline, by reason
  • Card expiry
  • Mandate enrolment
  • Pending-to-active transition
  • Activation delay
  • Unsupported-bank rejection
  • Collection failure, by reason
  • Card fallback
  • Mandate revocation
  • Dispute opening and resolution
  • Refund