| name | checkout |
| description | 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. |
Autumn Checkout Guide
When to Use This Skill
- Debugging Autumn checkout creation, retrieval, preview, or confirmation
- Understanding why
billing.attach returned an Autumn checkout URL
- Working on
server/src/internal/checkouts/
- Changing how checkout previews are rendered or confirmed
- Determining whether a flow should use
autumn_checkout, stripe_checkout, or no checkout
- Explaining the attach confirmation flow to another agent quickly
Core Concept
Autumn 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 flow
- If the attach context resolves to
checkoutMode === "autumn_checkout", Autumn does not execute billing immediately
- Instead, it stores a lightweight checkout object containing the original attach params
- Public checkout routes later re-run attach from those stored params to build a fresh preview or to execute billing on confirm
This means the checkout does not persist a frozen billing plan. It persists the request, then recomputes from current state.
Entry Point
Main path:
server/src/internal/billing/v2/actions/attach/attach.ts
server/src/internal/billing/v2/actions/attach/createAutumnCheckout.ts
server/src/internal/billing/v2/utils/billingPlan/billingPlanToAutumnCheckout.ts
server/src/internal/checkouts/middleware/checkoutMiddleware.ts
server/src/internal/checkouts/handlers/handleGetCheckout.ts
server/src/internal/checkouts/handlers/handlePreviewCheckout.ts
server/src/internal/checkouts/handlers/handleConfirmCheckout.ts
Attach -> Autumn Checkout
attach() 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:
- Autumn checkout is decided after the billing plan exists
- Validation already ran before the checkout is created
skipAutumnCheckout: true is the escape hatch used by confirm so the second attach call executes billing instead of creating another checkout
What Gets Stored
billingPlanToAutumnCheckout() builds a Checkout record with:
id
org_id
env
internal_customer_id
customer_id
action: "attach"
params
params_version
status: "pending"
created_at
expires_at
Storage model:
- Cache is the primary store via
setCheckoutCache()
- Postgres is written as audit/backup via
checkoutRepo.insert()
- TTL is 24 hours in cache, and
expires_at is also set on the DB record
The returned billing response uses checkoutToUrl() so the caller gets /c/:checkout_id as payment_url.
Public Checkout Flow
Router + Middleware
server/src/internal/checkouts/checkoutRouter.ts exposes:
GET /:checkout_id
POST /:checkout_id/preview
POST /:checkout_id/confirm
checkoutMiddleware does the shared setup:
- Rate limits by checkout ID
- Loads checkout from cache first
- Falls back to DB only to determine that the checkout exists but is unavailable
- Rejects completed or expired checkouts
- Marks expired DB records as
expired
- Rehydrates public request context with the checkout's
org, env, and features
Key behavior: if cache is missing, the middleware does not rebuild the checkout from DB. It throws unavailable after checking DB for audit state.
GET /checkouts/:checkout_id
handleGetCheckout.ts:
- Only supports
CheckoutAction.Attach
- Casts
checkout.params back to AttachParamsV1
- Calls
billingActions.attach({ preview: true })
- Recomputes the current billing plan from the stored params
- Converts that plan into an attach preview response for the UI
The checkout page therefore renders current computed pricing, not a persisted snapshot from creation time.
POST /checkouts/:checkout_id/preview
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.
POST /checkouts/:checkout_id/confirm
handleConfirmCheckout.ts:
- Validates
action === "attach"
- Validates
status === "pending"
- Re-runs
attach({ preview: false, skipAutumnCheckout: true })
- Executes the real billing plan
- Deletes the cache entry so the checkout is one-time-use
- Marks the DB row as
completed
- Returns success metadata including
invoice_id
Important error behavior:
- Cache is deleted only after successful execution
- On failure, the checkout stays pending and cached so the user can retry
- Non-
RecaseError failures are wrapped as internal checkout failures
Checkout Mode Decision Tree
The 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.
First Pass: Should Stripe Checkout Be Required?
Stripe checkout is chosen when Autumn cannot or should not bill directly:
- Customer has no payment method and product is one-off
- Customer has no payment method, product is paid recurring, and customer does not already have a Stripe subscription
- Exception: if that first paid recurring product starts with a trial and
cardRequired === false, it returns null instead of Stripe checkout
Two important suppressors:
- If a payment method already exists, this pass returns
null
- If
invoiceMode is enabled, this pass returns null
Second Pass: Forced Redirects (redirect_mode: "always")
If the first pass returned null and redirect_mode === "always", Autumn forces a redirect-style flow:
- One-off product ->
"stripe_checkout"
- Paid recurring product with no existing Stripe subscription ->
"stripe_checkout"
- Everything else ->
"autumn_checkout"
When Autumn Checkout Applies
Autumn checkout is the fallback for redirect_mode: "always" when Stripe checkout is not required.
In practice, that means cases like:
- Customer already has a payment method, and you still want a confirmation page before applying attach
- Customer is changing an existing recurring subscription and you want a redirect/confirmation UX
- Customer is attaching something that is neither one-off nor the first paid recurring subscription, and Stripe checkout is unnecessary
- Invoice mode is enabled,
redirect_mode is "always", and you still want the user to land on an Autumn confirmation page
- Free-product attaches with
redirect_mode: "always" also land here
The important mental model:
stripe_checkout means Stripe still needs to collect payment details or own the checkout UX
autumn_checkout means Autumn already has enough context to bill, but the API caller requested a confirmation step
What Autumn Checkout Does Not Do
- It does not support arbitrary billing actions today; handlers currently accept only
CheckoutAction.Attach
- It does not store a frozen
billingPlan
- It does not bypass normal attach validation
- It does not delete the DB row on success; it marks it completed and removes the cache entry
- It does not recover a missing cache entry by restoring from DB
Debugging Checklist
If a checkout link appears unexpectedly:
- Check
params.redirect_mode
- Check whether
setupAttachCheckoutMode() saw a payment method
- Check whether the product is one-off, free, or paid recurring
- Check whether the customer already has a Stripe subscription
- Check whether invoice mode or a no-card-required trial suppressed Stripe checkout
If the checkout preview looks different from the original attach response:
- Remember
GET /checkouts/:id recomputes attach from stored params
- Compare customer state between creation time and retrieval time
- Check whether feature quantities were changed via preview
If confirmation creates a second checkout instead of charging:
- Confirm the code path uses
skipAutumnCheckout: true
If a valid-looking checkout URL says unavailable:
- Check whether the cache entry expired or was deleted
- Check DB status for
completed or expired
- Remember DB is audit/backup, not a recovery source for public use
Current Scope
The data model allows CheckoutAction.UpdateSubscription, but the public handlers currently only support attach.
If you extend Autumn checkout beyond attach, update:
- checkout creation
- public handlers
- preview/response shaping
- middleware assumptions
- any skill docs that still describe attach-only behavior