Cart Updates
Only add cart updates if the payer can change something after checkout opens, such as plan, interval, quantity, discount, or shipping choice. If the payer cannot edit the order, keep the cart fixed from createHandoff(...) through payment.
Why Use The Adapter
Section titled “Why Use The Adapter”After a Proxy session is bound to a Stripe Checkout Session, the two systems must stay in sync. Do not update Stripe line items directly. Use syncCheckoutCart(...) so the SDK can:
- retrieve the current Proxy cart;
- build the next merchant cart;
- write the new Proxy cart with an expected cart version;
- update Stripe Checkout Session line items;
- roll back or surface reconciliation failure if one side cannot be updated.
Route Shape
Section titled “Route Shape”If the payer can edit line items, include stable item IDs in the cart snapshot you pass to createHandoff(...). The route below uses those IDs to update quantity.
import { createProxyCheckoutServerClient } from "@proxy-checkout/server-js";import { ProxyStripeCartSyncError, syncCheckoutCart,} from "@proxy-checkout/stripe-server-js";import Stripe from "stripe";
const proxy = createProxyCheckoutServerClient({ apiKey: process.env.PROXY_SECRET_KEY!,});
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
type CartSnapshot = { currency: string; lineItems: Array<{ id: string; name: string; amountMinor: number; quantity: number; }>;};
type CartUpdateInput = { checkoutSessionId: string; lineItemId: string; quantity: number;};
export async function POST( request: Request, { params }: { params: { proxySessionId: string } },): Promise<Response> { const input = (await request.json()) as CartUpdateInput; if (!Number.isInteger(input.quantity) || input.quantity < 1) { return Response.json({ error: "invalid_quantity" }, { status: 400 }); }
try { const result = await syncCheckoutCart({ proxy, stripe, proxySessionId: params.proxySessionId, checkoutSessionId: input.checkoutSessionId, input, buildNextCart: ({ currentCart, input }) => { const cart = currentCart as CartSnapshot; // Only allow cart changes your checkout UI supports. // Recompute item prices from your server-side catalog here. const lineItems = cart.lineItems.map((item) => item.id === input.lineItemId ? { ...item, quantity: input.quantity } : item, ); const amountMinor = lineItems.reduce( (total, item) => total + item.amountMinor * item.quantity, 0, ); const nextCart = { ...cart, lineItems };
return { amountMinor, cart: nextCart, cartSnapshot: nextCart, currency: nextCart.currency, }; }, buildStripeLineItems: (cart) => cart.lineItems.map((item) => ({ price_data: { currency: cart.currency, product_data: { name: item.name }, unit_amount: item.amountMinor, }, quantity: item.quantity, })), });
return Response.json({ amountMinor: result.amountMinor, cart: result.cart, currency: result.currency, }); } catch (error) { if (error instanceof ProxyStripeCartSyncError && error.reconciled === false) { return Response.json({ error: "cart_reconciliation_required" }, { status: 409 }); }
throw error; }}Keep the route authenticated or otherwise constrained to the active payer checkout flow. The route should only accept changes your product supports, and it should recompute pricing from your server-side catalog.
Error Handling
Section titled “Error Handling”A ProxyStripeCartSyncError with reconciled: false means Proxy and Stripe may not agree. Block payment confirmation for that session until an operator or reconciliation process resolves the issue.
- Recompute totals server-side; never trust browser-submitted amounts.
- Use one checkout update path after provider binding.
- Treat cart version conflicts as a prompt to re-read the current session and retry from fresh state.
- Test rollback and duplicate-click behavior, not just the successful path.