A cart in AppEngine is a server-persisted document keyed by an authorid (the customer or guest identifier) and a cartid. There is one read endpoint, one write endpoint, and one clear endpoint. Line-item add/remove/update all go through the same update call — the client posts the desired cart state, the server replaces it.
Read a cart
/storefront/cart/get/:authorid/:cartid?No authauthorid is the signed-in customer's SK when authenticated, or a stable guest id (UUID) stored in localStorage for anonymous shoppers. cartid is optional — omit it and the server returns the customer's most recent open cart.
// authenticated
const cart = await storefrontAPI.getCart(`${authorId}/${cartId}`);
// guest (no cartId yet — server creates one)
const guestCart = await fetch(
`/api/storefront/cart/get/${guestId}`,
{ headers: { orgid: ORG_ID } },
).then(r => r.json());
The response is a BaseModel<Cart>. The interesting fields:
{
"pk": "my-org|cart",
"sk": "my-org|cart|cart-123",
"data": {
"cartId": "cart-123",
"authorId": "cust-abc",
"productItems": [
{
"sku": "SNK-AIR-1-9-BLK",
"name": "Air Sneaker (Size 9 / Black)",
"image": "https://cdn.../sneaker-1-black.jpg",
"options": { "size": "9", "color": "black" },
"quantity": 1,
"price": 89.99
}
],
"rentalItems": [],
"subtotal": 89.99,
"currency": "USD",
"updatedAt": "2026-04-25T10:12:00Z"
}
}
Update a cart
/storefront/cart/update/:cartidNo authThe body is the new cart state — line items and totals. The server reconciles prices and stock against the catalog. To remove an item, leave it out of productItems. To bump quantity, change quantity and re-post.
await fetch(`/api/storefront/cart/update/${cartId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', orgid: ORG_ID },
body: JSON.stringify({
authorId: authorId,
productItems: [
{ sku: 'SNK-AIR-1-9-BLK', quantity: 2, price: 89.99, name: 'Air Sneaker' },
],
rentalItems: [],
}),
});
The update endpoint is intentionally idempotent. The client owns the truth of "what's in the cart"; the server validates and persists. Don't try to model add-line / remove-line as separate calls.
Clear a cart
/storefront/cart/clear/:cartidNo authA GET (not DELETE) for symmetry with the rest of the cart endpoints. The cart record stays — only productItems and rentalItems are emptied — so the client can keep the same cartid after clearing.
Anonymous vs authenticated
The same endpoints work for both. The pattern in base-app:
- On first interaction, generate a UUID and store it in
localStorageasguest-cart-id. - Use that UUID as both
authoridandcartiduntil the user signs in. - After sign-in, call
cart/updatewith the customer's SK asauthoridand the samecartid— the cart attaches to the customer record.
Step 3 is deliberate: there's no separate "merge carts" endpoint. The customer's pre-signin cart is what they had as a guest, and any prior server-side cart for that customer is replaced on the next update. If you need merge semantics, do them client-side before the first authenticated update.
Mixed carts (products + rentals)
Rentals share the cart but live in rentalItems. The shape adds time fields:
{
"rentalItems": [
{
"sku": "RENT-DRILL-01",
"quantity": 1,
"startDate": "2026-05-01T09:00:00Z",
"endDate": "2026-05-03T09:00:00Z",
"rentalPeriod": "daily",
"price": 25
}
]
}
When checkout fires, the server splits the cart: products become a regular order, rentals become rental bookings. See POST /storefront/checkout-mixed for that flow.
Pricing the cart
Don't compute totals in the client beyond a rough preview. The pricing engine knows about coupons, group benefits, and free-shipping minimums:
/storefront/pricing/calculate-cartNo auth/storefront/discounts/calculateNo authThe discounts/calculate endpoint is the single source of truth for "what does this cart cost right now" — it accepts the cart, an optional coupon code, and shipping/billing addresses, and returns subtotal, shipping, tax, discount, and total. Re-run it on every cart change and at checkout. The base-app Checkout component does exactly this.
Persistence rules
- A cart record lives until explicitly cleared or until the org's purge policy kicks in (default 90 days untouched).
- One customer can have multiple carts (saved-for-later patterns); only one is "active" at any time, picked by the most recent
updatedAt. - Carts are scoped to the org via
orgidlike every other record. A cart from org A is invisible to org B even if the SK collides.