new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Stripe in Production · all subjects

Checkout and Payment Intents

7 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

PaymentIntent succeeded client-side — can I fulfill the order?

No. Client-side confirmation (stripe.confirmPayment resolving without error) is a strong hint, not proof of money movement — fulfill only on the payment_intent.succeeded webhook (or a server-side retrieve confirming status). Reasons: webhook is the authoritative, signed channel; client state can be spoofed or raced; and for redirect-based methods (3DS, bank debits) the customer may never return to your success page. Pattern: create the PaymentIntent server-side, confirm client-side, show optimistic UI, but flip the order to 'paid' only from the webhook, deduped by event id. For strong consistency show 'processing' until the webhook lands and the client polls or receives a push. This rule also kills the 'success URL tampering' free-product exploit.

Why does my PaymentIntent sit in requires_action forever?

requires_action means the customer must complete authentication — usually 3D Secure/SCA — and nothing will progress until they do. With stripe.confirmPayment and a return_url, Stripe redirects the customer to their bank and back; if they close the tab mid-flow, the Intent stays in requires_action (or eventually requires_payment_method after abandonment/timeout) and you get no money and no failure event. Handle it: listen for payment_intent.requires_action / payment_intent.payment_failed webhooks, send an abandoned-payment email with a link that re-opens confirmation, and expire stale Intents. Don't create a new PaymentIntent per retry on the same order — reuse the same Intent (or its idempotency key) so you can't double-charge a customer who completes two flows.

When does 3D Secure / SCA actually trigger?

SCA (PSD2) applies when both the merchant's and cardholder's banks are in the EEA/UK — then 3DS is mandatory unless an exemption applies (low-value under ~€30, low-risk transaction-risk analysis, MIT/subscription exemptions for subsequent charges, corporate cards). Outside regulated regions, 3DS is optional but still available and shifts fraud liability to the issuer when completed. With Checkout and PaymentIntents using automatic payment methods, Stripe applies 3DS when required or when Radar rules request it (request_three_d_secure). Practical behavior: the FIRST subscription payment often needs 3DS; renewals usually run as merchant-initiated transactions without it — but issuers can still soft-decline with authentication_required, which is why dunning flows must support re-authentication via a hosted page.

Do idempotency keys expire? Can I reuse them?

Stripe stores idempotency keys for about 24 hours (as of early 2026; treat the exact window as Stripe-controlled). Within the window, retrying a POST with the same key returns the ORIGINAL response without re-executing — your safety net against double-creating charges or customers during timeouts. After the window, the same key executes as a fresh request. Rules: generate a key per logical operation (e.g. order UUID), not per request attempt; reuse it across retries of THAT operation only; never reuse keys across different operations or different users. Keys are per-account and apply to POST mutations. They don't make GETs idempotent (GETs already are), and they don't dedupe webhooks — that's the event-id job.

Checkout success_url as the fulfillment signal — what's the worst case?

Worst case is real and common: a customer pays, their bank's 3DS page or a browser crash prevents the redirect back, success_url never loads, and your app never provisions the product — an angry paying customer. The reverse also happens: users learn they can just visit /success?session_id=guessable and your naive handler grants access without payment (always verify the session server-side if you do anything on that page). The success_url is for UX only. Fulfillment must live in the checkout.session.completed / invoice.paid / payment_intent.succeeded webhook path, which fires regardless of whether the browser ever comes back. Treat every client-side success signal as 'probably paid, confirm pending'.

PaymentIntent stuck in requires_action — cancel it or keep waiting?

requires_action means the customer never finished authentication, usually 3DS. Exact abandonment timeouts vary by payment method and flow — there is no single documented number to rely on — so treat 'stuck' as an operational state, not a clock you can trust. Run a periodic job that lists Intents sitting in requires_action or requires_confirmation older than your business window (e.g. a checkout session's lifetime), email the customer a link that resumes confirmation on the SAME Intent, and cancel the Intent once your window closes so it can't complete unexpectedly weeks later. Alert on the stale-Intent rate — a spike usually means your 3DS return_url or redirect flow is broken. Never create a second Intent for the same order; reuse and resume the existing one.

Checkout Sessions or the Payment Intents API — which do I build on?

Checkout is Stripe-hosted: you create a Checkout Session, redirect (or embed), and Stripe renders the payment page, handles SCA/3DS redirects and payment-method localization — least code, least PCI/SCA surface, least UX control. Key events: checkout.session.completed, plus async_payment_succeeded/async_payment_failed for delayed methods, and the underlying invoice.paid or payment_intent.succeeded. The Payment Intents API is fully custom: you render your own form with Stripe.js/Elements or the Payment Element, confirm client-side, and own the UX — but you must handle requires_action, redirects, errors, and retries yourself; SCA still applies automatically when required. Rule of thumb: default to Checkout (or Payment Links) for standard SaaS; drop to Payment Intents when you need native in-app UX or payments embedded in a larger custom flow.

Give your agent this brain