beginner
shop-rentals
add-ons
upsell

Selling Add-ons at Checkout

How add-ons appear in the walk-in wizard and public booking — bookable inventory caps, snapshot semantics, and reservation total adjustment

Levy Fleets TeamMay 7, 20265 min read

Once you've configured add-ons, they automatically appear in both the walk-in wizard and the public booking page. Customers can add them in any quantity (subject to inventory caps) and the total adds to the reservation's bill.

Where customers see them

In the walk-in wizard

Step 3 of the walk-in flow. Each active add-on shows with name, category badge, price, and quantity buttons. Operator-driven — the staff member adds them on behalf of the customer.

On the public booking page

Step 3 (after picking a bike, before customer details). Same +/- quantity controls. Customer-driven — they self-select.

On the operator new-booking flow

/dashboard/shop-rentals/new doesn't have add-on selection in the form yet (planned). Use the walk-in wizard or the API to attach add-ons.

Inventory enforcement

Bookable add-ons (those with is_bookable=true and a finite quantity_total) are subject to availability checks. If you have 10 helmets total and 8 are already booked across overlapping reservations, the 11th helmet attempt fails availability.

The check fires at:

  • Time of selection (UI counts down quantity available)
  • Time of booking submission (final guard)
  • The hold step on public booking — held add-ons subtract from visible inventory just like vehicles

Snapshot semantics

When an add-on is attached to a reservation, the system writes a reservation_addons row with:

  • addon_id (link to catalog)
  • quantity, unit_price_cents, total_cents
  • addon_snapshot — JSON capturing name, category, currency at booking time

The snapshot is what makes receipts durable. If you later rename "Helmet" to "Bicycle Helmet" or raise the price from $5 to $7, the existing reservation's row still shows "Helmet at $5" — the customer's receipt is unchanged.

Reservation total math

When add-ons are added:

addons_total_cents = sum(reservation_addons.total_cents)
new_subtotal_cents = base_cost_cents + addons_total_cents
new_tax_cents = round(new_subtotal_cents * tax_rate / 100)
new_total_cents = new_subtotal_cents + new_tax_cents
new_balance_due = max(0, new_total_cents - amount_paid_cents)

The reservation row is updated atomically. The financial summary on the booking detail shows the addon line items separately:

Base rental:    $40.00
Helmet × 1:      $5.00
U-Lock × 1:      $3.00
Subtotal:       $48.00
Tax (8%):        $3.84
Total:          $51.84

Removing an add-on after booking

The current API doesn't have a "remove add-on" endpoint. To unwind:

  1. Refund the customer for the add-on amount via the refund flow
  2. Optionally delete the reservation_addons row via SQL (preserve for audit) — most shops just leave the row and let the refund handle the math

A "remove add-on" UI is queued.

Adding an add-on after the booking is created

Possible via the API:

POST /api/reservations/[id]/addons   (planned, not yet released)
{ "addon_id": "...", "quantity": 1 }

For now, the workaround is:

  1. Insert a reservation_addons row directly via SQL
  2. Recompute the reservation's totals manually

This is brittle. A native "Add add-on after booking" button on the booking detail page is on the roadmap.

Bookable inventory math under the hood

The availability endpoint (POST /api/shop-rentals/add-ons/availability) calculates:

available = quantity_total
            - sum(reservation_addons.quantity for overlapping bookings,
                  excluding cancelled/no_show/expired/completed)
            - sum(reservation_holds.addon_holds for active holds)

So a helmet that was rented yesterday and returned today doesn't count against today's availability — only currently-active and future overlapping bookings do.

Common patterns

Required helmet on every rental

If your insurance requires a helmet, set the helmet add-on with a high sort_order so it's the first thing customers see. Some shops also bundle the helmet into the base rate (no separate charge) and just set the add-on as $0 — purely for tracking.

Insurance opt-in

Set "Damage Insurance" as an unlimited (not bookable) add-on with a clear name. On the public page, customers see it in step 3 alongside helmets and locks. Conversion rates are typically 30-50% when priced reasonably.

Delivery as an add-on

Don't use the add-on for delivery — use the fulfillment_type='delivery' flow with a delivery_fee_cents field on the reservation. Add-ons should be physical items or services that ship with the bike.

Tour package as an add-on

A "Guided tour" add-on at $50 fits — customers pick the bike + the tour slot together. Mark it as bookable with quantity = number of tour spots to enforce capacity.

Reporting add-on revenue

SELECT
  ras.addon_snapshot->>'name' AS addon_name,
  count(*) AS attached,
  sum(ras.total_cents) / 100.0 AS revenue
FROM reservation_addons ras
JOIN reservations r ON r.id = ras.reservation_uuid
WHERE r.subaccount_id = '<your-subaccount-id>'
  AND r.created_at &gt;= '2026-05-01'
GROUP BY 1
ORDER BY revenue DESC;

Track which add-ons are pulling weight. Underperformers might need a better description, image, or repositioning in the catalog.