docs
/
AppEngine API

Stowbo

Space custody — listings, carts and holds, bookings with per-item sessions, movements, delegated pickups, fees, settlement and host payouts.

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. init then answers { signedIn: false, host: false }.
  • Customer only. Everything else on the client surface throws 401 Sign in required when 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 on customer.data.benefits. Creating listings, units, add-ons, fees and coupons, and every take-payment, wallet and payout route, answer 403 without 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 in movement. 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 is present is read from the direction of its last movement.
  • Check-out is the only place an item stops using capacity. It refuses with 409 STILL_PRESENT until 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, or overstay past 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

POST/client/stowbo/checkout/quoteJWT
POST/client/stowbo/checkout/cartJWT
GET/client/stowbo/checkout/resumeJWT
PUT/client/stowbo/checkout/:checkoutId/progressJWT
POST/client/stowbo/checkout/suggestionsJWT
POST/client/stowbo/checkout/holdJWT
POST/client/stowbo/checkout/:checkoutId/confirmJWT
DELETE/client/stowbo/checkout/:checkoutIdJWT
POST/client/stowbo/bookingsJWT

The pending purchase is a stowbo_cart (open · held · converted · abandoned · expired). Each customer has one, and it is updated in place.

  • quote prices 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.
  • cart saves the cart without claiming any capacity.
  • resume prices the cart again rather than replaying the old total, and changed says whether the price moved.
  • hold takes the capacity off the market for holdTtlMinutes (default 15, or the STOWBO_HOLD_TTL_MINUTES env). If anything is unavailable it answers 409 UNAVAILABLE.
  • confirm captures by default; send captureNow: false to authorise only. What it charges is upfront% of the total plus any deposit, or nothing for a free or open-ended stay. A decline answers 402 and leaves the hold alive. A hold that has expired answers 409 HOLD_EXPIRED. Sending confirm again after it succeeded returns the same booking.

Cancellation terms are copied onto the booking when it is confirmed:

PolicyFree untilRefund after that
flexiblethe start—
moderate (default)24 hours before the startfull
strict48 hours before the starthalf

Guest routes

Discovery:

GET/client/stowbo/initJWT
GET/client/stowbo/chat-configJWT
GET/client/stowbo/listingsJWT
GET/client/stowbo/listings/:listingIdJWT
GET/client/stowbo/listings/:listingId/availabilityJWT
GET/client/stowbo/listings/:listingId/calendarJWT
GET/client/stowbo/listings/:listingId/nearbyJWT
GET/client/stowbo/listings/:listingId/more-from-hostJWT

listings 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:

GET/client/stowbo/bookingsJWT
GET/client/stowbo/bookings/:bookingIdJWT
GET/client/stowbo/bookings/:bookingId/hostJWT
GET/client/stowbo/bookings/:bookingId/timelineJWT
GET/client/stowbo/bookings/:bookingId/dueJWT
PUT/client/stowbo/bookings/:bookingId/cancelJWT
POST/client/stowbo/bookings/:bookingId/extendJWT
POST/client/stowbo/bookings/:bookingId/addonsJWT
POST/client/stowbo/bookings/:bookingId/settle-paymentJWT
POST/client/stowbo/bookings/:bookingId/requestJWT
GET/client/stowbo/my-itemsJWT
GET/client/stowbo/my-stuffJWT
  • due?action=current|cancel gives the numbers the cancel will use.
  • Cancelling is refused once any item is checked in. From then on, you check it out.
  • extend and addons take payment before they commit, so a decline changes nothing.
  • settle-payment can 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:

POST/client/stowbo/items/:itemId/movementJWT
POST/client/stowbo/items/:itemId/requestJWT
DELETE/client/stowbo/items/:itemId/requestJWT
POST/client/stowbo/parkJWT
GET/client/stowbo/park/:itemIdJWT
POST/client/stowbo/park/:itemId/stopJWT

staffing decides who moves things:

  • unmanned. The guest records their own in and out, and can use park to 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 manned listing refuses park.

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.

POST/client/stowbo/bookings/:bookingId/delegationsJWT
GET/client/stowbo/bookings/:bookingId/delegationsJWT
POST/client/stowbo/delegations/:id/resendJWT
DELETE/client/stowbo/delegations/:idJWT
GET/client/stowbo/my-pickupsJWT
GET/client/stowbo/pickup/:tokenJWT
GET/client/stowbo/pickup/:token/receiptJWT
  • What to send. The body is items[], name, phone or email (one is required), verify (qr · qr_name · qr_id, default qr_id), and optional validUntil, note and photo.
  • Which items. Each item must still be in custody and not on another live hand-off.
  • How long. validUntil is capped at the booking end.
  • The owner's code. While a hand-off is live, the booking's own pickupCode no longer releases those items.
  • The token. It is stored hashed. resend issues 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:

GET/client/stowbo/host/handoverJWT
POST/client/stowbo/host/handoverJWT
POST/client/stowbo/host/items/:itemId/refuseJWT
GET/client/stowbo/host/handoversJWT
GET/client/stowbo/host/bookings/:bookingId/delegationsJWT
  • GET host/handover?code= looks up a scanned code and releases nothing.
  • POST host/handover needs verified[] 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:

POST/client/stowbo/host/applyJWT
GET/client/stowbo/host/meJWT
GET/client/stowbo/host/listingsJWT
POST/client/stowbo/host/listingsJWT
GET/client/stowbo/host/listings/:idJWT
PUT/client/stowbo/host/listings/:idJWT
PUT/client/stowbo/host/listings/:id/statusJWT
DELETE/client/stowbo/host/listings/:idJWT
GET/client/stowbo/host/listings/:id/bookingsJWT
GET/client/stowbo/host/listings/:id/calendarJWT
GET/client/stowbo/host/listings/:id/occupancy-nowJWT
GET/client/stowbo/host/listings/:id/blackoutsJWT
POST/client/stowbo/host/listings/:id/blackoutsJWT
DELETE/client/stowbo/host/listings/:id/blackouts/:blackoutIdJWT
GET/client/stowbo/host/unitsJWT
POST/client/stowbo/host/unitsJWT
PUT/client/stowbo/host/units/:idJWT

apply enrols the customer in the stowbo-host benefit and runs setup first if the org has never had it.

Add-ons, fees and coupons:

GET/client/stowbo/host/addonsJWT
POST/client/stowbo/host/addonsJWT
PUT/client/stowbo/host/addons/:addonIdJWT
DELETE/client/stowbo/host/addons/:addonIdJWT
PUT/client/stowbo/host/listings/:listingId/addonsJWT
GET/client/stowbo/host/listings/:listingId/feesJWT
POST/client/stowbo/host/listings/:listingId/feesJWT
PUT/client/stowbo/host/listings/:listingId/feesJWT
DELETE/client/stowbo/host/listings/:listingId/fees/:feeIdJWT
PUT/client/stowbo/host/fees/:feeIdJWT
GET/client/stowbo/host/discountsJWT
POST/client/stowbo/host/discountsJWT
GET/client/stowbo/host/discounts/:id/statsJWT
PUT/client/stowbo/host/discounts/:idJWT
DELETE/client/stowbo/host/discounts/:idJWT
  • Add-ons. An add-on with price: 0 is free. service: true means buying it raises a fulfilment request for the host. DELETE retires 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_fee is defined once and attached to listings by name. Its fields answer four questions:
    • Whose money: type (host · platform · tax · processing) and paidBy (guest adds a line to the bill; host comes out of the payout).
    • When it applies: trigger (booking · cancellation · late_pickup · no_show).
    • How it is counted: appliesTo (checkout once, or per item) and basis (fixed or percent).
    • Limits and scope: priceFrom/priceTo, minFee/maxFee, channels, spaceTypes, validFrom/validTo.

The counter:

GET/client/stowbo/host/resolveJWT
GET/client/stowbo/host/todayJWT
POST/client/stowbo/host/bookingsJWT
GET/client/stowbo/host/bookings/:bookingId/itemsJWT
POST/client/stowbo/host/items/:itemId/checkinJWT
POST/client/stowbo/host/items/:itemId/movementJWT
POST/client/stowbo/host/items/:itemId/assignJWT
GET/client/stowbo/host/items/:itemId/checkoutJWT
POST/client/stowbo/host/items/:itemId/checkoutJWT
POST/client/stowbo/host/items/:itemId/requestJWT
  • resolve?q= takes at least 2 characters and searches plates, phones, references, units and tags across the host's own listings.
  • host/bookings opens a stay for someone at the counter. It defaults to an open stay, needs only one detail (phone or plate) to identify the customer, and 409s when the space cannot take it.
  • GET …/checkout previews what closing the item will bill, with the same calculation POST uses.
  • host/items/:itemId/request moves a guest's request along: acknowledged · preparing · ready.

Bookings, the bill and money:

GET/client/stowbo/host/bookingsJWT
GET/client/stowbo/host/bookings/:bookingIdJWT
GET/client/stowbo/host/bookings/:bookingId/dueJWT
GET/client/stowbo/host/bookings/:bookingId/customerJWT
GET/client/stowbo/host/bookings/:bookingId/timelineJWT
POST/client/stowbo/host/bookings/:bookingId/requests/:requestIdJWT
POST/client/stowbo/host/bookings/:bookingId/chargeJWT
POST/client/stowbo/host/bookings/:bookingId/refundJWT
POST/client/stowbo/host/bookings/:bookingId/lines/:index/reverseJWT
POST/client/stowbo/host/bookings/:bookingId/settleJWT
POST/client/stowbo/host/bookings/:bookingId/holdJWT
POST/client/stowbo/host/bookings/:bookingId/releaseJWT
POST/client/stowbo/host/bookings/:bookingId/cancelJWT
GET/client/stowbo/host/earningsJWT

due includes the meter still running on items that are present.

Take payment, wallet and payouts (approved host only):

POST/client/stowbo/host/take-paymentJWT
POST/client/stowbo/host/payment-requestJWT
GET/client/stowbo/host/payment-request/:requestIdJWT
POST/client/stowbo/host/payment-request/:requestId/cancelJWT
POST/client/stowbo/host/payment-request/:requestId/completeJWT
GET/client/stowbo/host/transactionsJWT
POST/client/stowbo/host/transactions/:ref/refundJWT
POST/client/stowbo/host/transactions/:ref/receiptJWT
GET/client/stowbo/host/walletJWT
GET/client/stowbo/host/payoutsJWT
POST/client/stowbo/host/payoutsJWT
GET/client/stowbo/host/payout-methodsJWT
POST/client/stowbo/host/payout-methodsJWT
PUT/client/stowbo/host/payout-methods/:methodId/defaultJWT
DELETE/client/stowbo/host/payout-methods/:methodIdJWT
GET/client/stowbo/pay/:refJWT
POST/client/stowbo/pay/:refJWT
  • What is cleaned. Before a take-payment or payment-request body reaches the ledger, the controller caps amount at 1,000,000 and rounds it to 2 decimals, whitelists category (anything unknown becomes other), and falls back to usd for currency. Emails and phone numbers that fail a format check are dropped.
  • take-payment records a Tap to Pay intent that has already succeeded.
  • payment-request writes a pending ledger row and returns a pay link and QR code.
  • complete is called by the signed-out web /pay flow, so it needs no customer. It verifies paymentIntentId with 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 answered 402 PAYMENT_AMOUNT_MISMATCH.
  • pay/:ref looks 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. available is balance minus reserved minus held. See finance wallets.

Operator routes

Setup and platform config:

POST/stowbo/setupJWT
GET/stowbo/setup/statusJWT
GET/stowbo/platform-configJWT
POST/stowbo/platform-configJWT
GET/stowbo/platform-feesJWT
POST/stowbo/platform-feesJWT
GET/stowbo/siteJWT
  • setup creates whatever is missing: the stowbo-host benefit, the stowbo-custody-pipeline workflow and the stowbo site. It is safe to run more than once, and it also runs on its own the first time someone applies to host.
  • platform-config holds minPayout, takeRate (a fraction from 0 to 1, default 0.18), gateway (default stripe), holdTtlMinutes, appUrl and site. It is stored as an org setting and takes effect with no restart.
  • platform-fees replaces the whole list. A body without a fees array answers 400.

A single booking:

POST/stowbo/bookingJWT
POST/stowbo/booking/:bookingId/extendJWT
POST/stowbo/booking/:bookingId/move-unitJWT
POST/stowbo/booking/:bookingId/accessJWT
GET/stowbo/booking/:bookingId/timelineJWT
POST/stowbo/booking/:bookingId/requestJWT
POST/stowbo/booking/:bookingId/requests/:requestIdJWT
POST/stowbo/booking/:bookingId/handoversJWT
POST/stowbo/items/:itemId/checkinJWT
POST/stowbo/items/:itemId/movementJWT
POST/stowbo/items/:itemId/assignJWT
GET/stowbo/items/:itemId/checkoutJWT
POST/stowbo/items/:itemId/checkoutJWT

POST /stowbo/booking goes through the same path as the customer app, with channel: operator. skipPayment records the booking as owed.

Money on a booking:

POST/stowbo/booking/:bookingId/holdJWT
POST/stowbo/booking/:bookingId/release-holdJWT
POST/stowbo/booking/:bookingId/chargeJWT
POST/stowbo/booking/:bookingId/cancelJWT
POST/stowbo/booking/:bookingId/refundJWT
POST/stowbo/booking/:bookingId/settleJWT
GET/stowbo/booking/:bookingId/adjustmentsJWT
POST/stowbo/booking/:bookingId/adjustmentsJWT
PUT/stowbo/booking/:bookingId/adjustments/:index/reverseJWT
POST/stowbo/booking/:bookingId/addonJWT
POST/stowbo/booking/:bookingId/payment-requestJWT
POST/stowbo/booking/:bookingId/take-paymentJWT
  • settle adds overstay for items still open, then captures. It refuses with BOOKING_UNPAID while 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.
  • cancel frees units, voids an uncaptured authorisation, refunds per the cancellation terms and keeps the record. Send noShow: true for a no-show.
  • refund without cancel leaves the stay running.
  • An adjustment's bearer (guest · host · platform) says who absorbs it.

Spaces:

GET/stowbo/availabilityJWT
GET/stowbo/listing/:listing/calendarJWT
GET/stowbo/listing/:listing/blackoutsJWT
POST/stowbo/listing/:listing/blackoutsJWT
DELETE/stowbo/listing/:listing/blackouts/:blackoutIdJWT
POST/stowbo/listing/:listing/capacityJWT
GET/stowbo/listing/:listing/occupancy-nowJWT
GET/stowbo/listing/:listing/addonsJWT
  • A blackout does not cancel bookings already inside its window. They come back as conflicts.
  • capacity limits a space for a window; capacity: 0 closes it.
  • addons lists the listing's own add-ons plus those of every listing above it.

Oversight:

GET/stowbo/pulseJWT
GET/stowbo/itemsJWT
GET/stowbo/bookingsJWT
GET/stowbo/calendarJWT
GET/stowbo/custodyJWT
GET/stowbo/overdueJWT
GET/stowbo/moneyJWT
GET/stowbo/activityJWT
GET/stowbo/funnelJWT
GET/stowbo/hostsJWT
GET/stowbo/addonsJWT
GET/stowbo/searchJWT
GET/stowbo/audienceJWT
GET/stowbo/customer/:idJWT
  • items lists booking items, filtered by lane: custody · arriving · departing · overstaying · awaitingCheckout · present · gone · requests.
  • funnel follows 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:

GET/stowbo/ledgerJWT
GET/stowbo/transactions/:refJWT
POST/stowbo/transactions/:ref/refundJWT
POST/stowbo/transactions/:ref/receiptJWT
POST/stowbo/transactions/:requestId/cancelJWT
GET/stowbo/discountsJWT
POST/stowbo/discountsJWT
GET/stowbo/discounts/:idJWT
PUT/stowbo/discounts/:idJWT
POST/stowbo/discounts/:id/:actionJWT
GET/stowbo/handoversJWT
GET/stowbo/handovers/:idJWT
POST/stowbo/handovers/:id/resendJWT
POST/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. :action is activate or deactivate. 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.