The error envelope
Every failed request answers with the same shape. Branch on error.code, which is stable, and show error.message, which is prose.
{
"error": {
"type": "invalid_request_error",
"code": "resource_missing",
"message": "That payment does not exist.",
"requestId": "req_01JAY7BQ2M4X8ND6VKQ0R3PZ5W"
}
}Request errors
These mean the request was wrong or could not be accepted. They are your problem to fix and they arrive with an HTTP status.
| HTTP | code | What it means |
|---|---|---|
| 400 | invalid_request | The body failed validation. Unknown fields are refused rather than ignored — a typo’d amount field must not silently charge a different figure. |
| 401 | unauthenticated | No key, a malformed key, a revoked key, or a source address the key does not permit. |
| 403 | permission_denied | The key authenticated but its scopes do not cover this operation. |
| 404 | resource_missing | No such object under your merchant. Somebody else’s id answers this too, so the endpoint cannot be used to test whether an id exists. |
| 409 | resource_conflict | The object is not in a state that allows this — already confirmed, already expired, or another attempt is in flight. |
| 412 | idempotency_key_reused | The same idempotency key arrived with a different body. That is a client bug, and answering with the first result would charge the wrong figure silently. |
| 422 | unprocessable_entity | Well-formed, but the platform cannot act on it. |
| 429 | rate_limit_exceeded | The key’s request budget is exhausted. Honour the retry-after header. |
| 503 | processor_unavailable | No provider route is available for this corridor right now. |
| 500 | api_error | Something failed on our side. The message never carries internal detail; quote the requestId. |
Payment failures
These are different: the request worked and the payment did not. The code is canonical across every provider, which is what makes decline analytics possible and what lets you write retry logic once.
| failureCode | What happened | Retry |
|---|---|---|
| INSUFFICIENT_FUNDS | The account or card had too little money. | Worth retrying |
| INVALID_ACCOUNT | Wrong number, or an account that does not exist. | Permanent |
| ACCOUNT_INACTIVE | The account exists but cannot transact. | Permanent |
| LIMIT_EXCEEDED | A per-transaction or per-period ceiling was hit. | Worth retrying |
| EXPIRED_INSTRUMENT | The card or mandate has lapsed. | Permanent |
| AUTHENTICATION_FAILED | A wrong PIN, or 3-D Secure was not completed. | Worth retrying |
| DECLINED_BY_ISSUER | The issuer refused without saying why. | Permanent |
| USER_CANCELLED | The customer declined the prompt. | Worth retrying |
| TIMEOUT | The customer never responded. | Worth retrying |
| DUPLICATE | The provider recognised this as a repeat of a payment it already took. | Permanent |
| FRAUD_SUSPECTED | Blocked by a risk check. | Permanent |
| CURRENCY_NOT_SUPPORTED | That rail cannot settle this currency. | Permanent |
| PROVIDER_UNAVAILABLE | The provider was unreachable or down. | Worth retrying |
| PROVIDER_ERROR | The provider answered with an error of its own. | Worth retrying |
| CONFIGURATION_ERROR | Our credentials or setup are wrong. Do not retry — this needs an engineer. | Permanent |
Retrying
A retryable failure is worth prompting the customer again. A permanent one is not — asking somebody to re-enter a closed card number four times is a worse experience than telling them once.