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.