Retail & E-commerce// diagnostic

The agent confirmed the order and the charge failed afterwards

In short

A charge that fails after an agent has confirmed an order is usually a delegated payment token being refused, not a card being declined: the total moved above the allowance, the token expired mid-confirmation, it was bound to a different session or merchant, or a retry lost its idempotency key. Check those 4 in order, and unwind the order side before the money side.

Key takeaways

  • An order with no authorisation is a state your schema must have, not an exception to catch.
  • Read allowance, expiry, merchant binding and session binding in order — the first refusal ends the search.
  • Token expiries run in minutes, so a slow completion handler is a payment defect, not a latency one.
  • A retry without the original idempotency key is how one failure becomes 2 charges, or none.
  • Unwind the order before the money: fulfilment moves faster than any refund you can issue.

You have an order record, a shopper who was told the purchase went through, and no successful authorisation against it. This is almost never a card decline. It is a delegated payment token being refused, and that token carries only 4 constraints: a maximum amount, a currency, an expiry, and a binding to one merchant and one session. Read them in that order and stop at the first mismatch.

Nobody designs for this state. On your own storefront, payment happens before the order exists. In an agent flow the confirmation came from the agent's surface, your completion handler creates the order, and authorisation is a separate call that can fail after both. Your schema needs a state for "ordered, unpaid" or operations will invent one in a spreadsheet.

Pull one failed completion apart before you touch the code

Take one occurrence and reconstruct it. The published Agentic Commerce Protocol specification requires every request to carry an Idempotency-Key, a Request-Id, a Signature and a Timestamp, and the response to echo the first 2 back. Those identifiers join your logs to the agent's; if you are not persisting them against the order, you are debugging blind.

  1. Find the completion call. Locate the POST to your session-complete endpoint by Request-Id, and record when it arrived and when your handler answered. That gap decides cause 2.
  2. Read the session status at the moment of the call. The specification names 4: not_ready_for_payment, ready_for_payment, completed and canceled. Completion attempted against anything but ready_for_payment is a sequencing bug on your side.
  3. Compare 2 totals: the one your session last returned to the agent, and the amount you presented for authorisation. If they differ by even the smallest unit of currency, you have cause 1.
  4. Read the refusal as your processor recorded it. An allowance breach, an expired credential and a binding mismatch are 3 different refusals, and the processor distinguishes them even where your logs collapse them into "declined".
  5. Count authorisation attempts on that session. More than one, under more than one idempotency key, is cause 4 — the only one that can leave a duplicate charge rather than none.
CheckWhat you compareRefusal signatureWhere the fix lives
AllowanceAmount presented against the cap the token was issued forExceeds the authorised amount, cap unchangedYour session: the total moved after approval
ExpiryAuthorisation timestamp against the validity windowExpired, regardless of amountCompletion-handler latency, and when you request the token
Merchant bindingAcquiring account presenting it against the one it namesRefused before any amount check runsProcessor account mapping, usually multi-entity
Session bindingSession id on the token against the session completingNot matching, or reversed on reconciliationSession lifecycle: an id regenerated, retried or reused
The 4 constraint checks, in the order that answers the question fastest

Cause 1: the total moved after the allowance was cut

The most common cause, and entirely a merchant-side defect. The shopper approved an amount, their provider issued a token capped at it, and something on your side then recalculated the total between the response the agent rendered and the authorisation you presented. The cap held, which is the system working.

  • Tax resolved late. The address completed, a destination-based rate applied, and the total rose after the session had reported itself ready for payment.
  • A fulfilment option defaulted. The agent chose no shipping method, your handler picked one at completion, and the line appeared after the cap was set.
  • A promotion expired between confirmation and completion. Discount validity is evaluated at charge time on most platforms, and an agent session can straddle midnight.
  • Rounding on a multi-line basket. Per-line tax rounding that differs from your quote's aggregate rounding produces a one-minor-unit difference, which is still a breach.
  • A currency mismatch dressed as an amount problem. The token names one currency; a total in another is a different constraint failing.

The fix is a rule, not a patch: nothing may change the total once the session reports itself ready for payment. If something must, the session leaves that state and the agent obtains a new token. The figure the allowance was cut from came out of your own tool responses, which is why designing the tool surface a shopping agent is allowed to call treats response shaping as the whole job.

Cause 2: the credential expired while your handler was still working

Delegated tokens expire in minutes, not days, and that short window is most of what makes them safe to hand to software you did not write. It also makes your completion handler's tail latency a payment characteristic. If p99 on that handler is 40 seconds because it reserves stock in an ERP synchronously and calls a tax service, you lose authorisations on the slow tail and nowhere else.

Cause 3: the token belongs to a different merchant or a different session

Binding failures cluster in 2 places. The first is any retailer with more than one legal entity or acquiring account — brands under one group, a marketplace entity beside a first-party one, an account per market. The token names the merchant the shopper approved; present it from the sibling account and it is refused before any amount is examined, which reads in your logs as an inexplicable decline on a valid basket.

The second is a session id that did not survive: recreated by a retry path, regenerated on a cart merge, reused on a second attempt. A token bound to the original id and presented against the replacement is indistinguishable from an attack, and should be — a system that accepts a token against the wrong session has given up the property the binding exists for.

Cause 4: the retry that leaves you with 2 charges, or none

Idempotency keys exist so a network failure does not become a second charge. The first request under a key has its status code and body stored; later requests presenting the same key get that stored result back instead of executing again. Providers retain keys for a bounded window — Stripe documents at least 24 hours — after which a key is pruned and becomes a new request. The agentic checkout specification carries the idea into commerce with an Idempotency-Key on every call and an error code for a request that is not idempotent.

  • A retry with a fresh key is a new charge attempt. That is how one confirmation becomes 2 authorisations, and it happens whenever an outer layer generates the retry without the original key.
  • A retry with the same key but changed parameters is rejected, not executed. Providers compare incoming parameters against the stored ones, so a retry that recalculates the total fails as a mismatch, not a decline.
  • A retry outside the retention window silently becomes a first request. Anything replayed from a dead-letter queue a day later is no longer protected by the key it carries.
  • A retry that never reaches you looks identical to a refusal. If your edge challenged the second attempt, the order is unpaid because of your own bot rules — traced in your bot protection is blocking the AI surfaces you are trying to sell through.

Unwind the order before you touch the money

The asymmetry that decides the recovery order: fulfilment is faster and less reversible than settlement. A pick list printed in a warehouse cannot be recalled by a payments team. Stop the goods first, then work out what happened to the money.

  1. Block fulfilment by a hard state, not a flag some downstream job may ignore. An order without a recorded authorisation must be structurally unpickable.
  2. Establish whether anything was authorised. Query the processor by order reference, not your internal payment record — the failure you are investigating is one where those 2 disagree.
  3. Void rather than refund any stray authorisation. A void releases a hold; a refund creates a settled outbound movement, a line on the shopper's statement, and a support conversation you did not need.
  4. Decide whether the session is recoverable. Cancelled or completed is terminal; still open with a total problem is correctable in place.
  5. Reconcile the order last. Move it to a named state — awaiting payment, expired, or cancelled by merchant — and stop reporting counting it as revenue in the same run.

The dangerous minute is not the one where the charge fails. It is the one where the order exists, the warehouse job already fired, and nothing has told either side to stop.

Telling a shopper you may have no way to reach

The shopper may exist to you only as a buyer record on a session — no login, and no consent usable for anything but this transaction. The message has to travel back through the surface it came from, which means knowing which surface that was: tag the order at session creation, because afterwards there is no referrer to recover it from, as telling which orders came from an AI surface sets out.

Say what state the order is in — "we could not take payment, so nothing has been charged and nothing is being shipped" answers the question they actually have. Never ask for card details in a recovery message: no card data was ever meant to travel, and an email asking for a card is indistinguishable from a phishing attempt. Give support the refusal in words, because "payment_declined" is neither answerable nor, in these 4 cases, accurate.

What we build here, and what we deliberately do not

BuildspaceLabs integrates payment providers. We are not a payment institution, we hold no financial licence, and no funds pass through anything we run. We build the merchant side: the completion endpoint, the check that a token matches the session it claims, the state machine that refuses to create a shippable order before authorisation returns, and the job that finds the orders where those went wrong. That is MVP and product build work with payments inside it, in our retail and e-commerce practice.

What a clean authorisation still will not guarantee

All of this gets the money and the order to agree. It says nothing about whether the order was right: an authorisation that succeeds against the wrong variant never appears in a payments log, and is traced in the size chart and the SKU disagree. Ordinary card declines and fraud rules are a different investigation too — those fail before the constraints are evaluated.

The rest of the handshake sits in agentic commerce. Take the constraint behaviour from there and the field names from whichever specification you implement: the checkout revision we read pins an API-Version dated 2025-09-12, and property names have moved between revisions before.

Frequently asked questions

Short answers to the follow-ups this page tends to raise.

Why did the payment fail after the agent already confirmed the order?

Because confirmation and authorisation are 2 separate events and only the second moves money. The agent confirmed from your session response; the charge then went against a delegated token with a capped amount, a currency, a short expiry and a binding to one merchant and one session. If the total moved, the token aged out or the binding failed, the charge is refused while the order already exists.

Should we retry the charge or cancel the order?

Retry only if the session is still open and you know which constraint refused. A total above the allowance can be corrected and re-authorised against a new token; an expired token cannot be retried, because the credential is dead. Retrying blindly under a new idempotency key is how one failure becomes 2 charges.

Can we charge the difference separately when the total exceeded the allowance?

No. The allowance is the amount the shopper approved, so a second charge for the balance is money they never authorised — and in a dispute, indistinguishable from overcharging. Correct the total, or have the agent obtain a new token for the correct figure.

How do we stop an unpaid order from being picked and shipped?

Make authorisation a precondition of the order state itself, not a flag on it. If no status can become shippable without a recorded authorisation reference, no downstream system has to remember the rule. Flags get ignored by exactly one integration, and it is always the one printing the pick list.

  • agentic commerce
  • payments
  • checkout
  • incident response
// shipped work

The work behind this page

Builds from our portfolio that this page draws on.

Read next

Working on something in this space?

Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.

Start the conversation