HTTP status mapping
The HTTP status and the error type always agree. There is no case where a
200 carries a failure in the body — a common pattern in older payment APIs and
a reliable source of bugs, because a client that checks only the status code
silently treats failures as successes.
Statuses
| Status | type | Meaning | Retry? |
|---|---|---|---|
200 | — | Success | — |
201 | — | Created | — |
202 | — | Accepted; processing asynchronously | — |
204 | — | Success, no body | — |
400 | invalid_request_error | Malformed request, unknown parameter, bad JSON | No |
401 | authentication_error | Missing, invalid, or expired credentials | No |
403 | permission_error | Valid credentials, insufficient permission | No |
404 | invalid_request_error | Resource does not exist, or is not yours | No |
409 | invalid_request_error | Conflict — idempotency key reuse, concurrent modification | Only for idempotency_request_in_progress |
422 | validation_error | Well-formed but fails a business rule | No |
429 | rate_limit_error | Rate limit exceeded | Yes, after Retry-After |
500 | api_error | Unexpected failure on our side | Yes, with backoff |
502 | api_error | A system we depend on failed | Yes, with backoff |
503 | api_error | Temporarily unavailable, e.g. maintenance | Yes, after Retry-After |
504 | api_error | Timed out downstream | Yes, with backoff — see below |
400 versus 422
The distinction is consistent and worth relying on:
400— the request could not be understood. Malformed JSON, an unknown field, a string where an integer belongs. The request is wrong in isolation.422— the request was understood and is well-formed, but conflicts with a rule or with current state. Issuing a card to a cardholder whose KYC has not passed is a422; the request is perfectly valid, the situation is not.
Practically: a 400 means fix your code. A 422 means fix the state, or tell
the user something.
404 on objects that exist
A 404 is returned both when an object does not exist and when it exists but
belongs to another organization. The two are deliberately indistinguishable —
distinguishing them would let anyone probe for valid IDs across tenants.
Retrying 504 and network timeouts
A 504 or a client-side timeout is the genuinely ambiguous case: the request
may have succeeded before the connection dropped.
Do not resolve this by guessing. Retry with the same
Idempotency-Key. If the original succeeded, you
get the original response back, with Idempotent-Replay: true. If it never
landed, it runs now. Either way, exactly one card is issued.
Without an idempotency key there is no safe answer, which is why the key should
be on every POST rather than only the ones that felt important at the time.
A correct retry predicate
function isRetryable(err) {
if (err.status === 429) return true;
if (err.status >= 500) return true;
if (err.code === 'idempotency_request_in_progress') return true;
return false;
}
Retrying anything else wastes your rate limit and delays surfacing a real bug. Every code's retryable flag is listed in the error code catalog.