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.
Before following this guide, make sure you have:
- An access token -- See the Getting Started guide and Authentication guide for how to obtain and manage tokens.
- A location ID -- Use
GET /locationsto find a store. See Getting Started. - Familiarity with the menu structure -- Menu items contain modifier groups that define customization options. See the Modifiers guide 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-4e5f6a7b8c9dCreate a new cart for a specific location. The cart starts empty with ACTIVE status and zero totals.
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):
{
"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-Keyheader. See the Idempotency section for details.
Before adding items, retrieve the location's menu to get item IDs and modifier group IDs.
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 -- strategies for keeping menu data current
- Modifiers guide -- understanding nested modifier groups with worked examples
Add a Bottled Water (no modifier groups) with quantity 2:
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:
{
"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.
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.
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.
Response (201 Created):
The cart now has two items with recalculated totals:
{
"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.
Update an item (change quantity or modifiers). Updates are full replacements -- provide the complete CartItemRequest:
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:
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:
updateCartItemis a full replacement (PUT), not a partial update. Always provide the completemenu_item_id,quantity, andmodifier_selections-- even if only changing the quantity.
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.
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):
{
"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"
}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
calculateimmediately before checkout to show the customer the most accurate total.
If the cart qualifies for discounts, the discounts arrays will contain entries like:
{
"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.
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.
| 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 |
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:
{
"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"
}The customer changed their mind and wants delivery instead. Call the same endpoint with the new mode:
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 theminimum_order_amountsfield). If the cart total is below the minimum for the selected handoff mode, the server auto-applies aSMALL_ORDERfee 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.
Checkout converts the ACTIVE cart into an order. By default, checkout uses the handoff mode already stored on the cart (set in Step 5).
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.
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):
{
"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_OUTand can no longer be modified. - The order is created with status
PENDING, awaiting payment. - The
order_idis 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
PENDINGstatus until payment is submitted.
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:
{
"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_noticestring 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).
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.
| 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 |
- 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.
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.
| 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. |
| 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. |
All errors follow the standard envelope:
{
"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.
This Python script demonstrates the end-to-end cart lifecycle with error handling and idempotency:
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.")- Modifiers guide -- 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