checkout-management
Checkout Management Skill
Overview
This skill covers how checkout sessions are created, stored, and managed in the Saleor storefront.
Checkout ID Storage
Checkout IDs are stored in two places:
1. Cookie (Primary Storage)
Cookie name: checkoutId-{channel}
Example: checkoutId-default-channel
The cookie is set in src/lib/checkout.ts:
export async function saveIdToCookie(channel: string, checkoutId: string) {
const cookieName = `checkoutId-${channel}`;
(await cookies()).set(cookieName, checkoutId, {
sameSite: "lax",
secure: shouldUseHttps,
});
}
2. URL Query Parameter
URL: /checkout?checkout=Q2hlY2tvdXQ6YThjN2Y4YjgtZmU0NS00ZTRkLThhZmItZDdjYWI2YTM5MTdm
The checkout ID is a base64-encoded Saleor global ID.
Checkout Lifecycle
Creation
A new checkout is created when:
- User adds first item to an empty cart
- No valid checkout ID exists in cookie
- Existing checkout is not found in Saleor
// src/lib/checkout.ts
export async function findOrCreate({ channel, checkoutId }) {
if (!checkoutId) {
return (await create({ channel })).checkoutCreate?.checkout;
}
const checkout = await find(checkoutId);
return checkout || (await create({ channel })).checkoutCreate?.checkout;
}
Persistence
The checkout persists across:
- Page refreshes
- Browser sessions (cookie-based)
- Cart modifications
Completion
When checkoutComplete mutation succeeds:
- Checkout is converted to an Order
- The checkout ID becomes invalid
- A new checkout should be created for future purchases
Common Issues
Hydration Mismatch with Checkout ID
Problem: extractCheckoutIdFromUrl() called during SSR reads an empty URL, causing React hydration mismatch and "PageNotFound" flash.
Symptom: Checkout page briefly shows error then loads correctly on refresh.
Fix: Delay extraction until after client-side mount:
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const id = useMemo(() => (mounted ? extractCheckoutIdFromUrl() : null), [mounted]);
See src/checkout/hooks/useCheckout.ts for the full implementation.
Stale Checkout with Failed Transactions
Problem: If payment fails multiple times, the checkout accumulates partial transactions. Subsequent payment attempts may fail with:
CHECKOUT_NOT_FULLY_PAID: The authorized amount doesn't cover the checkout's total amount.
Solutions:
- Clear cookies - Delete
checkoutId-{channel}cookie - Use incognito - Test in a private browser window
- Remove URL param - Navigate to checkout without
?checkout=XXX
Checkout Amount Mismatch
Problem: Checkout total changes after transactions are initialized (e.g., shipping added).
Solution: Always use live checkout data via useCheckout() hook before payment:
const { checkout: liveCheckout } = useCheckout();
const checkout = liveCheckout || initialCheckout;
const totalAmount = checkout.totalPrice.gross.amount;
Key Files
| File | Purpose |
|---|---|
src/lib/checkout.ts |
Checkout creation, cookie management |
src/checkout/hooks/useCheckout.ts |
React hook for checkout data |
src/checkout/lib/utils/url.ts |
URL query param extraction |
src/graphql/CheckoutCreate.graphql |
Checkout creation mutation |
Debugging Checkout Issues
1. Check Current Checkout ID
// In browser console
document.cookie.split(";").find((c) => c.includes("checkoutId"));
2. Decode Checkout ID
// Base64 decode the checkout ID from URL
atob("Q2hlY2tvdXQ6YThjN2Y4YjgtZmU0NS00ZTRkLThhZmItZDdjYWI2YTM5MTdm");
// Returns: "Checkout:a8c7f8b8-fe45-4e4d-8afb-d7cab6a3917f"
3. Query Checkout in Saleor
Use GraphQL playground to inspect checkout state:
query {
checkout(id: "Q2hlY2tvdXQ6...") {
id
totalPrice {
gross {
amount
currency
}
}
transactions {
id
chargedAmount {
amount
}
authorizedAmount {
amount
}
}
}
}
Payment App Issues
Transaction Fails with "AUTHORIZATION_FAILURE"
Symptom: Transaction is created but fails immediately:
{
"transaction": { "id": "...", "actions": [] },
"transactionEvent": {
"message": "Failed to delivery request.",
"type": "AUTHORIZATION_FAILURE"
}
}
Cause: The payment app (e.g., Dummy Gateway, Stripe, Adyen) is not responding.
Solutions:
- Check Saleor Dashboard → Apps - is the payment app active/healthy?
- Check if the payment app URL is accessible
- Restart the payment app if self-hosted
- Check Saleor Cloud status if using cloud-hosted apps
"CHECKOUT_NOT_FULLY_PAID" Error
Symptom: checkoutComplete fails with:
The authorized amount doesn't cover the checkout's total amount.
Causes:
- Payment app is down - transaction was created but authorization failed
- Stale checkout - previous partial transactions exist
- Amount mismatch - checkout total changed after transaction init
Debug steps:
- Check
[Payment] Transaction init result:logs fortransactionEvent.type - If
AUTHORIZATION_FAILURE→ payment app is down/unreachable - If transaction succeeded but amount is wrong → checkout data is stale
Best Practices
- Always use live checkout data for payment amounts
- Handle checkout not found gracefully (create new checkout)
- Clear checkout after completion to avoid stale data
- Test with fresh checkouts when debugging payment issues
- Check payment app health when transactions fail with
AUTHORIZATION_FAILURE