API reference

Every endpoint, every parameter. Short by design, because the why is on the pages either side of this one.

Conventions

Base URLhttps://api.xendlypay.com
AuthAuthorization: Bearer <key> or X-Api-Key
Content typeapplication/json, both directions
AmountsIntegers in minor units. RWF has no cents.
TimestampsRFC 3339, UTC
IdsUUIDs

Successes are wrapped:

{
  "success": true,
  "payload": { "id": "0f8c2b7e…", "status": "pending", "amount": 5000 },
  "timestamp": "2026-09-12T09:14:02.000Z"
}

Errors are not. They key on status instead. See errors and limits.

Create a payment (cash in)

POST/v1/payments
FieldTypeRequired
amountinteger, minor units, ≥ 100yes
msisdnstring, MTN or Airtel RWyes
currencystring, RWFno
descriptionstringno
referencestringno
metadataobjectno
method"prompt" (default) or "dial"no

With "method": "dial" the customer dials a short code instead of approving a prompt, and the response carries pay_with (what to dial, and a QR code of it in pay_with.qr). Pay by dialing.

Header: Idempotency-Key, optional but strongly advised. Returns 201 with a pending transaction. Full page.

Create a payout (cash out)

POST/v1/payouts

Same body as a payment. Requires an approved merchant, a live app, and an available balance covering amount plus fee. The amount must be at least 10,000 RWF; the networks do not send less. Returns 201; 409 when the balance will not cover it. Full page.

Refund a payment

POST/v1/payments/:id/refund
FieldTypeRequired
amountinteger, omit to refund in fullno
reasonstring, up to 64 charactersno

Only a succeeded cash in can be refunded, and only once. Returns 201 with a new payout carrying reverses_payment_id. Full page.

Retrieve a payment

GET/v1/payments/:id

Scoped to the key’s app: a payment belonging to another app answers 404, not 403. Authoritative and permanent, so it is safe to use for reconciliation.

List payments

GET/v1/payments
QueryValuesNotes
statuspending, succeeded, failed, cancelled, allOmit or all for no filter
kindCASHIN, CASHOUTOr direction=collect|payout
networkmtn, airtelRouted from the number, not chosen
msisdnany formatNormalised before matching, so 078… finds 250 78…
fromYYYY-MM-DDInclusive
toYYYY-MM-DDInclusive of the whole day
limit1–100, default 50. Anything outside that becomes 50.
offsetinteger
{
  "success": true,
  "payload": {
    "payments": [ … ],
    "total": 214
  },
  "timestamp": "2026-09-12T09:14:02.000Z"
}

app_id and mode are not yours to set here

An app key always sees its own app, in its own mode, whatever the query string says. Otherwise ?app_id= would be an access-control decision made by the caller.

Balance

GET/v1/balance

Optional ?currency=RWF. Returns pending and available for the key’s mode. Full page.

The payment object

FieldTypeNotes
iduuidOurs. Store it against your order.
kindstringAlias for direction: CASHIN or CASHOUT
directionstringcollect | payout
statusstringpending | succeeded | failed | cancelled
amountintegerMinor units, what the customer paid or received
feeintegerOur cut, at the rate that applied when it happened
currencystringRWF
msisdnstringCountry-coded: 250781234567
networkstringmtn | airtel | unknown
modestringtest | live
referencestring | nullYours
metadataobject | nullYours, untouched
failure_codestring | nullThe gateway's code, when it gave one
failure_messagestring | nullReadable reason
initiated_bystringapi | dashboard: who asked
reverses_payment_iduuid | nullSet on a refund
created_attimestamp
completed_attimestamp | nullWhen it reached a final status
settled_attimestamp | nullWhen it became withdrawable