Every error is the same envelope, with a stable code your integration may
branch on and a human-readable message that may change copy over time:
{ "error": { "code": "insufficient_funds", "message": "..." } }
Codes are published: never renamed, never removed — only added. Branch on
code, not on message or status text.
Two namespaces
There are two different kinds of “it didn’t work”, and they behave
differently with money:
- Rejections (this page’s first table) — the request itself was refused.
No payout was created and nothing was held. Fix the request (or wait,
where retryable) and submit again.
- Failure codes (second table) — a payout was accepted, held funds, and
later died. The payout is terminal,
failure: { code, message } is set,
and the hold is always released back to your available balance.
Resubmission is always a new payout with a new Idempotency-Key.
Rejection codes
| Code | HTTP | Meaning | Retry? |
|---|
validation_error | 400 | Malformed request: bad JSON, missing field, or a missing Idempotency-Key on create. | After fixing the request. |
unauthorized | 401 | Missing, malformed, revoked, or unknown API key. | No — fix the key. |
forbidden | 403 | Org suspended, or request IP not on your allowlist. | No — resolve with Jenzy / fix the allowlist. |
not_found | 404 | No such resource in your org. Unknown, foreign, and malformed ids look identical. | No. |
insufficient_funds | 402 | The amount exceeds your available balance. | After funding (book a trade). |
conflict | 409 | This Idempotency-Key was used with a different body, or its original request is still in progress. | Different body → new key. In progress → same key, shortly. |
unknown_bank | 422 | bank_uuid doesn’t name a payable bank. | After re-fetching GET /banks. |
unknown_operator | 422 | operator_ref_id doesn’t name a payable operator. | After re-fetching GET /operators. |
invalid_mobile_number | 422 | The mobile number doesn’t parse for the target operator. | After fixing the number. |
invalid_account_number | 422 | The account number doesn’t pass validation for the target bank. | After fixing the number. |
amount_limit_exceeded | 422 | This payout is above the per-payout maximum. | With a smaller amount. |
rate_limited | 429 | Too many requests. | Yes — honor Retry-After. |
org_limit_exceeded | 429 | Your org’s rolling daily cap or payout-velocity check tripped. | Yes — later. |
internal_error | 500 | Something broke on our side. | Yes — and on create, reuse the same Idempotency-Key: you’ll get the original outcome if one was reached, never a double payout. |
rail_disabled | 503 | This rail is administratively paused. | Yes — later. |
service_unavailable | 503 | A dependency is unavailable (e.g. reference data has never synced). | Yes — later. |
Failure codes
Carried in failure.code on a failed payout — on GET /payouts/{id} and
in the payout.failed webhook, identically. failure.message is fixed
per-code copy, safe to show to your own users.
| Code | Meaning | Can a resubmission succeed? |
|---|
recipient_account_invalid | The provider rejected the destination (account doesn’t resolve, invalid number). | Only after the destination is corrected. |
declined | The provider declined the payout, or failed it without a reason. The catch-all. | Unlikely unchanged — investigate before resubmitting. |
provider_unavailable | Timeout or outage at the downstream provider. | Yes — a fresh payout may plausibly succeed; retry shortly. |
temporarily_unavailable | A temporary condition on the Hermes side. | Yes — retry later; no funds were deducted. |
A failed payout is never retried automatically, and the four codes above
are the complete failure vocabulary — anything unmappable lands in
declined.