Skip to content

Cart updates

Only add cart updates if the payer can change plan, interval, quantity, discount, shipping, or another financial field after payment opens. Authorized viewers share the versioned Proxy cart until a qualifying payment.

Replace hosted and embedded Checkout Sessions

Section titled “Replace hosted and embedded Checkout Sessions”

Stripe does not support changing line items on hosted or embedded Checkout Sessions through this mutable-cart contract. syncCheckoutCart(...) detects an explicit hosted or embedded acquisition and throws ProxyStripeCartSyncError with the stable code: "hosted_replacement_required" before Stripe or Proxy mutation.

Replace a hosted or embedded Session in this order:

  1. Expire the bound Session from the merchant backend with the merchant-owned Stripe client.
  2. Wait for Proxy webhook/API reconciliation to expose the acquisition as provider-confirmed expired. Do not infer terminal state from a local timeout.
  3. Recompute prices from the server-side catalog and write the next Proxy cart with expectedCartVersion.
  4. Supersede the expired acquisition with reason checkout_expired and the canonical fingerprint of the next hosted parameters.
  5. Call openCheckout(...) with those exact parameters. Identical callers join the replacement and create one new Session.
import {
fingerprintHostedCheckoutProviderOptions,
openCheckout,
} from "@proxy-checkout/stripe-server-js";
await stripe.checkout.sessions.expire(acquisition.providerRootObjectId);
const expired = await waitForProxyAcquisitionStatus(acquisition.id, "expired");
const updated = await proxy.sessions.cart.set(proxySessionId, {
amountMinor: nextCart.amountMinor,
cartSnapshot: nextCart.cartSnapshot,
currency: nextCart.currency,
expectedCartVersion: session.cartVersion,
});
const nextParams = buildCheckoutSessionParams(updated);
await proxy.providerAcquisitions.supersedeProviderAcquisition(
proxySessionId,
expired.id,
{
expectedVersion: expired.version,
reason: "checkout_expired",
replacement: {
// Use "embedded" when replacing an embedded Checkout Session.
checkoutUiMode: "hosted",
integrationPath: "checkout_session",
providerOptionsFingerprint: fingerprintHostedCheckoutProviderOptions(nextParams),
},
},
);
return openCheckout({
...openOptions,
proxySessionId,
buildCheckoutSessionParams: () => nextParams,
});

Keep this route server-side and authenticated to the active payer flow. Browser callers provide only app-level edit input; they never provide Stripe credentials, Proxy metadata, or merchandiseSubtotalMinor.

If a stale or bypassed positive payment succeeds during replacement, Proxy retains the immutable payment and earned fee, raises reconciliation visibility, and does not provision it after another acquisition wins.

The Elements path uses that replacement sequence for immutable Session options. syncCheckoutCart(...) can update only a mutable custom Checkout acquisition. It validates the acquisition and contract-v2 metadata before provider access. Run it on the merchant server. Never accept browser-authored amount, currency, line, or fee inputs. It is not a fallback for hosted or embedded Checkout.

syncCheckoutCart(...) updates Checkout Sessions only. For a direct PaymentIntent financial edit, coordinate the Proxy cart and the bound PaymentIntent from your backend:

  1. Read the current Proxy session and acquisition.
  2. Calculate the next cart from your server-owned catalog.
  3. Write the next Proxy cart with expectedCartVersion.
  4. Update the bound PaymentIntent with the same amount and currency. Use an idempotency key derived from the Proxy session, acquisition, and cart version.
  5. If the Stripe update response is ambiguous, retrieve the same PaymentIntent.
  6. Confirm the acquisition cart with the new Proxy cart version, verified Stripe amount and currency, and deterministic evidence hash.

Do not treat a local rollback as proof that Stripe did not update. Until provider retrieval and Proxy confirmation complete, leave the revision unconfirmed. Proxy then reports a later payment as reconciliation-required instead of silently fulfilling stale cart state.