When a customer or operator cancels a booking, the system handles three things in one transaction: status update, cancellation fee calculation, and any auto-refund due. This guide covers all three.
Two cancel paths
| Path | Trigger | Who cancels |
|---|---|---|
| Operator cancel | Cancel Booking on booking detail | Staff member at the dashboard |
| Customer cancel | Cancel on the manage-link page | Rider with their /manage/[token] URL |
Both paths funnel through the same backend logic. The only operational
difference is cancelled_by — set to 'admin' for operator cancels,
'customer' for customer self-serve.
How the fee is calculated
The system reads the pricing snapshot that was bound to the reservation at booking time. From the snapshot:
free_cancellation_hours(e.g., 24)cancellation_fee_percent(e.g., 25%)non_refundable_depositflag (optional)
Then:
- Within the free window (cancellation > free_hours before pickup): fee = $0
- Outside the free window: fee =
base_cost_cents * cancellation_fee_percent / 100 - If
non_refundable_deposit=true: fee = max(fee, deposit) — deposit is always retained
The fee is rounded up to the next cent.
What gets auto-refunded
After the fee is calculated, the system computes:
amount_paid_cents
- cancellation_fee_cents
- already_refunded_cents
= refund_due_cents
If refund_due_cents > 0 and the customer has a customer record, the
system automatically issues a wallet refund for that amount through
the same source-of-truth flow as manual refunds.
So a typical flow:
- Customer paid $50 deposit upfront
- Cancellation within free-cancel window → fee = $0
- Auto-refund = $50 to wallet
- Reservation status → cancelled
Or:
- Customer paid $50 deposit upfront, full $200 paid
- Cancellation outside free-cancel → fee = 25% of $200 = $50
- Auto-refund = $200 - $50 = $150 to wallet
- Reservation status → cancelled
What's never auto-refunded
- Card refunds are never automatic — auto-refund always goes to wallet. If the customer wants their card refunded instead, the operator must manually do that after the cancel via the Refund modal.
- Custom adjustments — anything in
adjustment_cents(damage charges, late fees) is included in the refund-due math but the operator may want to retain specific adjustments. Override by manually refunding less.
Operator cancel flow
- Open the booking detail
- Click Cancel Booking (only visible for pending / confirmed / checked_in)
- Type an optional reason
- Click Confirm Cancel
The status flips to cancelled, the cancellation fee is computed and
stored on the reservation, the auto-wallet-refund (if applicable)
processes, and the page refreshes.
If the auto-refund fails (e.g., wallet helper error), the cancellation itself stays — only the refund step rolls back. The operator can manually issue the refund later from the (now-completed) booking detail.
Customer cancel flow
The customer opens their manage-link (/manage/[token]) — sent in the
booking confirmation email. If their booking is in pending or confirmed
status, they see a Cancel this booking button.
Clicking it:
- Opens an inline reason field
- Posts to
/api/public/manage/[token]withaction: cancel - Same backend logic runs — fee calc + auto-refund
The customer sees a green confirmation banner. If a refund was issued, they're told it'll appear in their wallet shortly.
For more on customer self-serve, see Customer self-serve manage link.
What blocks a cancel
| Status | Cancellable? |
|---|---|
| pending | ✅ |
| confirmed | ✅ |
| checked_in | ✅ |
| active | ❌ — bike is already out, treat as a return + partial refund |
| completed | ❌ — handled via post-hoc refund |
| cancelled | ❌ — already cancelled |
| no_show | ❌ — can't cancel after the no-show window expired |
| expired | ❌ — same |
If a customer wants to cancel an active rental, complete the return and issue a partial refund for the unused time.
Visible to the customer
After cancellation, the customer's manage-link page shows:
- Status: cancelled (red badge)
- "Booking cancelled" banner
- The amount auto-refunded (if any)
- "Any eligible refund will appear in your wallet shortly."
The customer doesn't see internal details like "cancellation fee was $50" — your customer-facing cancellation policy text on the public booking page should set expectations upfront.
Email notifications
The cancel flow sends a booking_cancelled notification (push + email
if configured). The notification includes the cancellation fee charged
and the refund amount.
You can override the email template per-subaccount in
subaccount_email_templates — see your subaccount settings.
Reporting cancellations
SELECT
date_trunc('day', cancelled_at) AS day,
cancelled_by,
count(*) AS cancellations,
sum(cancellation_fee_cents) / 100.0 AS total_fees_kept
FROM reservations
WHERE status = 'cancelled'
AND cancelled_at >= '2026-05-01'
GROUP BY 1, 2
ORDER BY 1, 2;
Track these monthly — high cancellation rates can signal pricing or expectations mismatches in your public booking flow.
Edge cases
Customer cancels and re-books
Two separate reservations. The cancelled one keeps its fee; the new one charges fresh. Operators sometimes credit the cancellation fee to the new booking via a manual partial refund — that's a goodwill gesture, not automatic.
Operator cancels for inventory reasons
If you have to cancel a customer's booking because a bike broke or your shop unexpectedly closed, don't charge the cancellation fee. After the cancel, manually refund the cancellation fee amount as a partial refund with reason "Operator cancellation — fee waived."
Cancel after the no-show cron has flipped the booking
Once a booking is no_show, the cancel path no longer applies. Use a
manual refund if your policy allows it.
Cancel during checked_in (pickup paperwork started)
Allowed. The customer is at the counter, paperwork is started, but they've changed their mind. The free-cancel window has obviously passed (you're at pickup time), so the cancellation fee will apply per your policy. Some shops waive this with a partial refund.
Running a fleet on this hardware?
Levy Fleets runs scooters, e-bikes, mopeds and golf carts on the IoT hardware you already have, with integration pages for 29 vendors including Segway, Okai, Omni, Queclink, Invers and Comodule. Keyless unlock, geofencing, rider payments and rider support are included, and the Managed plan is a share of revenue with no per-vehicle software fee.