Errors & rate limits
One envelope, stable codes, and the retry rules that keep you out of trouble.
The error envelope
Every error carries the same four fields — message, code, statusCode and requestId. errors is added only on field-level validation failures. Branch on statusCode first, then refine on code: it is always there, so you never have to test for its presence.
{
"message": "Validation failed.",
"code": "validation_failed",
"statusCode": 400,
"requestId": "7186288b-2f41-4c6a-9d85-1b0e3a7c5f92",
"errors": [
{ "field": "amount", "code": "invalid_format", "message": "amount has an invalid format." }
]
}Each errors entry is exactly field, code and message. The field code is one of required, invalid_format, unsupported_value, out_of_range, unknown_field or duplicate — and unknown_field means what it says: a field that isn't part of the request is rejected, not ignored. duplicate means a record with the same value already exists, for example a beneficiary with the same name, bank country and bank currency.
On the beneficiary and payment-method endpoints, a 422 validation_failed rebuilds errors[] with a field that names where the problem is, such as bankingInformation.accountNumber or contactInformation.emailAddress. The 422 on POST /v1/beneficiaries for a name field that doesn't match entityType is the exception: it lists those fields in errorData.errors[] instead.
Keep the requestId. It's the fastest way for Jeeves support to find what happened, and it echoes your own X-Request-Id when you send one.
message is localized and may be reworded without notice — never branch on it.
Status codes and stable codes
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request_body | The body could not be parsed at all — malformed JSON, or a JSON value that isn't an object. No errors array, because nothing could be read. |
| 400 | validation_failed | The body parsed, but field values were rejected. Offending fields listed in errors. Also returned for an unrecognised query parameter. |
| 401 | unauthorized | Missing, invalid, or expired bearer token. Get a new token and retry. |
| 403 | forbidden | Token lacks the scope, client certificate missing/invalid, or source IP not allowlisted. |
| 404 | payment_not_found | Unknown payment, or one belonging to another customer (existence is never confirmed). |
| 404 | beneficiary_not_found | Unknown, deleted, malformed or other-customer beneficiary. The body is identical in every case, and it is never a 403. |
| 409 | idempotency_conflict | Same Idempotency-Key, different body. Nothing created. |
| 409 | payment_method_already_exists | The beneficiary already has a payment method on this rail. The check uses the payment method only, not the account number. |
| 422 | quote_expired | Quote consumed or expired with onQuoteExpiry: fail. |
| 422 | validation_failed | Well-formed but invalid for the corridor — purpose code not in the corridor's list, or a beneficiary detail failing corridor rules. |
| 422 | insufficient_funds | Balance below quoted amount plus fees. |
| 422 | invalid_schedule | quoteId and scheduledDate together, or a scheduled payment without amount. |
| 422 | unsupported_pair | No rate for this currency pair on the selected account type. |
| 422 | not_cancelable | The payment can't be cancelled: it is past in-review or scheduled, or it was already cancelled. |
| 429 | rate_limit | Limit exceeded. Nothing was executed, not even partially. |
| 500 | service_error | Unexpected error. |
Note the token endpoint is the one exception: POST /oauth/token errors follow the OAuth 2.0 format (error / error_description per RFC 6749), not this envelope.
Scope note: 409, 422, and 429 are Payments API semantics. The cards/boletos/webhooks endpoints in the main reference return the 400/401/403/404/500 subset documented there, and their auth failures carry the older code: AuthError rather than unauthorized / forbidden — branch on statusCode if you call both APIs.
Rate limits
Every response carries the window state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window (e.g. 300/minute per credential). |
X-RateLimit-Remaining | Requests left. |
X-RateLimit-Reset | Unix time the window resets. |
Retry-After | On 429 only: seconds to wait. |
On 429, back off exponentially with jitter, honoring Retry-After. Webhook deliveries to you don't count against your limits.
Retry rules
| Situation | Do |
|---|---|
429 | Wait Retry-After, back off with jitter, retry. Nothing was executed. |
Timeout or 5xx on POST /v1/payments | Retry with the same Idempotency-Key — the safe path. A new key risks a duplicate payment. |
Timeout or 5xx elsewhere | Idempotent reads are safe to retry as-is. |
401 | Request a new token, retry once. |
409 idempotency_conflict | Don't retry — use a fresh key for what is genuinely a new payment. |
422 | Don't blind-retry — the request was rejected deterministically; fix the cause (new quote, valid purpose code, funded account). |
Timeout on POST /v1/payments/{paymentId}/cancel, POST /v1/beneficiaries or POST /v1/payment-methods | These calls are not idempotent and read no Idempotency-Key. A retry after a success returns 422 not_cancelable, 422 with errors[].code: duplicate, or 409 payment_method_already_exists. Read the resource first to see whether the first call went through. |