This guide covers how discounts and fees work in the Tote Online Ordering API, including promo codes, automatic discounts, fee categories, minimum order enforcement, customer identity for member pricing, and handling discount changes during checkout.
By the end, you will understand every discount type, how fees are structured and displayed, how to apply and remove promo codes, how customer identity unlocks member pricing, and how to recover gracefully when prices change between calculation and checkout.
All discounts appear as DiscountLineItem objects in the PriceCalculation response. Discounts can be:
- Cart-level -- in the top-level
discountsarray (e.g., "10% off your order") - Item-level -- in each line item's
discountsarray (e.g., "$1 off this coffee")
Each discount has a source indicating where it came from and an application_scope indicating when it is applied in the pricing formula.
All current discount sources use PRE_TAX scope, meaning they reduce the taxable amount before tax calculation.
| Discount Source | Application Scope | When Applied | Example |
|---|---|---|---|
AUTOMATIC | PRE_TAX | During /calculate | Happy Hour 10% Off |
PROMO_CODE | PRE_TAX | After code applied | SUMMER25 -- $2 off subs |
LOYALTY_REWARD | PRE_TAX | During /calculate | Member 5% discount |
MANUAL | PRE_TAX | POS staff override | Manager comp |
Note: All current discount sources use
PRE_TAXscope, reducing the taxable amount before tax calculation. See the Loyalty-as-Discount section for a worked example showing the tax savings.
Promo codes follow a four-step lifecycle: validate (optional), apply, see the discount in price calculation, and remove if needed.
Before applying a code, you can check whether it is valid for the current cart. This is useful for inline validation as the user types.
GET /carts/{cart_id}/promo-codes/validate?code=SUMMER25Response (200 OK):
{
"code": "SUMMER25",
"valid": true,
"discount_preview": {
"estimated_discount": { "amount": 200, "currency": "USD" },
"description": "$2 off your order",
"applicable_items": []
},
"rejection_reason": null,
"rejection_message": null
}An empty applicable_items array means the discount applies to the entire cart (cart-level discount).
Note: The discount preview is an estimate. The actual discount is computed when the code is applied and prices are calculated.
Apply a promo code to the cart. The code is case-insensitive and always returned as uppercase.
POST /carts/{cart_id}/promo-codes{
"code": "summer25"
}Response (200 OK): The updated Cart with the promo code in the promo_codes array.
{
"id": "c1d2e3f4-a5b6-7890-cdef-1234567890ab",
"location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d",
"status": "ACTIVE",
"items": ["..."],
"promo_codes": [
{
"code": "SUMMER25",
"status": "ACTIVE",
"discount_preview": {
"estimated_discount": { "amount": 200, "currency": "USD" },
"description": "$2 off your order"
},
"applied_at": "2026-02-01T14:30:00Z"
}
],
"subtotal": { "amount": 1000, "currency": "USD" },
"total_tax": { "amount": 80, "currency": "USD" },
"total_discount": { "amount": 200, "currency": "USD" },
"total": { "amount": 880, "currency": "USD" },
"created_at": "2026-02-01T14:00:00Z",
"updated_at": "2026-02-01T14:30:00Z"
}One code at a time. Only one promo code can be active on a cart. Attempting to apply a second code returns a
409 Conflictwithrejection_reason: ALREADY_APPLIED. Remove the current code first, then apply the new one.
After applying a promo code, call /calculate to see the full price breakdown including the promo discount.
POST /carts/{cart_id}/calculateThe promo discount appears as a DiscountLineItem in the discounts array:
{
"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": "Coffee",
"quantity": 1,
"base_price": { "amount": 1000, "currency": "USD" },
"modifier_total": { "amount": 0, "currency": "USD" },
"discounts": [],
"item_subtotal": { "amount": 1000, "currency": "USD" },
"item_tax": { "amount": 80, "currency": "USD" },
"item_total": { "amount": 1080, "currency": "USD" }
}
],
"discounts": [
{
"id": "disc-001",
"name": "$2 Off Order",
"type": "FIXED",
"value": null,
"amount": { "amount": 200, "currency": "USD" },
"source": "PROMO_CODE",
"application_scope": "PRE_TAX"
}
],
"promo_codes": [
{
"code": "SUMMER25",
"status": "ACTIVE",
"discount_preview": {
"estimated_discount": { "amount": 200, "currency": "USD" },
"description": "$2 off your order"
},
"applied_at": "2026-02-01T14:30:00Z"
}
],
"member_pricing_applied": false,
"fees": [],
"subtotal": { "amount": 1000, "currency": "USD" },
"total_tax": { "amount": 80, "currency": "USD" },
"total_discount": { "amount": 200, "currency": "USD" },
"total_fees": { "amount": 0, "currency": "USD" },
"taxable_amount": { "amount": 800, "currency": "USD" },
"total": { "amount": 880, "currency": "USD" },
"calculated_at": "2026-02-01T14:30:05Z"
}Notice that taxable_amount is 800 ($8.00), not 1000 ($10.00). The $2.00 pre-tax discount reduced the taxable amount, so tax is calculated on $8.00.
Remove a promo code from the cart.
DELETE /carts/{cart_id}/promo-codes/SUMMER25Response (200 OK): The updated Cart with the promo code removed and totals recalculated.
When a promo code cannot be applied, the validation response (or the apply endpoint) returns a rejection_reason. Use this to show helpful messages to the customer.
| Rejection Reason | Description | Suggested UX |
|---|---|---|
INVALID_CODE | Code not recognized | "This code is not valid" |
EXPIRED | Past expiration date | "This code has expired" |
ALREADY_USED | Single-use, already redeemed | "This code has already been used" |
MINIMUM_NOT_MET | Cart below minimum amount | "Add $X more to use this code" |
NOT_APPLICABLE | No eligible items in cart | "This code doesn't apply to items in your cart" |
ALREADY_APPLIED | Another code already active | "Remove your current code first" |
UX tip: Show a promo code input field at checkout, similar to DoorDash or Uber Eats. Validate inline as the user types or on submit. Show the discount preview before the user confirms. This gives immediate feedback and avoids surprises at the final total.
Automatic discounts are applied by the server during /calculate based on rules configured in the Tote dashboard. Partners cannot apply or remove automatic discounts through the API -- they are fully server-controlled.
| Category | Example | How It Works |
|---|---|---|
| Time-based | Happy hour pricing, weekend specials | Server checks current time against configured windows |
| Spend thresholds | "Spend $25, get $5 off" | Server evaluates cart subtotal against threshold |
| Item combos | "Buy 2 coffees, get 1 free" | Server detects qualifying item combinations |
| Location-specific | Store grand opening discounts | Discount tied to a specific location ID |
Automatic discounts appear in the PriceCalculation.discounts array with source: AUTOMATIC:
{
"id": "disc-auto-001",
"name": "Happy Hour 10% Off",
"type": "PERCENTAGE",
"value": "10.00",
"amount": { "amount": 100, "currency": "USD" },
"source": "AUTOMATIC",
"application_scope": "PRE_TAX"
}Your integration's job is to display automatic discounts to the customer with their name and amount. Do not try to remove automatic discounts -- there is no endpoint for this.
Non-discountable items: Items with
non_discountable: trueon theMenuItemare excluded from all automatic discount calculations. See the C-Store Features guide for more on item-level flags.
Fees are separate from item pricing and discounts. They appear in the fees array on PriceCalculation, Cart, and Order responses. Each fee is a FeeLineItem with a fee_type categorizing the charge, a name and label for display, an amount in cents, and a taxable flag indicating whether tax applies.
| Fee Type | Description | Example |
|---|---|---|
DELIVERY | Fee for delivering the order to the customer | "Delivery Fee: $3.99" |
SERVICE | Platform or convenience service fee | "Service Fee: $1.50" |
BAG | Per-bag charge required by local regulations | "Bag Fee: $0.10" |
SMALL_ORDER | Surcharge when cart subtotal is below the location's minimum | "Small Order Fee: $2.00" |
OTHER | Fees that do not fit the above categories | "Processing Fee: $0.50" |
Tolerant Reader: Treat
fee_typeas an open enum. New fee types may be added in future API versions. If your integration encounters an unrecognized fee type, display it using thenameandamountfields.
Each location MAY define minimum order amounts per handoff mode. These are available on the Location resource in the minimum_order_amounts field, keyed by handoff mode (delivery, pickup, curbside, kiosk).
If the cart subtotal is below the minimum for the selected handoff mode, the server automatically applies a SMALL_ORDER fee covering the shortfall. For example, if the delivery minimum is $15.00 and the cart subtotal is $12.00, a SMALL_ORDER fee of $3.00 is applied.
Partners do not need to enforce minimums client-side. Display whatever fees the server returns -- the server handles all minimum order logic. You can optionally read minimum_order_amounts from the Location resource to show the minimum before the customer starts shopping.
See the Cart Lifecycle guide for how fees appear in the cart summary response.
Render each fee from the fees array as a separate line item in your order summary:
- Use the
labelfield for compact summary views (e.g., "Delivery") - Use the
namefield for detailed receipt views (e.g., "Delivery Fee") - The
taxableboolean indicates whether tax applies to this fee -- the server handles the tax calculation, so you do not need to compute it - Fee amounts are always positive (they add to the total)
Subtotal: $12.00
Delivery Fee: $3.99
Small Order Fee: $3.00
Tax: $1.50
Discount: -$2.00
─────────────────────────
Total: $18.49Fees are added after discounts in the pricing formula:
total = subtotal + total_tax + total_fees - total_discounttotal_feesis the sum of all fee amounts in thefeesarray- Taxable fees are included in
taxable_amountfor tax calculation -- the server determines which fees are taxable based on local rules - A
SMALL_ORDERfee may or may not be taxable depending on local tax regulations
Note: Fees do not interact with discounts. A promo code does not reduce fees, and fees do not affect discount eligibility. They are independent adjustments to the total.
Setting a customer_id on the cart enables member-specific pricing. This is how loyalty members see their discounted prices.
You can set customer_id at cart creation:
POST /carts
{
"location_id": "b5a7c8d9-e0f1-4a2b-8c3d-4e5f6a7b8c9d",
"customer_id": "CUST-12345"
}Or update it later via PATCH (the "login-after-browsing" flow, where a customer adds items first and logs in at checkout):
PATCH /carts/{cart_id}{
"customer_id": "CUST-12345"
}Response (200 OK): The updated Cart with customer_id set.
When customer_id is set, the server applies member-specific pricing on the next call to /calculate. The member_pricing_applied field in the PriceCalculation response indicates whether the server recognized the customer and applied member prices.
{
"member_pricing_applied": true,
"discounts": [
{
"id": "disc-loyalty-001",
"name": "Member 5% Discount",
"type": "PERCENTAGE",
"value": "5.00",
"amount": { "amount": 50, "currency": "USD" },
"source": "LOYALTY_REWARD",
"application_scope": "PRE_TAX"
}
]
}Use member_pricing_applied to show a badge or message in your UI (e.g., "Member pricing applied").
Set customer_id to null to revert to anonymous pricing:
PATCH /carts/{cart_id}
{
"customer_id": null
}Important: After changing
customer_id, always re-calculate prices. Previous pricing may be stale because member discounts are only evaluated during/calculate.
Retrieve orders for a specific customer:
GET /orders?customer_id=CUST-12345The customer_id filter is an exact, case-sensitive match.
"Loyalty-as-discount" is the model used by Tote: loyalty rewards are applied as pre-tax discounts during /calculate. Pre-tax discounts reduce the taxable amount, which saves the customer money on tax compared to applying the same dollar amount after tax.
Consider a single item with a $2 promo code discount at 10% tax:
Item: Coffee $10.00
Promo: "SAVE2" ($2 off) -$2.00
source: PROMO_CODE
application_scope: PRE_TAX
------
Subtotal (after discount): $8.00
Tax (10% of $8.00): $0.80
------
Total: $8.80
Without the pre-tax discount, the total would be:
$10.00 + $1.00 tax - $2.00 = $9.00
Pre-tax discount saves the customer $0.20 in tax.Here is the full PriceCalculation JSON for the worked example above:
{
"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": "Coffee",
"quantity": 1,
"base_price": { "amount": 1000, "currency": "USD" },
"modifier_total": { "amount": 0, "currency": "USD" },
"discounts": [],
"item_subtotal": { "amount": 1000, "currency": "USD" },
"item_tax": { "amount": 80, "currency": "USD" },
"item_total": { "amount": 1080, "currency": "USD" }
}
],
"discounts": [
{
"id": "disc-promo-001",
"name": "SAVE2 - $2 Off",
"type": "FIXED",
"value": null,
"amount": { "amount": 200, "currency": "USD" },
"source": "PROMO_CODE",
"application_scope": "PRE_TAX"
}
],
"promo_codes": [
{
"code": "SAVE2",
"status": "ACTIVE",
"discount_preview": {
"estimated_discount": { "amount": 200, "currency": "USD" },
"description": "$2 off your order"
},
"applied_at": "2026-02-01T15:00:00Z"
}
],
"member_pricing_applied": false,
"fees": [],
"subtotal": { "amount": 1000, "currency": "USD" },
"total_tax": { "amount": 80, "currency": "USD" },
"total_discount": { "amount": 200, "currency": "USD" },
"total_fees": { "amount": 0, "currency": "USD" },
"taxable_amount": { "amount": 800, "currency": "USD" },
"total": { "amount": 880, "currency": "USD" },
"calculated_at": "2026-02-01T15:00:05Z"
}Key fields to verify:
subtotal: 1000 (the item price before cart-level discounts)total_discount: 200 (the $2 promo code discount)taxable_amount: 800 (subtotal minus pre-tax discounts: 1000 - 200)total_tax: 80 (10% of taxable_amount)total: 880 (subtotal + total_tax - total_discount: 1000 + 80 - 200)
All discount sources currently use
PRE_TAXscope. This is the recommended default for loyalty programs in convenience stores, where reducing the taxable amount provides a clear, tangible benefit to the customer.
Discounts may change between /calculate and /checkout. Several things can cause the calculated total to become stale:
- A promo code may expire (time-limited promotions)
- An automatic discount may no longer apply (e.g., happy hour ended)
- Item prices may change (menu update)
- Items may become unavailable (out of stock)
- Fees may change (delivery fee recalculation)
Checkout uses expected_total to detect staleness. If the total has changed since your last calculation, checkout returns 409 Conflict with change_reasons explaining what changed.
409 Conflict response example:
{
"error": {
"code": "CONFLICT_ERROR",
"message": "Price has changed since your last calculation.",
"detail": "The expected total of 880 does not match the current total of 1080.",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"field": "expected_total",
"change_reasons": ["PROMO_EXPIRED"]
}
}The change_reasons array tells you what changed. Possible values:
| Reason | What Changed |
|---|---|
PROMO_EXPIRED | A promo code expired or was invalidated |
DISCOUNT_CHANGED | An automatic discount changed or was removed |
ITEM_PRICE_CHANGED | One or more item prices were updated |
ITEM_UNAVAILABLE | One or more items are no longer available |
FEE_CHANGED | A fee amount changed (delivery, service, etc.) |
When you receive a 409, re-calculate prices, show the updated total to the customer, and retry checkout with the new total.
Follow these guidelines to minimize staleness:
- Always re-calculate when showing the checkout summary screen
- Re-calculate after any cart modification (add/remove items, apply/remove promo code, change
customer_id) - Do not cache
PriceCalculationresults for extended periods -- prices are computed at request time and may change
Tip: The
calculated_attimestamp on thePriceCalculationresponse tells you when the prices were last computed. If the timestamp is more than a few minutes old, re-calculate before proceeding to checkout.
- Use the Integration Checklist to verify your promo code handling in the sandbox, including applying, removing, and testing rejection scenarios.
- Review the C-Store Features guide for
non_discountableitems and tender restrictions that interact with discount calculations. - See the Error Code Reference for all v2 error codes, including promo code validation errors, customer ID validation errors, and fee-related error scenarios.