Understand, debug, and edit Autumn checkout flows. Covers how attach creates an Autumn checkout, how public checkout routes recompute previews, how confirmation executes billing, and when Autumn checkout vs Stripe checkout vs no checkout is chosen.
billing.attach returned an Autumn checkout URLserver/src/internal/checkouts/autumn_checkout, stripe_checkout, or no checkoutAutumn checkout is not a separate billing engine. It is a thin confirmation layer around the existing V2 attach action.
attach() still does the normal setup -> compute -> evaluate flowcheckoutMode === "autumn_checkout", Autumn does not execute billing immediatelyThis means the checkout does not persist a frozen billing plan. It persists the request, then recomputes from current state.
Main path:
server/src/internal/billing/v2/actions/attach/attach.tsserver/src/internal/billing/v2/actions/attach/createAutumnCheckout.tsserver/src/internal/billing/v2/utils/billingPlan/billingPlanToAutumnCheckout.tsserver/src/internal/checkouts/middleware/checkoutMiddleware.tsserver/src/internal/checkouts/handlers/handleGetCheckout.tsserver/src/internal/checkouts/handlers/handlePreviewCheckout.tsserver/src/internal/checkouts/handlers/handleConfirmCheckout.tsattach() follows the normal V2 pipeline first:
const billingContext = await setupAttachBillingContext(...)
const autumnBillingPlan = computeAttachPlan(...)
const stripeBillingPlan = await evaluateStripeBillingPlan(...)
await handleAttachV2Errors(...)
After that, the branch is simple:
if (billingContext.checkoutMode === "autumn_checkout" && !skipAutumnCheckout) {
return await createAutumnCheckout(...)
}
return await executeBillingPlan(...)
Important consequences:
skipAutumnCheckout: true is the escape hatch used by confirm so the second attach call executes billing instead of creating another checkoutbillingPlanToAutumnCheckout() builds a Checkout record with:
idorg_idenvinternal_customer_idcustomer_idaction: "attach"paramsparams_versionstatus: "pending"created_atexpires_atStorage model:
setCheckoutCache()checkoutRepo.insert()expires_at is also set on the DB recordThe returned billing response uses checkoutToUrl() so the caller gets /c/:checkout_id as payment_url.
server/src/internal/checkouts/checkoutRouter.ts exposes:
GET /:checkout_idPOST /:checkout_id/previewPOST /:checkout_id/confirmcheckoutMiddleware does the shared setup:
expiredorg, env, and featuresKey behavior: if cache is missing, the middleware does not rebuild the checkout from DB. It throws unavailable after checking DB for audit state.
handleGetCheckout.ts:
CheckoutAction.Attachcheckout.params back to AttachParamsV1billingActions.attach({ preview: true })The checkout page therefore renders current computed pricing, not a persisted snapshot from creation time.
handlePreviewCheckout.ts is the same idea as GET, but it merges updated feature_quantities into the stored params before re-running preview attach.
Use this when debugging quantity edits in checkout UI.
handleConfirmCheckout.ts:
action === "attach"status === "pending"attach({ preview: false, skipAutumnCheckout: true })completedinvoice_idImportant error behavior:
RecaseError failures are wrapped as internal checkout failuresThe decision lives in server/src/internal/billing/v2/actions/attach/setup/setupAttachCheckoutMode.ts.
Possible outputs:
null"stripe_checkout""autumn_checkout"redirect_mode: "never"Always returns null.
No checkout URL is returned, even if one would otherwise be required.
Stripe checkout is chosen when Autumn cannot or should not bill directly:
cardRequired === false, it returns null instead of Stripe checkoutTwo important suppressors:
nullinvoiceMode is enabled, this pass returns nullredirect_mode: "always")If the first pass returned null and redirect_mode === "always", Autumn forces a redirect-style flow:
"stripe_checkout""stripe_checkout""autumn_checkout"Autumn checkout is the fallback for redirect_mode: "always" when Stripe checkout is not required.
In practice, that means cases like:
redirect_mode is "always", and you still want the user to land on an Autumn confirmation pageredirect_mode: "always" also land hereThe important mental model:
stripe_checkout means Stripe still needs to collect payment details or own the checkout UXautumn_checkout means Autumn already has enough context to bill, but the API caller requested a confirmation stepCheckoutAction.AttachbillingPlanIf a checkout link appears unexpectedly:
params.redirect_modesetupAttachCheckoutMode() saw a payment methodIf the checkout preview looks different from the original attach response:
GET /checkouts/:id recomputes attach from stored paramsIf confirmation creates a second checkout instead of charging:
skipAutumnCheckout: trueIf a valid-looking checkout URL says unavailable:
completed or expiredThe data model allows CheckoutAction.UpdateSubscription, but the public handlers currently only support attach.
If you extend Autumn checkout beyond attach, update: