Skip to main content

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​

StatustypeMeaningRetry?
200—Success—
201—Created—
202—Accepted; processing asynchronously—
204—Success, no body—
400invalid_request_errorMalformed request, unknown parameter, bad JSONNo
401authentication_errorMissing, invalid, or expired credentialsNo
403permission_errorValid credentials, insufficient permissionNo
404invalid_request_errorResource does not exist, or is not yoursNo
409invalid_request_errorConflict — idempotency key reuse, concurrent modificationOnly for idempotency_request_in_progress
422validation_errorWell-formed but fails a business ruleNo
429rate_limit_errorRate limit exceededYes, after Retry-After
500api_errorUnexpected failure on our sideYes, with backoff
502api_errorA system we depend on failedYes, with backoff
503api_errorTemporarily unavailable, e.g. maintenanceYes, after Retry-After
504api_errorTimed out downstreamYes, 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 a 422; 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.