Soft declines make up the majority of card declines, and they can ride inside an HTTP 200.Roughly 80% to 90% of declines are temporary and recoverable, so even a careful agent that trusts the status code instead of the nested transaction state will confirm orders that never actually paid (Checkout.com, 2025).

Key facts.
- Roughly 80-90% of declines are soft, temporary, recoverable: insufficient funds, step-up auth, processing glitch. A large share clear on a correct retry. Hard declines are permanent and must not be retried (Checkout.com, soft declines, 2025).
- HTTP 200 means the request was received and processed at the transport layer. Not that the business operation succeeded. Payment outcome lives in the response body, not the status line (RFC 9110, 200 OK semantics).
- Handling a soft decline safely, retrying or re-checking without charging twice, depends on an idempotency key so the reattempt references the same logical transaction (Checkout.com).
What does the failure actually look like?
A soft decline hidden in a 200 ships the order on money that never moved; a more capable agent reads it the same. (Checkout.com, 2025)
Consider the anatomy of it. Gateway returns a soft decline. Transport succeeded, so HTTP 200. Decline lives in a nested field: transaction state "declined", reason "insufficient_funds". The agent reads only the top-level 200, concludes payment succeeded, and fires every downstream action a real payment should trigger: confirmation email, order record, inventory decrement. None of those should have happened, because the money never moved. The decline was temporary and recoverable, the kind that a retry or a step-up would often clear (Checkout.com), but the missing state check turned a handleable not-yet into a silent paid-and-shipped.
# Transport succeeded (200). The PAYMENT did not. Read the nested state. { "http_status": 200, "transaction": { "state": "declined", "reason": "insufficient_funds" } } # Agent reads http_status==200 -> "success" -> emails + order + inventory. # Correct: assert transaction.state == "captured" before any downstream action.
Why doesn't the agent catch it?
Because it trusted a superficial success signal instead of verifying the business state. A 200 means the request was transported and accepted, not that the payment captured, and the agent collapsed those two into one. There was no independent post-call check, re-query the payment status, assert the transaction reached a captured state, before treating the order as paid, so the gap between accepted and settled was invisible. this is the same failure underneath any clean-looking response: the call returns successfully at the transport layer while the business outcome is wrong. And it's brittle to drift, a gateway schema change that nests the state differently will silently satisfy a top-level check that was written for the old shape, so an agent that passed in testing starts shipping unpaid orders in production without a single error being thrown.
How would verification have caught it?
By checking the transaction state, not the status code, and treating a soft decline as what it's. Validate the full response against its contract and assert the nested state explicitly, the transaction must be captured, not merely a 200, before any downstream action runs. Classify the outcome: a soft decline is recoverable, so route it to a bounded retry or a step-up flow rather than success, while a hard decline is permanent and must not be retried (Checkout.com). Re-query settlement to confirm the money actually moved, and use idempotency keys so the retry or the re-check can't trigger a second charge. Pin and monitor the gateway schema so drift is caught, not silently absorbed. The order then ships only when the payment is confirmed captured.

Reading the status versus verifying the state
| Step | Unverified agent | Verified agent |
|---|---|---|
| Reads | Top-level http_status 200 | Nested transaction.state |
| Soft decline | Treated as success | Routed to retry / step-up |
| Downstream actions | Fire on unpaid order | Only after captured |
| Schema drift | Silently passes old check | Contract validation fails loudly |
| Retry safety | Risks double charge | Idempotency key protects it |
The pattern is that a soft decline is a recoverable not-yet that can ride inside a 200, so an agent reading only the status code confirms an order that never paid and ships it. Assert the nested transaction state, classify and retry soft declines safely with idempotency, and verify settlement before any downstream action, and a temporary decline becomes a handled retry instead of an unpaid shipment. Verifying the business state rather than the status code is reliability at the pattern level, which is what VibeModel builds as the Pattern Intelligence Layer.
Frequently asked questions
Why would a declined payment return a 200?
Because the HTTP status reflects the transport, not the payment. The request succeeded in reaching the gateway, so it returns 200, while the decline lives in a nested transaction state. You have to read the state, not the status code.
How should an agent handle a soft decline?
As recoverable. Route it to a bounded retry or a step-up authentication flow rather than treating it as success, and never trigger downstream actions until the transaction is confirmed captured. Hard declines, by contrast, must not be retried.
How do I make the retry safe?
Use an idempotency key so retrying or re-checking the payment references the same logical transaction and can't create a second charge, then assert the settled, captured state before proceeding.

