StowboModule runs a marketplace for temporary custody of space: a guest books somewhere to leave a car, a bag or a box, and a host takes it in. Two surfaces, 188 routes: /stowbo/* is the platform operator's console (68 routes, scoped to nobody because overseeing everyone is its job), and /client/stowbo/* is the guest and host app (120 routes, every read scoped to whoever is signed in).
Every route takes the orgid header and needs a JWT. The operator controller is @StaffOnly(); on the client surface the gate is the JWT plus checks in the service:
- No customer needed. Browsing, the pickup pass (
pickup/:token) and the pay page (pay/:ref) work with the site's app token and nobody signed in.initthen answers{ signedIn: false, host: false }. - Customer only. Everything else on the client surface throws
401 Sign in requiredwhen there is no customer. Guest routes also check that the booking is yours (403 Not your booking). - Approved host. Hosting is the CRM benefit
stowbo-host(see CRM). Approval puts it oncustomer.data.benefits. Creating listings, units, add-ons, fees and coupons, and every take-payment, wallet and payout route, answer403without it. Host routes on a listing or booking also check that you own the listing (403 Not your listing). - Operator.
/stowbo/*answers only someone signed in to the business — never a customer or the site's app token (403).
The model
A listing is the bookable thing, and listings nest through parent: a locker bank holds locker types, a warehouse holds bays. Identical slots are a capacity count on one listing. Numbered doors are stowbo_unit rows. A listing carries rates (bands read as posted or progressive), payment (upfront %, deposit), taxRate, fees[] and addOns[] (by name), cancellationPolicy, staffing (manned · unmanned), blackouts and status (draft · active · paused · inactive).
A booking is an order. Its items[] is the bill: lines of kind space · addon · fee · tax · discount · adjustment · overstay. Amounts are signed, and a line is never edited — a mistake is undone by a reversing line. Order status: requested → confirmed → active → completed → settled, or cancelled. Payments are not stored on the booking. They are sf_transaction rows carrying metadata.booking, and the balance is worked out from them.
A booking item (stowbo_booking_item) is one thing booked, with its own window, unit, identifier (plate, tag), session and movements. Its status runs reserved → active → ended, and can also be disputed or cancelled. Twelve units under one order are twelve items, so every counter action addresses an item, never the order.
- Check-in opens the session and records the first
inmovement. It is idempotent. - Movements (
in/out) are unlimited and append-only inside one session. They are never billed and never touch capacity. Whether a thing ispresentis read from the direction of its last movement. - Check-out is the only place an item stops using capacity. It refuses with
409 STILL_PRESENTuntil the thing has moved out. It adds what that item actually cost to the order as lines: an open stay priced from start to exit, oroverstaypast a fixed window.
Availability is never stored. It is worked out from open booking items, and the booking write checks it again, so a stale read can under-sell but cannot double-book.
Checkout
/client/stowbo/checkout/quoteJWT/client/stowbo/checkout/cartJWT/client/stowbo/checkout/resumeJWT/client/stowbo/checkout/:checkoutId/progressJWT/client/stowbo/checkout/suggestionsJWT/client/stowbo/checkout/holdJWT/client/stowbo/checkout/:checkoutId/confirmJWT/client/stowbo/checkout/:checkoutIdJWT/client/stowbo/bookingsJWTThe pending purchase is a stowbo_cart (open · held · converted · abandoned · expired). Each customer has one, and it is updated in place.
quoteprices a mixed cart line by line and has no side effects. A bad code or a short line comes back in the answer; it is not thrown.cartsaves the cart without claiming any capacity.resumeprices the cart again rather than replaying the old total, andchangedsays whether the price moved.holdtakes the capacity off the market forholdTtlMinutes(default 15, or theSTOWBO_HOLD_TTL_MINUTESenv). If anything is unavailable it answers409 UNAVAILABLE.confirmcaptures by default; sendcaptureNow: falseto authorise only. What it charges isupfront% of the total plus any deposit, or nothing for a free or open-ended stay. A decline answers402and leaves the hold alive. A hold that has expired answers409 HOLD_EXPIRED. Sendingconfirmagain after it succeeded returns the same booking.
Cancellation terms are copied onto the booking when it is confirmed:
| Policy | Free until | Refund after that |
|---|---|---|
flexible | the start | — |
moderate (default) | 24 hours before the start | full |
strict | 48 hours before the start | half |
Guest routes
Discovery:
/client/stowbo/initJWT/client/stowbo/chat-configJWT/client/stowbo/listingsJWT/client/stowbo/listings/:listingIdJWT/client/stowbo/listings/:listingId/availabilityJWT/client/stowbo/listings/:listingId/calendarJWT/client/stowbo/listings/:listingId/nearbyJWT/client/stowbo/listings/:listingId/more-from-hostJWTlistings returns only top-level active listings. It searches by map viewport (swLat/swLng/neLat/neLng), by a point and radiusKm (default 10, max 200), or by city/keyword. With startDate + endDate it returns only what is free for that window, priced for it. Without dates it is a plain catalogue browse, with no availability or price.
My bookings and items:
/client/stowbo/bookingsJWT/client/stowbo/bookings/:bookingIdJWT/client/stowbo/bookings/:bookingId/hostJWT/client/stowbo/bookings/:bookingId/timelineJWT/client/stowbo/bookings/:bookingId/dueJWT/client/stowbo/bookings/:bookingId/cancelJWT/client/stowbo/bookings/:bookingId/extendJWT/client/stowbo/bookings/:bookingId/addonsJWT/client/stowbo/bookings/:bookingId/settle-paymentJWT/client/stowbo/bookings/:bookingId/requestJWT/client/stowbo/my-itemsJWT/client/stowbo/my-stuffJWTdue?action=current|cancelgives the numbers the cancel will use.- Cancelling is refused once any item is checked in. From then on, you check it out.
extendandaddonstake payment before they commit, so a decline changes nothing.settle-paymentcan be sent more than once with the same payment intent; it settles once.- The guest timeline leaves out who bore each cost internally.
Movements and self-serve parking:
/client/stowbo/items/:itemId/movementJWT/client/stowbo/items/:itemId/requestJWT/client/stowbo/items/:itemId/requestJWT/client/stowbo/parkJWT/client/stowbo/park/:itemIdJWT/client/stowbo/park/:itemId/stopJWTstaffing decides who moves things:
unmanned. The guest records their owninandout, and can useparkto start a metered open stay that is already checked in.- Anything else (attended). The guest raises a tracked retrieval or return request and the host does the move. A
mannedlisting refusespark.
Delegated pickup
The owner hands some items to someone else: a buyer, a colleague, a courier. The hand-off is a stowbo_pickup_delegation with its own pickup code and pass token.
/client/stowbo/bookings/:bookingId/delegationsJWT/client/stowbo/bookings/:bookingId/delegationsJWT/client/stowbo/delegations/:id/resendJWT/client/stowbo/delegations/:idJWT/client/stowbo/my-pickupsJWT/client/stowbo/pickup/:tokenJWT/client/stowbo/pickup/:token/receiptJWT- What to send. The body is
items[],name,phoneoremail(one is required),verify(qr·qr_name·qr_id, defaultqr_id), and optionalvalidUntil,noteandphoto. - Which items. Each item must still be in custody and not on another live hand-off.
- How long.
validUntilis capped at the booking end. - The owner's code. While a hand-off is live, the booking's own
pickupCodeno longer releases those items. - The token. It is stored hashed.
resendissues a new link and the old one stops working. - Where the outcome goes. Who collected, what was verified and any refusals are written on the booking item.
The host at the counter:
/client/stowbo/host/handoverJWT/client/stowbo/host/handoverJWT/client/stowbo/host/items/:itemId/refuseJWT/client/stowbo/host/handoversJWT/client/stowbo/host/bookings/:bookingId/delegationsJWTGET host/handover?code=looks up a scanned code and releases nothing.POST host/handoverneedsverified[]to cover what the pass asks for (400 VERIFY_REQUIRED) and a photo for a hand-off (400 PHOTO_REQUIRED). It then moves each item out and checks it out.- A refusal (
wrong_person·expired·revoked·id_mismatch·other) moves nothing, and the owner is told.
Host routes
Becoming a host, and listings:
/client/stowbo/host/applyJWT/client/stowbo/host/meJWT/client/stowbo/host/listingsJWT/client/stowbo/host/listingsJWT/client/stowbo/host/listings/:idJWT/client/stowbo/host/listings/:idJWT/client/stowbo/host/listings/:id/statusJWT/client/stowbo/host/listings/:idJWT/client/stowbo/host/listings/:id/bookingsJWT/client/stowbo/host/listings/:id/calendarJWT/client/stowbo/host/listings/:id/occupancy-nowJWT/client/stowbo/host/listings/:id/blackoutsJWT/client/stowbo/host/listings/:id/blackoutsJWT/client/stowbo/host/listings/:id/blackouts/:blackoutIdJWT/client/stowbo/host/unitsJWT/client/stowbo/host/unitsJWT/client/stowbo/host/units/:idJWTapply enrols the customer in the stowbo-host benefit and runs setup first if the org has never had it.
Add-ons, fees and coupons:
/client/stowbo/host/addonsJWT/client/stowbo/host/addonsJWT/client/stowbo/host/addons/:addonIdJWT/client/stowbo/host/addons/:addonIdJWT/client/stowbo/host/listings/:listingId/addonsJWT/client/stowbo/host/listings/:listingId/feesJWT/client/stowbo/host/listings/:listingId/feesJWT/client/stowbo/host/listings/:listingId/feesJWT/client/stowbo/host/listings/:listingId/fees/:feeIdJWT/client/stowbo/host/fees/:feeIdJWT/client/stowbo/host/discountsJWT/client/stowbo/host/discountsJWT/client/stowbo/host/discounts/:id/statsJWT/client/stowbo/host/discounts/:idJWT/client/stowbo/host/discounts/:idJWT- Add-ons. An add-on with
price: 0is free.service: truemeans buying it raises a fulfilment request for the host.DELETEretires the add-on rather than removing it. - Coupons. Host coupons are storefront discounts. The server locks each one to the host's own listings.
- Fees. A
stowbo_feeis defined once and attached to listings by name. Its fields answer four questions:- Whose money:
type(host·platform·tax·processing) andpaidBy(guestadds a line to the bill;hostcomes out of the payout). - When it applies:
trigger(booking·cancellation·late_pickup·no_show). - How it is counted:
appliesTo(checkoutonce, or peritem) andbasis(fixedorpercent). - Limits and scope:
priceFrom/priceTo,minFee/maxFee,channels,spaceTypes,validFrom/validTo.
- Whose money:
The counter:
/client/stowbo/host/resolveJWT/client/stowbo/host/todayJWT/client/stowbo/host/bookingsJWT/client/stowbo/host/bookings/:bookingId/itemsJWT/client/stowbo/host/items/:itemId/checkinJWT/client/stowbo/host/items/:itemId/movementJWT/client/stowbo/host/items/:itemId/assignJWT/client/stowbo/host/items/:itemId/checkoutJWT/client/stowbo/host/items/:itemId/checkoutJWT/client/stowbo/host/items/:itemId/requestJWTresolve?q=takes at least 2 characters and searches plates, phones, references, units and tags across the host's own listings.host/bookingsopens a stay for someone at the counter. It defaults to an open stay, needs only one detail (phone or plate) to identify the customer, and409s when the space cannot take it.GET …/checkoutpreviews what closing the item will bill, with the same calculationPOSTuses.host/items/:itemId/requestmoves a guest's request along:acknowledged·preparing·ready.
Bookings, the bill and money:
/client/stowbo/host/bookingsJWT/client/stowbo/host/bookings/:bookingIdJWT/client/stowbo/host/bookings/:bookingId/dueJWT/client/stowbo/host/bookings/:bookingId/customerJWT/client/stowbo/host/bookings/:bookingId/timelineJWT/client/stowbo/host/bookings/:bookingId/requests/:requestIdJWT/client/stowbo/host/bookings/:bookingId/chargeJWT/client/stowbo/host/bookings/:bookingId/refundJWT/client/stowbo/host/bookings/:bookingId/lines/:index/reverseJWT/client/stowbo/host/bookings/:bookingId/settleJWT/client/stowbo/host/bookings/:bookingId/holdJWT/client/stowbo/host/bookings/:bookingId/releaseJWT/client/stowbo/host/bookings/:bookingId/cancelJWT/client/stowbo/host/earningsJWTdue includes the meter still running on items that are present.
Take payment, wallet and payouts (approved host only):
/client/stowbo/host/take-paymentJWT/client/stowbo/host/payment-requestJWT/client/stowbo/host/payment-request/:requestIdJWT/client/stowbo/host/payment-request/:requestId/cancelJWT/client/stowbo/host/payment-request/:requestId/completeJWT/client/stowbo/host/transactionsJWT/client/stowbo/host/transactions/:ref/refundJWT/client/stowbo/host/transactions/:ref/receiptJWT/client/stowbo/host/walletJWT/client/stowbo/host/payoutsJWT/client/stowbo/host/payoutsJWT/client/stowbo/host/payout-methodsJWT/client/stowbo/host/payout-methodsJWT/client/stowbo/host/payout-methods/:methodId/defaultJWT/client/stowbo/host/payout-methods/:methodIdJWT/client/stowbo/pay/:refJWT/client/stowbo/pay/:refJWT- What is cleaned. Before a take-payment or payment-request body reaches the ledger, the controller caps
amountat 1,000,000 and rounds it to 2 decimals, whitelistscategory(anything unknown becomesother), and falls back tousdforcurrency. Emails and phone numbers that fail a format check are dropped. take-paymentrecords a Tap to Pay intent that has already succeeded.payment-requestwrites a pending ledger row and returns a pay link and QR code.completeis called by the signed-out web/payflow, so it needs no customer. It verifiespaymentIntentIdwith the gateway, and settles the request only when the charge covers its amount and currency; a charge that doesn't is recorded on its own and answered402 PAYMENT_AMOUNT_MISMATCH.pay/:reflooks up a request token, then a ledger row, then a booking id, and always charges the balance the server computes.- Payout methods. Bank numbers are encrypted when they arrive, and only the last four digits are ever returned.
- The wallet.
availableis balance minus reserved minus held. See finance wallets.
Operator routes
Setup and platform config:
/stowbo/setupJWT/stowbo/setup/statusJWT/stowbo/platform-configJWT/stowbo/platform-configJWT/stowbo/platform-feesJWT/stowbo/platform-feesJWT/stowbo/siteJWTsetupcreates whatever is missing: thestowbo-hostbenefit, thestowbo-custody-pipelineworkflow and thestowbosite. It is safe to run more than once, and it also runs on its own the first time someone applies to host.platform-configholdsminPayout,takeRate(a fraction from 0 to 1, default 0.18),gateway(defaultstripe),holdTtlMinutes,appUrlandsite. It is stored as an org setting and takes effect with no restart.platform-feesreplaces the whole list. A body without afeesarray answers400.
A single booking:
/stowbo/bookingJWT/stowbo/booking/:bookingId/extendJWT/stowbo/booking/:bookingId/move-unitJWT/stowbo/booking/:bookingId/accessJWT/stowbo/booking/:bookingId/timelineJWT/stowbo/booking/:bookingId/requestJWT/stowbo/booking/:bookingId/requests/:requestIdJWT/stowbo/booking/:bookingId/handoversJWT/stowbo/items/:itemId/checkinJWT/stowbo/items/:itemId/movementJWT/stowbo/items/:itemId/assignJWT/stowbo/items/:itemId/checkoutJWT/stowbo/items/:itemId/checkoutJWTPOST /stowbo/booking goes through the same path as the customer app, with channel: operator. skipPayment records the booking as owed.
Money on a booking:
/stowbo/booking/:bookingId/holdJWT/stowbo/booking/:bookingId/release-holdJWT/stowbo/booking/:bookingId/chargeJWT/stowbo/booking/:bookingId/cancelJWT/stowbo/booking/:bookingId/refundJWT/stowbo/booking/:bookingId/settleJWT/stowbo/booking/:bookingId/adjustmentsJWT/stowbo/booking/:bookingId/adjustmentsJWT/stowbo/booking/:bookingId/adjustments/:index/reverseJWT/stowbo/booking/:bookingId/addonJWT/stowbo/booking/:bookingId/payment-requestJWT/stowbo/booking/:bookingId/take-paymentJWTsettleadds overstay for items still open, then captures. It refuses withBOOKING_UNPAIDwhile the ledger shows a balance. It then splits the money at the take rate and credits the host's wallet.- A hold (
paymentHold) still lets the guest be charged but stops the host being paid at settle. cancelfrees units, voids an uncaptured authorisation, refunds per the cancellation terms and keeps the record. SendnoShow: truefor a no-show.refundwithoutcancelleaves the stay running.- An adjustment's
bearer(guest·host·platform) says who absorbs it.
Spaces:
/stowbo/availabilityJWT/stowbo/listing/:listing/calendarJWT/stowbo/listing/:listing/blackoutsJWT/stowbo/listing/:listing/blackoutsJWT/stowbo/listing/:listing/blackouts/:blackoutIdJWT/stowbo/listing/:listing/capacityJWT/stowbo/listing/:listing/occupancy-nowJWT/stowbo/listing/:listing/addonsJWT- A blackout does not cancel bookings already inside its window. They come back as
conflicts. capacitylimits a space for a window;capacity: 0closes it.addonslists the listing's own add-ons plus those of every listing above it.
Oversight:
/stowbo/pulseJWT/stowbo/itemsJWT/stowbo/bookingsJWT/stowbo/calendarJWT/stowbo/custodyJWT/stowbo/overdueJWT/stowbo/moneyJWT/stowbo/activityJWT/stowbo/funnelJWT/stowbo/hostsJWT/stowbo/addonsJWT/stowbo/searchJWT/stowbo/audienceJWT/stowbo/customer/:idJWTitemslists booking items, filtered bylane:custody·arriving·departing·overstaying·awaitingCheckout·present·gone·requests.funnelfollows cart → details → payment → held → booked → turned up → collected, and counts walk-ins separately.audience?group=turns a group into real recipients. Groups:hosts.all·hosts.pending·hosts.overstaying·guests.inCustody·guests.overstaying·guests.unpaid·booking.both.
Ledger, discounts and hand-offs:
/stowbo/ledgerJWT/stowbo/transactions/:refJWT/stowbo/transactions/:ref/refundJWT/stowbo/transactions/:ref/receiptJWT/stowbo/transactions/:requestId/cancelJWT/stowbo/discountsJWT/stowbo/discountsJWT/stowbo/discounts/:idJWT/stowbo/discounts/:idJWT/stowbo/discounts/:id/:actionJWT/stowbo/handoversJWT/stowbo/handovers/:idJWT/stowbo/handovers/:id/resendJWT/stowbo/handovers/:id/revokeJWT- Refunds. A refund writes a linked refund row and marks the original; the original amount is never changed.
- Cancelling a request refuses one that is already paid; refund it instead.
- Discounts.
:actionisactivateordeactivate. A platform discount belongs to nobody, so no host can edit it.
Datatypes
stowbo_listing, stowbo_unit, stowbo_addon, stowbo_fee, stowbo_cart, stowbo_booking, stowbo_booking_item, stowbo_pickup_delegation, plus sf_transaction for every payment and refund.