Schemas
Every object the API sends, field by field. Times are RFC 3339 in UTC; amounts are whole numbers of the currency's smallest unit (RWF has none, so 5000 is RWF 5,000).
Envelopes
Every successful answer is wrapped the same way; the object you asked for is in payload.
{
"success": true,
"payload": { … },
"timestamp": "2026-09-12T09:14:02.000Z"
}{
"status": "error",
"message": "amount must be a positive whole number of minor units",
"timestamp": "2026-09-12T09:14:02.000Z"
}Check the HTTP status first, then message. Errors and limits lists them.
Payment
One movement of money: a collection, a payout or a refund. Returned by POST /v1/payments, POST /v1/payouts, POST /v1/payments/:id/refund, GET /v1/payments/:id, and inside every payment.* and payout.* webhook.
| Field | Type | Notes |
|---|---|---|
id | uuid | Ours. Store it against your order. |
merchant_id | uuid | Your business. |
app_id | uuid | The app whose key made it. |
mode | string | test or live, from the app. |
direction | string | collect (money in) or payout (money out). A refund is a payout. |
kind | string | CASHIN or CASHOUT: the same as direction, for those who prefer it. |
amount | integer | What the customer paid or received. |
currency | string | RWF |
fee | integer | Our fee on this payment, at the rate that applied when it happened. |
msisdn | string | The customer's number, with the country code: 250781234567. |
network | string | mtn, airtel or unknown. |
description | string | Yours. Shown to the customer on the prompt where the network allows. |
reference | string | null | Yours: your order number. Comes back in webhooks. |
metadata | object | null | Yours, returned untouched. |
status | string | pending, succeeded, failed or cancelled. |
failure_code | string | null | Why it failed, as a code, when the network gave one. |
failure_message | string | null | Why it failed, in words you can show the customer. |
xendly_reference | string | Our reference to quote to us, e.g. XP-9F1C-2D3E-4F5A. |
initiated_by | string | api or dashboard. |
reverses_payment_id | uuid | null | On a refund: the collection it gives back. |
created_at | timestamp | |
updated_at | timestamp | |
completed_at | timestamp | null | When it reached a final status. |
settled_at | timestamp | null | When a collection became withdrawable. Null while it matures. |
{
"id": "0f8c2b7e-1a45-4c31-9d02-6e8f1b2c3d44",
"merchant_id": "7c1e…",
"app_id": "a3f1…",
"mode": "live",
"direction": "collect",
"kind": "CASHIN",
"amount": 5000,
"currency": "RWF",
"fee": 195,
"msisdn": "250781234567",
"network": "mtn",
"description": "Order #1043",
"reference": "1043",
"metadata": { "order_id": "1043" },
"status": "succeeded",
"failure_code": null,
"failure_message": null,
"xendly_reference": "XP-9F1C-2D3E-4F5A",
"initiated_by": "api",
"reverses_payment_id": null,
"created_at": "2026-09-12T09:14:02Z",
"updated_at": "2026-09-12T09:14:37Z",
"completed_at": "2026-09-12T09:14:37Z",
"settled_at": null
}Payment list
GET /v1/payments answers a page of payments and how many match in all.
| Field | Type | Notes |
|---|---|---|
payments | Payment[] | Newest first. |
total | integer | How many payments match the filters, across every page. |
Checkout session
POST /v1/checkouts answers this. Open url (or pass token to the widget) to let the customer pay.
| Field | Type | Notes |
|---|---|---|
id | uuid | The checkout. |
token | string | What the widget opens. Single use, not a secret worth guarding: it can only pay this checkout. |
url | string | The hosted payment page for this checkout. |
amount | integer | |
currency | string | RWF |
mode | string | test or live. |
status | string | open when created. |
expires_at | timestamp | Thirty minutes after creation. |
Checkout
Inside every checkout.* webhook: the session once nothing more can happen to it.
| Field | Type | Notes |
|---|---|---|
id | uuid | |
merchant_id, app_id | uuid | |
mode | string | test or live. |
amount, currency | integer, string | |
description | string | |
reference | string | null | Yours. |
metadata | object | null | Yours. |
msisdn | string | null | The number it was paid from, or fixed to. |
success_url, cancel_url | string | null | Where the page sent the customer. |
status | string | open, paid, failed, cancelled or expired. |
attempts | integer | Payment attempts made; at most 3. |
payment_id | uuid | null | The payment that paid it. |
expires_at | timestamp | |
created_at | timestamp |
Balance
GET /v1/balance, for the app's mode.
| Field | Type | Notes |
|---|---|---|
currency | string | RWF |
mode | string | test or live. |
pending | integer | Collected but still maturing. |
available | integer | Yours to withdraw now. |
{ "currency": "RWF", "mode": "live", "pending": 24500, "available": 180250 }Webhook event
What we POST to your webhook URL, signed in X-Xendly-Signature (see Webhooks).
| Field | Type | Notes |
|---|---|---|
type | string | What happened (below). |
data | Payment | Checkout | The object as it is now. |
| type | data | When |
|---|---|---|
payment.succeeded | Payment | A collection went through. |
payment.failed | Payment | It did not: declined, wrong PIN, no funds, timed out. |
payment.cancelled | Payment | The customer or the network cancelled it. |
payout.succeeded | Payment | A payout or a refund reached the customer. |
payout.failed | Payment | A payout or refund did not go through; the money stays in your balance. |
payout.cancelled | Payment | A payout or refund was cancelled. |
checkout.paid | Checkout | A checkout was paid. |
checkout.failed | Checkout | Every attempt failed. |
checkout.cancelled | Checkout | The customer gave up. |
checkout.expired | Checkout | Thirty minutes passed unpaid. |
payment.* fires per attempt
payment.failed inside one is not the end. To decide an order's fate, listen for checkout.*, which fires once.