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_centsaddon_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:
- Refund the customer for the add-on amount via the refund flow
- Optionally delete the
reservation_addonsrow 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:
- Insert a
reservation_addonsrow directly via SQL - 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 >= '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.