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/paymentsApp key
FieldTypeRequired
amountinteger, minor units, ≥ 100yes
msisdnstring, MTN or Airtel RWyes
currencystring, RWFno
descriptionstringno
referencestringno
metadataobjectno

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

Create a payout (cash out)

POST/v1/payoutsApp key

Same body as a payment. Requires an approved merchant, a live app, and an available balance covering amount plus fee. Returns 201; 409 when the balance will not cover it. Full page.

Refund a payment

POST/v1/payments/:id/refundApp key
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/:idApp key

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/paymentsApp key
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/balanceApp key

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

Apps

GET/v1/appsAccount key
POST/v1/appsAccount key

Account-scoped: these are the only endpoints an account key can reach, and it cannot move money on any of them. Creating an app takes a name; the new app is created in the key’s own mode. A test key that could mint a live app would be a privilege escalation dressed as a convenience.

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: 250788924941
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