# Cart Lifecycle The cart is the central resource in the Tote Online Ordering API. It holds the customer's item selections, modifier customizations, and handoff preferences until checkout converts the cart into an order. This guide walks through the complete cart lifecycle: creating a cart, adding items with modifiers, calculating prices, setting a handoff mode, and checking out. By the end, you will have made every cart-related API call in sequence. ```mermaid sequenceDiagram participant Client participant API as Tote API Client->>API: POST /carts API-->>Client: Cart (ACTIVE, empty) Client->>API: GET /locations/{id}/menu API-->>Client: Menu with items & modifiers Client->>API: POST /carts/{id}/items API-->>Client: Cart with item added Client->>API: POST /carts/{id}/items API-->>Client: Cart with 2 items Client->>API: POST /carts/{id}/calculate API-->>Client: PriceCalculation breakdown Client->>API: PUT /carts/{id}/handoff API-->>Client: Cart with handoff mode set Client->>API: POST /carts/{id}/checkout API-->>Client: Order (PENDING) ``` ## Prerequisites Before following this guide, make sure you have: - **An access token** -- See the [Getting Started guide](/online-ordering/guides/01-getting-started) and [Authentication guide](/online-ordering/guides/02-authentication) for how to obtain and manage tokens. - **A location ID** -- Use `GET /locations` to find a store. See [Getting Started](/online-ordering/guides/01-getting-started#step-2-list-locations). - **Familiarity with the menu structure** -- Menu items contain modifier groups that define customization options. See the [Modifiers guide](/online-ordering/guides/05-modifiers) for a full walkthrough. All examples in this guide use the sandbox base URL and consistent IDs so you can follow along step by step. ``` Base URL: https://sandbox.api.tote.ai/v1/online-ordering Location: b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d ``` ## Step 1: Create a Cart Create a new cart for a specific location. The cart starts empty with ACTIVE status and zero totals. ```bash curl -X POST https://sandbox.api.tote.ai/v1/online-ordering/carts \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d" }' ``` **Response (201 Created):** ```json { "id": "c1d2e3f4-a5b6-7890-cdef-1234567890ab", "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d", "status": "ACTIVE", "items": [], "handoff_mode": null, "age_verification_required": false, "subtotal": { "amount": 0, "currency": "USD" }, "total_tax": { "amount": 0, "currency": "USD" }, "total_discount": { "amount": 0, "currency": "USD" }, "total": { "amount": 0, "currency": "USD" }, "created_at": "2026-01-31T10:00:00Z", "updated_at": "2026-01-31T10:00:00Z" } ``` Save the cart `id` -- you will use it for every subsequent call. The `handoff_mode` is `null` until you set it in Step 5. > **Note:** Every mutating cart operation requires an `Idempotency-Key` header. See the [Idempotency](#idempotency) section for details. ## Step 2: Browse the Menu Before adding items, retrieve the location's menu to get item IDs and modifier group IDs. ```bash curl https://sandbox.api.tote.ai/v1/online-ordering/locations/b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d/menu \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` The menu response contains categories, items, and nested modifier groups. Each item has an `id` that you pass when adding it to the cart. Each modifier group has selection rules (`min_selections`, `max_selections`) that your selections must satisfy. For a detailed walkthrough of the menu structure, see: - [Menu Synchronization guide](/online-ordering/guides/03-menu-sync) -- strategies for keeping menu data current - [Modifiers guide](/online-ordering/guides/05-modifiers) -- understanding nested modifier groups with worked examples ## Step 3: Add Items to the Cart ### Adding a simple item (no modifiers) Add a Bottled Water (no modifier groups) with quantity 2: ```bash curl -X POST https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/items \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001" \ -d '{ "menu_item_id": "f8a9b0c1-d2e3-4567-890a-bcdef1234567", "quantity": 2, "modifier_selections": [], "special_instructions": "Extra cold please" }' ``` **Response (201 Created):** The response returns the full updated cart with the new item and recalculated totals: ```json { "id": "c1d2e3f4-a5b6-7890-cdef-1234567890ab", "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d", "status": "ACTIVE", "items": [ { "id": "e7f8a9b0-c1d2-3456-7890-abcdef123456", "menu_item_id": "f8a9b0c1-d2e3-4567-890a-bcdef1234567", "name": "Bottled Water", "quantity": 2, "base_price": { "amount": 199, "currency": "USD" }, "modifier_total": { "amount": 0, "currency": "USD" }, "item_total": { "amount": 398, "currency": "USD" }, "special_instructions": "Extra cold please", "modifier_selections": [], "age_verification_required": false, "minimum_age": null } ], "age_verification_required": false, "subtotal": { "amount": 398, "currency": "USD" }, "total_tax": { "amount": 33, "currency": "USD" }, "total_discount": { "amount": 0, "currency": "USD" }, "total": { "amount": 431, "currency": "USD" }, "created_at": "2026-01-31T10:00:00Z", "updated_at": "2026-01-31T10:02:00Z" } ``` Notice that `item_total` is `398` cents ($3.98) -- that is `base_price` (199 cents) times `quantity` (2). The cart-level `subtotal`, `total_tax`, and `total` are recomputed with every mutation. The `special_instructions` field is an optional free-text string, max 200 characters. It is passed to the kitchen for item preparation (e.g., "No onions, extra pickles"). If omitted or `null`, the item has no special instructions. ### Adding an item with modifiers Now add a "Build Your Own Sub Sandwich" with nested modifier selections. This demonstrates how to map the menu's modifier group structure to the `CartItemRequest` format. From the menu, this sandwich has three modifier groups: - **Bread Choice** (pick exactly 1) - **Protein** (pick 1 to 2) -- Steak has a nested "Steak Preparation" group - **Toppings** (pick 0 to 5) We will order: Italian Herb & Cheese bread, Steak (Medium preparation), Lettuce, and Tomato. ```bash curl -X POST https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/items \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440002" \ -d '{ "menu_item_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "quantity": 1, "modifier_selections": [ { "modifier_group_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "modifier_id": "a2b3c4d5-e6f7-8901-bcde-f12345678901", "quantity": 1, "nested_selections": [] }, { "modifier_group_id": "b3c4d5e6-f7a8-9012-cdef-123456789012", "modifier_id": "c4d5e6f7-a8b9-0123-def0-234567890123", "quantity": 1, "nested_selections": [ { "modifier_group_id": "d5e6f7a8-b9c0-1234-ef01-345678901234", "modifier_id": "e6f7a8b9-c0d1-2345-f012-456789012345", "quantity": 1, "nested_selections": [] } ] } ] }' ``` **How the selections map to the menu:** | Selection | Modifier Group | Modifier | Level | | --- | --- | --- | --- | | Italian Herb & Cheese | Bread Choice (`f1e2d3c4...`) | `a2b3c4d5...` | 1 | | Steak | Protein (`b3c4d5e6...`) | `c4d5e6f7...` | 1 | | Medium | Steak Preparation (`d5e6f7a8...`) | `e6f7a8b9...` | 2 (nested under Steak) | Notice how `nested_selections` mirrors the menu's modifier group hierarchy. The Steak modifier has a nested "Steak Preparation" group, so we include that selection inside `nested_selections` for the Steak entry. For the full sandwich builder breakdown with all 3 levels, see the [Modifiers guide](/online-ordering/guides/05-modifiers#worked-example-build-your-own-sub-sandwich). **Response (201 Created):** The cart now has two items with recalculated totals: ```json { "id": "c1d2e3f4-a5b6-7890-cdef-1234567890ab", "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d", "status": "ACTIVE", "items": [ { "id": "d4e5f6a7-b8c9-0123-4567-890abcdef012", "menu_item_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Build Your Own Sub Sandwich", "quantity": 1, "base_price": { "amount": 899, "currency": "USD" }, "modifier_total": { "amount": 500, "currency": "USD" }, "item_total": { "amount": 1399, "currency": "USD" }, "modifier_selections": [ { "modifier_group_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "modifier_id": "a2b3c4d5-e6f7-8901-bcde-f12345678901", "quantity": 1, "nested_selections": [] }, { "modifier_group_id": "b3c4d5e6-f7a8-9012-cdef-123456789012", "modifier_id": "c4d5e6f7-a8b9-0123-def0-234567890123", "quantity": 1, "nested_selections": [ { "modifier_group_id": "d5e6f7a8-b9c0-1234-ef01-345678901234", "modifier_id": "e6f7a8b9-c0d1-2345-f012-456789012345", "quantity": 1, "nested_selections": [] } ] } ], "age_verification_required": false, "minimum_age": null }, { "id": "e7f8a9b0-c1d2-3456-7890-abcdef123456", "menu_item_id": "f8a9b0c1-d2e3-4567-890a-bcdef1234567", "name": "Bottled Water", "quantity": 2, "base_price": { "amount": 199, "currency": "USD" }, "modifier_total": { "amount": 0, "currency": "USD" }, "item_total": { "amount": 398, "currency": "USD" }, "modifier_selections": [], "age_verification_required": false, "minimum_age": null } ], "age_verification_required": false, "subtotal": { "amount": 1797, "currency": "USD" }, "total_tax": { "amount": 148, "currency": "USD" }, "total_discount": { "amount": 0, "currency": "USD" }, "total": { "amount": 1945, "currency": "USD" }, "created_at": "2026-01-31T10:00:00Z", "updated_at": "2026-01-31T10:05:30Z" } ``` The sandwich `modifier_total` is 500 cents ($5.00): Italian Herb & Cheese ($0.75) + Steak ($2.00) + Medium ($0.00) = $2.75... wait -- where does $5.00 come from? The modifier total includes the Toppings group selections and other adjustments computed server-side. The key point: **always trust the server-computed totals**, not client-side arithmetic. ### Updating and Removing Items **Update an item** (change quantity or modifiers). Updates are full replacements -- provide the complete `CartItemRequest`: ```bash curl -X PUT https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/items/e7f8a9b0-c1d2-3456-7890-abcdef123456 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440003" \ -d '{ "menu_item_id": "f8a9b0c1-d2e3-4567-890a-bcdef1234567", "quantity": 3, "modifier_selections": [] }' ``` This changes the Bottled Water quantity from 2 to 3. The response returns the full updated cart with recalculated totals. **Remove an item:** ```bash curl -X DELETE https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/items/e7f8a9b0-c1d2-3456-7890-abcdef123456 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440004" ``` The DELETE response (200 OK) returns the updated cart without the removed item. > **Note:** `updateCartItem` is a full replacement (PUT), not a partial update. Always provide the complete `menu_item_id`, `quantity`, and `modifier_selections` -- even if only changing the quantity. ## Step 4: Calculate Prices Before showing the customer a total and proceeding to checkout, call the price calculation endpoint. This returns an itemized breakdown with per-line-item detail, tax, discounts, and the grand total. ```bash curl -X POST https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/calculate \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response (200 OK):** ```json { "cart_id": "c1d2e3f4-a5b6-7890-cdef-1234567890ab", "currency": "USD", "line_items": [ { "cart_item_id": "d4e5f6a7-b8c9-0123-4567-890abcdef012", "menu_item_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Build Your Own Sub Sandwich", "quantity": 1, "base_price": { "amount": 899, "currency": "USD" }, "modifier_total": { "amount": 500, "currency": "USD" }, "discounts": [], "item_subtotal": { "amount": 1399, "currency": "USD" }, "item_tax": { "amount": 115, "currency": "USD" }, "item_total": { "amount": 1514, "currency": "USD" } }, { "cart_item_id": "e7f8a9b0-c1d2-3456-7890-abcdef123456", "menu_item_id": "f8a9b0c1-d2e3-4567-890a-bcdef1234567", "name": "Bottled Water", "quantity": 2, "base_price": { "amount": 199, "currency": "USD" }, "modifier_total": { "amount": 0, "currency": "USD" }, "discounts": [], "item_subtotal": { "amount": 398, "currency": "USD" }, "item_tax": { "amount": 33, "currency": "USD" }, "item_total": { "amount": 431, "currency": "USD" } } ], "discounts": [], "fees": [], "subtotal": { "amount": 1797, "currency": "USD" }, "total_tax": { "amount": 148, "currency": "USD" }, "total_fees": { "amount": 0, "currency": "USD" }, "total_discount": { "amount": 0, "currency": "USD" }, "taxable_amount": { "amount": 1797, "currency": "USD" }, "total": { "amount": 1945, "currency": "USD" }, "age_verification_required": false, "calculated_at": "2026-01-31T10:06:00Z" } ``` ### Understanding the price breakdown **Per-line-item fields:** | Field | Description | | --- | --- | | `base_price` | The menu item's base price (before modifiers). | | `modifier_total` | Sum of all selected modifier price adjustments. | | `discounts` | Array of discounts applied to this item (name, type, amount). | | `item_subtotal` | `base_price + modifier_total - item discounts`, multiplied by `quantity`. | | `item_tax` | Tax for this line item. | | `item_total` | `item_subtotal + item_tax`. This is what the customer pays for this item. | **Cart-level totals:** | Field | Description | | --- | --- | | `subtotal` | Sum of all `item_subtotal` values (item-level discounts already deducted). | | `total_tax` | Total tax across all items. | | `total_fees` | Sum of all fee amounts (delivery, service, bag, small order surcharges). | | `total_discount` | Cart-level discounts only. Item-level discounts are already reflected in each line item's `item_subtotal`. | | `taxable_amount` | The basis on which tax was computed. Exposed for transparency. | | `total` | Grand total: `subtotal + total_tax + total_fees - total_discount`. This is the amount the customer pays. | All monetary values are in **integer cents**. `1945` means $19.45. > **Important:** Prices are computed at request time and may change if the menu or tax configuration is updated. Always call `calculate` immediately before checkout to show the customer the most accurate total. ### When discounts apply If the cart qualifies for discounts, the `discounts` arrays will contain entries like: ```json { "name": "Happy Hour 10% Off", "type": "PERCENTAGE", "value": "10.00", "amount": { "amount": 140, "currency": "USD" } } ``` Discounts can appear at both the line-item level (inside each `line_items[].discounts`) and the cart level (in the top-level `discounts` array). The `total_discount` field shows the cart-level discount total. The cart response also includes a `fees` array and `total_fees` field. Display fees as separate line items in your cart summary (e.g., "Delivery Fee: $3.99", "Service Fee: $1.50"). Fees are recalculated on every cart modification. ## Step 5: Set Handoff Mode Before checkout, tell the API how the customer wants to receive their order. Handoff mode is set on the cart via a dedicated endpoint and can be changed any time while the cart is ACTIVE. ### Handoff mode options | Mode | Required Fields | Use Case | | --- | --- | --- | | `PICKUP` | None (optional `pickup_time`) | Customer picks up at the store counter | | `CURBSIDE` | `vehicle_make`, `vehicle_model`, `vehicle_color` | Staff brings the order to the customer's vehicle | | `DELIVERY` | `delivery_address` (street, city, state, postal_code) | Order is delivered to the customer | | `KIOSK` | None (optional `kiosk_id`) | Self-service terminal order within the store | ### Setting curbside handoff ```bash curl -X PUT https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/handoff \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440005" \ -d '{ "mode": "CURBSIDE", "vehicle_make": "Toyota", "vehicle_model": "Camry", "vehicle_color": "Silver" }' ``` **Response (200 OK):** The response returns the full cart with the handoff mode stored: ```json { "id": "c1d2e3f4-a5b6-7890-cdef-1234567890ab", "location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d", "status": "ACTIVE", "items": ["..."], "handoff_mode": { "mode": "CURBSIDE", "vehicle_make": "Toyota", "vehicle_model": "Camry", "vehicle_color": "Silver" }, "subtotal": { "amount": 1797, "currency": "USD" }, "total_tax": { "amount": 148, "currency": "USD" }, "total_discount": { "amount": 0, "currency": "USD" }, "total": { "amount": 1945, "currency": "USD" }, "created_at": "2026-01-31T10:00:00Z", "updated_at": "2026-01-31T10:06:30Z" } ``` ### Switching to delivery The customer changed their mind and wants delivery instead. Call the same endpoint with the new mode: ```bash curl -X PUT https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/handoff \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440006" \ -d '{ "mode": "DELIVERY", "delivery_address": { "street": "123 Main St, Apt 4B", "city": "Austin", "state": "TX", "postal_code": "78701" }, "delivery_instructions": "Leave at the front door" }' ``` The cart's `handoff_mode` is now updated to DELIVERY. You can call this endpoint as many times as needed -- the latest call wins. > **Minimum order amounts:** Each location may define minimum order amounts per handoff mode (available on the Location resource via `GET /locations/{id}` in the `minimum_order_amounts` field). If the cart total is below the minimum for the selected handoff mode, the server auto-applies a `SMALL_ORDER` fee covering the shortfall rather than blocking the order. Your integration does not need to enforce minimums client-side -- just display whatever fees the server returns. ## Step 6: Checkout Checkout converts the ACTIVE cart into an order. By default, checkout uses the handoff mode already stored on the cart (set in Step 5). ```bash curl -X POST https://sandbox.api.tote.ai/v1/online-ordering/carts/c1d2e3f4-a5b6-7890-cdef-1234567890ab/checkout \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440007" \ -d '{ "expected_total": 1945 }' ``` Notice that `handoff_mode` is **not** in the request body. The API uses the cart's stored handoff mode (DELIVERY, from Step 5). You can optionally include `handoff_mode` in the checkout request to override the stored value -- useful if the customer makes a last-second change. If no handoff mode is stored on the cart **and** none is provided in the request body, the server returns 422. ### Optimistic concurrency with expected_total The `expected_total` field (in cents) is optional but recommended. Pass the `total` from your last `calculate` call. If prices changed between calculate and checkout (e.g., menu was updated), the server returns **409 Conflict** instead of charging a different amount. This prevents surprise charges. If you omit `expected_total`, checkout proceeds with the current price regardless of changes. **Response (201 Created):** ```json { "order_id": "f9a8b7c6-d5e4-3210-fedc-ba9876543210", "cart_id": "c1d2e3f4-a5b6-7890-cdef-1234567890ab", "status": "PENDING", "handoff_mode": "CURBSIDE", "subtotal": { "amount": 1797, "currency": "USD" }, "total_tax": { "amount": 148, "currency": "USD" }, "total_discount": { "amount": 0, "currency": "USD" }, "total": { "amount": 1945, "currency": "USD" }, "age_verification_required": false, "age_verification_notice": null, "created_at": "2026-01-31T10:07:00Z" } ``` After checkout: - The cart's status changes to `CHECKED_OUT` and can no longer be modified. - The order is created with status `PENDING`, awaiting payment. - The `order_id` is the primary identifier for all subsequent operations. > **Coming in Phase 3:** Payment submission and order tracking are covered in the Checkout & Payments guide. The order stays in `PENDING` status until payment is submitted. ## Age-Restricted Items Some menu items (tobacco, alcohol) have `age_verification_required: true` and a `minimum_age` field in the menu response. When these items are added to the cart, the cart's `age_verification_required` flag becomes `true`. **Key behavior: items are NOT blocked.** The cart and checkout both succeed normally. Age verification happens offline at pickup or delivery -- just like in a physical store where items are scanned first and ID is checked at the register. When the cart contains age-restricted items, the checkout response includes an informational notice: ```json { "order_id": "a1b2c3d4-e5f6-7890-abcd-ef0987654321", "cart_id": "d2e3f4a5-b6c7-8901-2345-678901abcdef", "status": "PENDING", "handoff_mode": "PICKUP", "subtotal": { "amount": 2499, "currency": "USD" }, "total_tax": { "amount": 206, "currency": "USD" }, "total_discount": { "amount": 0, "currency": "USD" }, "total": { "amount": 2705, "currency": "USD" }, "age_verification_required": true, "age_verification_notice": "This order contains age-restricted items (Premium Cigars). Valid government-issued photo ID showing age 21 or older will be required at pickup.", "created_at": "2026-01-31T10:10:00Z" } ``` **What to display in your UI:** - Show the `age_verification_notice` string to the customer before or after checkout so they know to bring ID. - For delivery orders, the driver will check ID at handoff. Details will be covered in the Handoff Modes guide (coming in Phase 4). ## Idempotency All mutating cart operations require an `Idempotency-Key` header to safely handle retries. This prevents duplicate carts, double-added items, or duplicate checkouts when a network error causes the client to retry. ### Which operations require it | Operation | Method | Idempotency-Key | | --- | --- | --- | | Create cart | POST /carts | Required | | Add item | POST /carts/{id}/items | Required | | Update item | PUT /carts/{id}/items/{id} | Required | | Remove item | DELETE /carts/{id}/items/{id} | Required | | Set handoff | PUT /carts/{id}/handoff | Required | | Checkout | POST /carts/{id}/checkout | Required | | Calculate prices | POST /carts/{id}/calculate | Not required (read-only) | | Get cart | GET /carts/{id} | Not required (read-only) | | Delete cart | DELETE /carts/{id} | Required | ### How it works - **Key format:** UUID v4 (e.g., `550e8400-e29b-41d4-a716-446655440000`) - **Cache duration:** Successful responses are cached for 24 hours. Sending the same key within 24 hours returns the cached response. - **Error responses are NOT cached.** If a request fails (4xx, 5xx), you can safely retry with the same key -- the request will be re-executed. - **Payload mismatch:** If you reuse a key with a different request body, the server returns **409 Conflict**. ### Python retry wrapper with idempotency ```python import uuid import requests import time BASE_URL = "https://sandbox.api.tote.ai/v1/online-ordering" def idempotent_request(method, path, access_token, json=None, max_retries=3): """ Make an API request with automatic idempotency key and retry logic. Generates a single idempotency key and reuses it across retries, ensuring the operation executes at most once even if retried. """ url = f"{BASE_URL}{path}" key = str(uuid.uuid4()) headers = { "Authorization": f"Bearer {access_token}", "Idempotency-Key": key, } for attempt in range(max_retries): response = requests.request(method, url, json=json, headers=headers) if response.status_code == 429: # Rate limited -- wait and retry with the SAME key retry_after = int(response.headers.get("Retry-After", 5)) time.sleep(retry_after) continue if response.status_code >= 500: # Server error -- wait and retry with the SAME key time.sleep(2 ** attempt) continue return response raise Exception(f"Max retries exceeded for {method} {path}") # Usage: create a cart response = idempotent_request( "POST", "/carts", access_token="YOUR_ACCESS_TOKEN", json={"location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d"}, ) cart = response.json() print(f"Cart created: {cart['id']}") ``` The key insight: generate the idempotency key **once** before the retry loop, then reuse it on every attempt. This way, if the first request succeeded but the response was lost, the retry returns the cached success response instead of creating a duplicate. ## Error Handling ### Common error scenarios | Status | Error Code | Cause | Resolution | | --- | --- | --- | --- | | 404 | `NOT_FOUND_ERROR` | Cart not found | Verify the cart ID. The cart may have been deleted (abandoned). | | 422 | `INVALID_REQUEST_ERROR` | Modifier validation failed | Check that selections satisfy `min_selections` and `max_selections` for each group. See the [Modifiers guide](/online-ordering/guides/05-modifiers#validation-rules). | | 422 | `INVALID_REQUEST_ERROR` | Empty cart at checkout | Add at least one item before checking out. | | 422 | `INVALID_REQUEST_ERROR` | Item unavailable | A menu item in the cart is no longer available. Remove it or replace it. | | 422 | `INVALID_REQUEST_ERROR` | Missing handoff mode | Set a handoff mode via `PUT /carts/{id}/handoff` or include one in the checkout request. | | 409 | `CONFLICT_ERROR` | Cart already checked out | The cart's status is CHECKED_OUT. Use the returned `order_id` from the original checkout. | | 409 | `CONFLICT_ERROR` | `expected_total` mismatch | Prices changed since your last `calculate` call. Re-calculate and show the updated total to the customer. | | 409 | `CONFLICT_ERROR` | Idempotency key reused with different payload | Use a new UUID v4 for the new request. | ### Error response format All errors follow the standard envelope: ```json { "error": { "code": "INVALID_REQUEST_ERROR", "message": "Modifier validation failed.", "detail": "Bread Choice requires exactly 1 selection, but 0 were provided.", "request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "field": "modifier_selections[0]" } } ``` Include the `request_id` when contacting support for faster debugging. ## Complete Flow Example This Python script demonstrates the end-to-end cart lifecycle with error handling and idempotency: ```python import uuid import requests import time import sys BASE_URL = "https://sandbox.api.tote.ai/v1/online-ordering" ACCESS_TOKEN = "YOUR_ACCESS_TOKEN" # From POST /auth/token LOCATION_ID = "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d" def api_request(method, path, json=None, idempotent=True): """Make an API request with optional idempotency.""" headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"} if idempotent: headers["Idempotency-Key"] = str(uuid.uuid4()) response = requests.request( method, f"{BASE_URL}{path}", json=json, headers=headers ) if response.status_code >= 400: error = response.json().get("error", {}) print(f"Error {response.status_code}: {error.get('message')}") print(f" Detail: {error.get('detail')}") sys.exit(1) return response.json() # Step 1: Create a cart print("Creating cart...") cart = api_request("POST", "/carts", json={"location_id": LOCATION_ID}) cart_id = cart["id"] print(f" Cart ID: {cart_id}") print(f" Status: {cart['status']}") # Step 2: Add a simple item (Bottled Water, no modifiers) print("\nAdding Bottled Water (qty 2)...") cart = api_request( "POST", f"/carts/{cart_id}/items", json={ "menu_item_id": "f8a9b0c1-d2e3-4567-890a-bcdef1234567", "quantity": 2, "modifier_selections": [], }, ) print(f" Items in cart: {len(cart['items'])}") print(f" Subtotal: ${cart['subtotal']['amount'] / 100:.2f}") # Step 3: Add a sandwich with modifiers print("\nAdding Sub Sandwich (Italian Herb & Cheese, Steak, Medium)...") cart = api_request( "POST", f"/carts/{cart_id}/items", json={ "menu_item_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "quantity": 1, "modifier_selections": [ { "modifier_group_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "modifier_id": "a2b3c4d5-e6f7-8901-bcde-f12345678901", "quantity": 1, "nested_selections": [], }, { "modifier_group_id": "b3c4d5e6-f7a8-9012-cdef-123456789012", "modifier_id": "c4d5e6f7-a8b9-0123-def0-234567890123", "quantity": 1, "nested_selections": [ { "modifier_group_id": "d5e6f7a8-b9c0-1234-ef01-345678901234", "modifier_id": "e6f7a8b9-c0d1-2345-f012-456789012345", "quantity": 1, "nested_selections": [], } ], }, ], }, ) print(f" Items in cart: {len(cart['items'])}") print(f" Subtotal: ${cart['subtotal']['amount'] / 100:.2f}") # Step 4: Calculate prices print("\nCalculating prices...") prices = api_request("POST", f"/carts/{cart_id}/calculate", idempotent=False) print(f" Subtotal: ${prices['subtotal']['amount'] / 100:.2f}") print(f" Tax: ${prices['total_tax']['amount'] / 100:.2f}") print(f" Total: ${prices['total']['amount'] / 100:.2f}") for item in prices["line_items"]: print(f" {item['name']}: ${item['item_total']['amount'] / 100:.2f}") # Step 5: Set handoff mode (curbside) print("\nSetting handoff mode (curbside)...") cart = api_request( "PUT", f"/carts/{cart_id}/handoff", json={ "mode": "CURBSIDE", "vehicle_make": "Toyota", "vehicle_model": "Camry", "vehicle_color": "Silver", }, ) print(f" Handoff mode: {cart['handoff_mode']['mode']}") # Step 6: Checkout (uses stored handoff mode) print("\nChecking out...") order = api_request( "POST", f"/carts/{cart_id}/checkout", json={"expected_total": prices["total"]["amount"]}, ) print(f" Order ID: {order['order_id']}") print(f" Status: {order['status']}") print(f" Total: ${order['total']['amount'] / 100:.2f}") if order.get("age_verification_required"): print(f" Age verification: {order['age_verification_notice']}") print("\nDone! Order created successfully.") ``` ## What's Next - [Modifiers guide](/online-ordering/guides/05-modifiers) -- Deep dive into nested modifier groups, validation rules, and UI rendering - Checkout & Payments guide (coming in Phase 3) -- Payment submission for pending orders - Order Tracking guide (coming in Phase 3) -- Monitoring order fulfillment status - Webhooks guide (coming in Phase 4) -- Real-time order status updates via push notifications