A declined payment returns 200. The request was well formed, it reached the issuer, and the issuer said no. Nothing is wrong with your integration.
That is the distinction your code has to make:
| HTTP status | What it means | What to do | |
|---|---|---|---|
| Approved | 200, result is APPROVED | The issuer approved it | Fulfill |
| Declined | 200, result is DECLINED | The issuer refused it | Ask for another payment method |
| Error | 200, result is ERROR | The transaction could not be attempted | Investigate; do not retry blindly |
| Failed request | 4xx or 5xx | The request never became a transaction | See Errors and retries |
Branch on result, not on the status code. Treating any 200 as success is the most common defect in a first integration, and it ships goods for payments that never happened.
{
"id": "TRANSACTION-01KEW36C9TQ5N1P7YRFGZ0BVW4",
"type": "SALE",
"result": "DECLINED",
"response_code": "51",
"response_description": "Insufficient funds",
"batch_id": null,
"amount": 10000
}response_code and response_description come from the processor. batch_id is null, because a declined transaction never joins a batch.
The transaction still exists and still has an id. A decline is recorded, not discarded, so you can read it back later from the reporting endpoints.
The useful split is between declines that might succeed later and declines that never will.
- Do not retry the same card immediately. Card networks penalize repeated attempts on a refused card, and issuers may treat a burst as suspicious.
- A soft decline — insufficient funds, a velocity limit — may succeed on a different day. For a recurring charge, retry on a schedule measured in days, not seconds.
- A hard decline — closed account, stolen card, invalid number — will not succeed. Ask for a different payment method.
- Never retry with a slightly different amount to get around a decline. It looks like probing, and it is.
The gateway does not classify a decline as soft or hard for you. The processor's response_code carries that meaning, and the mapping is processor-specific.
Do not show response_code, response_description, or anything under source. "Do not honor" and 51 mean nothing to a cardholder, and some of them imply things about their account that you should not be relaying on the issuer's behalf.
Say what they can do instead:
That card was declined. Try another card, or contact your bank.
Keep the codes in your logs, where they are worth having. Log the x-request-id header with them — it is what support needs to trace one specific attempt.
An ACH debit can be approved and then returned days later. There is no decline at the moment of payment to catch.
Watch is_returned on the transaction rather than waiting for a decline that will not come. Accept an ACH payment covers the return fields and the trap that return_code is omitted rather than null.
No card number triggers a decline against the real gateway. To exercise this path in the API reference, send the header x-redocly-response-body-example: saleDeclined and the mock returns a declined body. See Environments and test data.