Errors and limits

One envelope, honest status codes, and a clear line between “fix your request” and “try again in a moment”.

The shape

Errors key on status, where successes key on success. That asymmetry is not elegant and it is load-bearing: every Xendly service answers this way and clients switch on it.

{
  "status": "error",
  "message": "amount must be a positive whole number of minor units",
  "timestamp": "2026-09-12T09:14:02.000Z"
}

message is written for a human reading a log, and it says what was actually wrong rather than “invalid request”. Some errors add a details object; none of them leak anything internal.

The statuses

StatusMeansRetry?
400Your request is wrong: amount below 100 RWF, a number that is not MTN or Airtel, a body that will not parse, a refund larger than its payment.No. It will be wrong next time too.
401The key is missing, malformed, unknown, revoked, or its mode does not match its record. All five say the same thing on purpose.No. Fix the key.
403The key was fine, the action was not: an account key on a money endpoint, a live key on an unverified business, a disabled app, a suspended account.No, until the underlying condition changes.
404No such transaction, or one belonging to a different app. We do not distinguish the two.No.
409A real conflict: insufficient balance, or a payment already refunded.Only after something changes: a top-up, a settlement.
429Over your per-minute budget, or the gateway's.Yes, after Retry-After.
502The gateway failed in a way we could not interpret.Yes, carefully, with the same idempotency key.
503A dependency is briefly unavailable. Carries Retry-After.Yes, after the header says.

A 4xx from the network is still a 4xx

When MTN or Airtel refuses, whether declined, no wallet, or over a limit, you get that refusal with its meaning intact: a 400 or a 409 naming the reason, not a blanket 502. A mistyped phone number should not be logged as a provider outage, and on most payment APIs it is.

Retrying safely

For anything that moves money, retry only with the same Idempotency-Key. That turns a retry from a risk into a question, and is the single most important line of code in a payments integration.

  • Back off exponentially. One second, two, four. A tight retry loop against a struggling service is how a short outage becomes a long one.
  • Never retry a 400 or a 403. Nothing about them will have changed.
  • Treat a timeout as unknown, not as failed. The payment may well exist. Retry with the key, or ask GET /v1/payments/:id.

Rate limits

Budgets are per API key, per minute, and reads and writes are counted separately. Polling payment status cannot exhaust what creating a payment draws on.

BudgetPer minuteEndpoints
Writes120POST /v1/payments, /v1/payouts, /refund, POST /v1/apps
Reads600GET /v1/payments, /v1/payments/:id, /v1/balance, /v1/apps

Keyed on the API key, not your IP address. So scaling your servers out does not multiply your budget, and two companies behind one host do not share one.

Every response tells you where you stand:

RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 41

Over budget answers 429 with a Retry-After in seconds and "code": "RATE_LIMITED" in the body. Wait that long and try again; nothing about the request was recorded.

If you need more

These are guards against a runaway loop, not a commercial limit. If your legitimate traffic touches them, tell us and we will raise your budget. That is a setting on your account, not a negotiation.

The rails underneath have their own limits, and a 429 from them is passed straight through with the same shape. So one retry-on-429 path handles both.

Be reasonable with polling

Polling GET /v1/payments/:id every few seconds while a customer watches a spinner is fine, and well inside the read budget. Polling every 200ms for an hour is neither, and is unnecessary — the webhook tells you the moment it happens.

Transaction limits

Minimum100 RWF, on cash in and cash out alike
MaximumSet by MTN and Airtel, per transaction and per day, per customer. Theirs, not ours, and a refusal comes back naming the limit.
CurrencyRWF only. There is no conversion and no second currency.
NetworksMTN MoMo (078, 079) and Airtel Money (072, 073). Anything else is a 400.