{"slug": "stripe-shared-payment-tokens-five-wire-shapes-i-only-found-by-shipping", "title": "Stripe Shared Payment Tokens: five wire shapes I only found by shipping", "summary": "A developer building on Stripe's Shared Payment Tokens preview API documented five undocumented response shapes from real test-mode traffic, including HTTP 402 error envelopes with nested PaymentIntents, empty 3D Secure action payloads, and payment_method_details becoming null after a successful charge. The findings, captured against the 2026-04-22.preview version, are pinned in tests to prevent regressions and highlight pitfalls such as treating non-2xx as transport failures and relying on status codes other than 'succeeded'.", "body_md": "Five response shapes from Stripe's Shared Payment Tokens preview API that I didn't find documented, captured from real test-mode responses.\n\nStripe's Shared Payment Tokens are the payment primitive for agentic commerce: a token that carries its own spend limits — max_amount, currency, expires_at — so an agent can pay without ever holding a card credential. The concept docs are good, and there are a few decent explainers around now.\n\nWhat I couldn't find anywhere was the failure surface. Every write-up I read, including mine at first, covers the happy path: mint a token, charge it, done. Then you ship it, and the responses stop looking like the examples.\n\nBelow are five shapes captured from real test-mode responses while building a system that mints an SPT scoped to an exact approved amount and charges it. Each one cost me time. All are pinned in tests now, because prose is not evidence about a wire format.\n\nThis is against the 2026-04-22.preview version. Preview APIs move — treat these as observations with a date on them, not as a contract.\n\nIt doesn't. Stripe answers HTTP 402, which means your HTTP client throws, and the PaymentIntent you need is nested inside the error body:\n\nHTTP 402\n\n{\n\n\"error\": {\n\n\"code\": \"card_declined\",\n\n\"decline_code\": \"generic_decline\",\n\n\"network_decline_code\": \"01\",\n\n\"advice_code\": \"try_again_later\",\n\n\"message\": \"Your card was declined.\",\n\n\"charge\": \"ch_...\",\n\n\"payment_intent\": {\n\n\"id\": \"pi_...\",\n\n\"object\": \"payment_intent\",\n\n\"status\": \"requires_payment_method\"\n\n}\n\n}\n\n}\n\nIf your client treats a non-2xx as a transport failure, the exception escapes and you lose the decline entirely — no outcome, no record, just a stack trace. In my case that meant a run which had already held a seat and granted a spend authority ended with nothing written down.\n\nThe fix is narrow on purpose: convert an error envelope that carries an intent into a result, and rethrow everything else. A 401 or a missing preview header says nothing about whether money moved, so those must not be flattened into \"declined\".\n\nAnd use decline_code, not code. code is the category (card_declined) and tells a reader nothing. The actual reason is one field lower, in decline_code (generic_decline). My own test asserted the wrong one for over a week.\n\nReach this branch with pm_card_chargeDeclined.\n\nCharge a granted token whose underlying card is pm_card_authenticationRequired and you get:\n\nHTTP 200\n\n{\n\n\"status\": \"requires_action\",\n\n\"next_action\": {\n\n\"shared_payment_token_action\": {},\n\n\"type\": \"shared_payment_token_action\"\n\n},\n\n\"amount_received\": 0,\n\n\"last_payment_error\": null\n\n}\n\nThat's 3D Secure, seen from the seller's chair. Two things about it:\n\nThe action payload is empty. shared_payment_token_action: {} is what comes back — not trimmed for the docs, actually empty. You're told an action is required and handed nothing to perform it with. There is no redirect, no client secret to hand to a browser, nowhere to send the buyer. Which makes sense once you think about who the buyer is in an agentic flow: not present.\n\namount_received: 0. No money moved.\n\nThe practical rule: treat only status === \"succeeded\" as success. Not !== \"failed\", not \"2xx means paid\". A status you have never seen before must never read as success, because the alternative is telling someone their order is confirmed and then settling with your own money for funds that never arrived.\n\n// at grant time\n\n\"payment_method_details\": { \"card\": { \"brand\": \"visa\", \"last4\": \"4242\", ... }, \"type\": \"card\" }\n\n// after a successful charge against the same token\n\n\"payment_method_details\": null\n\nI assumed this was a preview gap and asked. It's intended behaviour, confirmed. A partial capture consumes the token too, so there's no used-but-still-readable state to fall back on.\n\nRelated, and it cost me an hour on its own: the field is payment_method_details, not payment_method_preview. Guess wrong and optional chaining swallows it — no error, the brand just silently goes missing from your UI.\n\n\"usage_limits\": { \"currency\": \"aud\", \"expires_at\": 1786185855, \"max_amount\": 120275 },\n\n\"usage_details\": { \"amount_captured\": { \"currency\": \"aud\", \"value\": 0 } }\n\nmax_amount is a bare integer in minor units. amount_captured, three lines away, is { currency, value }.\n\nCompare them directly and you get 120275 > [object Object], which is false, silently. Nothing throws. You just get a number on screen that is quietly wrong — and for a system whose entire claim is about spend limits, quietly wrong is the worst available failure.\n\nThe issued token (the agent's side) has a status field — active, requires_action, deactivated.\n\nThe granted token (the seller's side, spt_…) has no status field and no next_action. Those fields don't exist on it.\n\nSo when a charge comes back requires_action, the state blocking it is invisible on the only SPT object a seller can read. Neither party sees both halves. Don't write code that checks a granted token's status; there isn't one.\n\nTwo smaller things while we're here:\n\nThere is no list endpoint for granted tokens. Retrieve by id only. Store the id when you mint.\n\nExactly one webhook event exists: shared_payment.granted_token.deactivated, with the cause in deactivated_reason.\n\nOne trap that isn't a wire shape\n\nIf you get 403 \"does not have access to this endpoint\" from a rkcs_test_ sandbox key, that is very probably a key problem, not a regional-eligibility one. They are indistinguishable from the response.\n\nI nearly concluded SPT was unavailable on an Australian account because of this. It wasn't — the sandbox key hadn't been claimed. Don't declare a feature unavailable in your region without minting against a properly claimed key first.\n\nWhere this came from\n\nI built a policy-gated travel booking agent on top of this: it evaluates a request against a corporate travel policy and only then mints a token scoped to the exact approved fare. Rules that are a financial limit become usage_limits enforced by the network; rules that aren't — cabin class, carrier — are enforced by never minting a token at all.\n\nThe code is public, and every shape above is pinned in tests/stripe-token.test.ts against the real captured responses:\n\n[https://github.com/hkarekar403/TG24-Stripe-Shared-payment-tokens](https://github.com/hkarekar403/TG24-Stripe-Shared-payment-tokens)\n\nIt runs offline with no keys and no network if you just want to see the flow:\n\nnpm install\n\nINVENTORY_MODE=replay PAYMENTS_MODE=stub JOURNAL_PATH=:memory: npm start\n\nIf you've hit a shape I haven't, I'd genuinely like to know — the preview surface is bigger than what any one integration touches.", "url": "https://wpnews.pro/news/stripe-shared-payment-tokens-five-wire-shapes-i-only-found-by-shipping", "canonical_source": "https://dev.to/hkarekar403/stripe-shared-payment-tokens-five-wire-shapes-i-only-found-by-shipping-3ch3", "published_at": "2026-08-20 14:24:18+00:00", "updated_at": "2026-08-20 14:45:34.930446+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-products"], "entities": ["Stripe", "Shared Payment Tokens"], "alternates": {"html": "https://wpnews.pro/news/stripe-shared-payment-tokens-five-wire-shapes-i-only-found-by-shipping", "markdown": "https://wpnews.pro/news/stripe-shared-payment-tokens-five-wire-shapes-i-only-found-by-shipping.md", "text": "https://wpnews.pro/news/stripe-shared-payment-tokens-five-wire-shapes-i-only-found-by-shipping.txt", "jsonld": "https://wpnews.pro/news/stripe-shared-payment-tokens-five-wire-shapes-i-only-found-by-shipping.jsonld"}}