How does a payment agent confirm an order that never paid?

A soft decline is the bank saying 'not yet': temporary, retryable. If the agent reads only the HTTP 200 and never checks the transaction state, it treats a failed payment as a paid order and ships the goods.

B

Balagei G Nagarajan

5 MIN READ


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).

A checkout register flashing a green approved light to a departing customer carrying goods, while the payment terminal beneath quietly shows a declined, try-again message that no one read
Decline lives in a nested field: transaction state "declined", reason "insufficient_funds".
— from “How does a payment agent confirm an order that never paid?”

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.

A decision path: a gateway response with http_status 200 but a nested declined state; the unverified path reads 200 and fires order, email, inventory on an unpaid order; the verified path asserts transaction.state, routes the soft decline to retry or step-up, and only proceeds on captured

Reading the status versus verifying the state

StepUnverified agentVerified agent
ReadsTop-level http_status 200Nested transaction.state
Soft declineTreated as successRouted to retry / step-up
Downstream actionsFire on unpaid orderOnly after captured
Schema driftSilently passes old checkContract validation fails loudly
Retry safetyRisks double chargeIdempotency 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.


Share this post

Join the discussion

Have a take, a war story, or a question? Sign in with GitHub to comment and react. Comments are powered by GitHub Discussions, ad-free and yours to moderate.

Continue Reading

Find where your agent breaks, before you build it

Faultmap maps where your agent will fail from the goal and your data, then hands you the first test suite it has to pass.