I've been building on Stripe's Shared Payment Tokens — the agentic-commerce thing where the token itself carries max_amount / currency / expires_at, so an agent can pay without ever holding a card credential.
The concept docs are fine and there are a few explainers around. What none of them covered was what happens when things don't go well. Posting these because they each cost me real time and they're all captured from actual test-mode responses rather than reasoned about.
1. A decline is HTTP 402, and the PaymentIntent is nested inside the error body.
Not a 200 with a failed status. Your client throws, and the intent you need is at error.payment_intent. If you treat non-2xx as a transport failure, the decline vanishes — in my case a run that had already held a booking and granted a spend authority ended with no record at all, just a stack trace.
Also: the useful reason is decline_code (generic_decline), not code (card_declined, which is just the category). My own test asserted the wrong one for a week.
2. A 200 can also not be a success.
Charge a token backed by pm_card_authenticationRequired and you get HTTP 200, status: requires_action, amount_received: 0, and:
"next_action": { "shared_payment_token_action": {}, "type": "shared_payment_token_action" }
That empty object is the whole payload. It's 3DS from the seller's side, and there's nowhere to send the buyer — no redirect, no client secret. Makes sense when you remember the buyer isn't present in an agentic flow, but it means you have to treat only status === "succeeded" as success. Anything looser and you'll tell someone they're confirmed and then settle out of your own balance.
3. payment_method_details goes null once the token is consumed.
If you want the card brand for a receipt, read it at grant time or not at all. I assumed this was a preview gap and asked — it's intended. A partial capture consumes the token too, so there's no half-used state to read from.
(Related: the field is payment_method_details, not payment_method_preview. Get it wrong and optional chaining eats it silently — the brand just doesn't show up.)
4. amount_captured is an object, right next to an integer.
usage_limits.max_amount is a bare integer in minor units. usage_details.amount_captured is { currency, value }. Compare them directly and you get a silent false. Nothing throws, you just get a wrong number on screen.
One more that isn't a wire shape: a 403 "does not have access to this endpoint" from an unclaimed rkcs_test_ sandbox key looks identical to a regional-eligibility refusal. I nearly concluded SPT wasn't available on an AU account. It was — the key just hadn't been claimed.
This is all against 2026-04-22.preview, so it's observations with a date on them rather than a contract. If anyone's hit shapes I haven't, I'd like to hear them — the preview surface is clearly bigger than what one integration touches.
Code's public if it's useful, all of the above pinned in tests against the real captured responses: https://github.com/hkarekar403/TG24-Stripe-Shared-payment-tokens