docs
/
AI skills

Rentals

Rent things out by the hour, day, week or month. Set up the rental rules and the items, quote a price and check dates, let shoppers book and pay on the site, take bookings at the counter, then hand the item over and take it back, with deposits, late fees, cancellations and the books. Every email each step sends, read from the code.

Rentals use three records (appengine storefront/services/rental.service.ts):

RecordWhat it is
sf_rental_configThe house rules: currency, tax rate, default deposit, late fee, fees, availability, terms. One can be the default.
sf_rental_itemOne thing you rent out, by sku, with its prices and its own rules. Anything it leaves blank comes from its config.
sf_rentalOne booking. Its id in every URL is its name, a 10-letter code unless one was given.

A booking's status is one of pending, confirmed, ready, picked_up, delivered, active, overdue, returned, completed or cancelled. Its deposit has its own status: pending, held, partially_refunded, refunded, forfeited or waived. Its payment status is pending, partial, paid or refunded.

Only the customer's own rentals are theirs to see and cancel. Every other step (confirm, hand over, take back, payments, deposits, books) needs someone signed in to the business.

Ask first

  • What is rented? Name, SKU, how many you have, photos, description.
  • Prices. Per hour, day, week and/or month. Leave a period blank to not offer it.
  • Deposit. None, a fixed amount, or a percent of the rental. Refundable or not.
  • Fees. For example cleaning or insurance. Fixed or percent. Which ones the customer can choose.
  • Late returns. A rate per hour, day, week or month, a grace period, a cap.
  • Availability. Shortest and longest rental, a gap between rentals for cleaning, dates you are closed.
  • Handover. Pickup, delivery (and its fee), drop-off, or collection from the customer (and its fee).
  • Who may rent. Minimum age, ID check level, which countries or states.
  • Terms. The rental agreement the customer must accept.
  • Online or counter? Sold on the website, or booked only by staff.

Steps

1. Rental › Config: the house rules

Studio: Storefront › Rental, tab Config (/rental?openConfig=<sk> opens one). Or call_appengine PUT repository/create with datatype: "sf_rental_config", "isNew": true and data (sdk sf-rental-config.ts):

FieldWhat it does
nameUnique. Items point at a config by this name.
title, description, statusLabels.
isDefaultOnly one config can be the default. An item with no config uses it.
defaultPricing.currency, defaultPricing.taxRateCurrency, and the tax rate (percent) saved on each booking.
defaultDeposit{ required, type: fixed | percent, amount, refundable }, for items with no deposit of their own.
defaultLateFee{ enabled, rate, per, gracePeriod, gracePeriodUnit, maxFee }, for items with no late fee of their own.
defaultFees[]{ name, amount, type: fixed | percent, optional }.
defaultAvailabilitySame shape as the item's availability (step 2).
restrictions[]Same shape as the item's (step 2).
defaultTerms, requireAgreementThe agreement text. requireAgreement is on unless false.

An item whose config names a config that doesn't exist gets no config. It does not fall back to the default (getConfig, rental.service.ts 248). Leave config blank to use the default.

Hands on: the config name. Check: call_appengine GET storefront/rental/config?name=<name> returns it.

2. Rental › Items: the item

Tab Items (/rental?openItem=<sk>). Or PUT repository/create with datatype: "sf_rental_item" (sdk sf-rental-item.ts):

FieldWhat it does
name, slug, skusku is 3 to 50 characters and unique. The site page is /rental/<slug or sku>.
statusactive, inactive or maintenance. Only active items are listed.
configA config name. Blank: the default config.
quantityHow many you can rent out at once.
description, imagesFor the item page.
pricing{ currency, hourly, daily, weekly, monthly }.
deposit{ required, type: fixed | percent, amount, refundable }.
fees[]{ name, amount, type, optional }. An item fee replaces a config fee with the same name.
lateFee{ enabled, rate, per: hour | day | week | month, gracePeriod, gracePeriodUnit, maxFee }.
availabilityminDuration/minDurationUnit, maxDuration/maxDurationUnit, bufferTime/bufferTimeUnit, blockedDates[] { startDate, endDate, reason }.
fulfillmentallowPickup, allowDelivery, deliveryFee, deliveryFeeType: flat | per_mile | tiered, allowDropoff, allowReturnPickup, returnPickupFee.
pickupLocation, returnLocationAddresses.
restrictions[]{ type: age | kyc | location | license | membership | custom, enabled, minAge, maxAge, kycLevel, locationRule, countries, states, message }.
termsThe agreement, in place of the config's defaultTerms.

What the restrictions really check (validateRestrictions, rental.service.ts 393):

TypeChecked
ageOnly when a date of birth is given.
kycA verified customer_kyc record for the email, at kycLevel or above (basic, standard, enhanced, full).
locationcountries / states allowed or blocked.
license, membership, customNot checked.

Hands on: the item's sku. Check: GET storefront/rental/items/<sku> returns it, and GET storefront/rental/items lists it when active.

3. Quote a price and check the dates

call_appengine POST storefront/rental/items/<sku>/price with { startDate, endDate, quantity, optionalFees: ["<fee name>"] } (calculatePrice, rental.service.ts 510):

  • The cheapest of monthly, weekly and daily is used, with each counted in whole periods, rounded up. Under 24 hours with an hourly price: hourly.
  • Fixed fees count once per booking. Percent fees are on the subtotal.
  • The deposit is the item's, else the config's. A percent deposit is on the subtotal.
  • Tax is the config's taxRate.

Tested ($50 a day, $250 a week, $10 cleaning, 8% tax, $100 deposit):

DatesQuote
3 days$150 + $10 + 8% tax = $172.80, deposit $100
3 days, quantity 2Subtotal $300. The $10 fee and $100 deposit are not multiplied.
7 daysOne week: $250
8 daysDaily, $400, because weekly rounds up to two weeks ($500)
5 hours, no hourly priceOne day

GET storefront/rental/items/<sku>/availability?startDate=&endDate=&quantity= says yes, or why not (checkAvailability, 322). Tested refusals: "The shortest rental is 1 day", "The longest rental is 14 days", "Closed 2026-12-24 to 2026-12-26: Holiday", and more than you have. The buffer counts in whole calendar days: a 1-day buffer blocked a start 26 hours after the last rental ended. Cancelled and completed rentals don't count.

The price quote doesn't check the shortest or longest rental. Always check availability too.

POST storefront/rental/items/<sku>/validate-restrictions with { email, dateOfBirth?, country?, state? } answers { valid, failed[] }.

Check: read the quote back to the owner: dates, periods, fees, tax, deposit, total.

4. Site settings: the rental pages

Stop: turning on a site feature changes the live site. Ask first.

Rentals have their own site feature, rental. Studio's feature list doesn't show it unless the site already has it; see Site page routing. With it on, the site has (base-app components/rental/RentalRoute.tsx; see Built-in pages):

  • /rental: every active item;
  • /rental/<slug or sku>: the item page, with dates, quantity, optional fees and Add to Cart.

The customer must tick the agreement when a config applies and either requireAgreement is true, or it isn't false and there are terms. The terms shown are the item's terms, else the config's defaultTerms.

For a custom page, the runtime has window.appmint.rental (see AppMint JS engine on a page).

Check: curl -s -o /dev/null -w "%{http_code}" "<site>/rental" answers 200 and the item page shows the item. (Not tested here: with the feature off, /rental answered 404.)

5. Checkout: the shopper books and pays

Add to Cart puts a cart line with itemType: "rental", the dates, the chosen fees and the agreement. The customer pays through the shop's cart and /checkout like any order; see Take payments on the site.

The server prices the line itself (quoteRentalLine, discount.service.ts). Tested: a cart sent with made-up prices was priced at $150 + $15 fees + $100 deposit + $25 delivery = $290. A flat delivery or collection fee is the item's. A per-mile or tiered fee is never less than the item's base fee. The rental's total at checkout is subtotal + deposit + tax + delivery − discount. Rental discounts (free_deposit, free_delivery, waive_fees) are in Discount codes.

Dates are checked before the order is made (storefront.order.service.ts ~1981). The sf_rental is made after the order. With a card gateway the deposit is recorded as held and the payment recorded on it, using the card payment's reference (~2185).

If the booking can't be made after the order (a restriction fails, for example), the order stands and no rental is made. The error is only logged (storefront.order.service.ts 2234). Check that every paid rental order has its rental (step 6).

The customer sees their rental in My Account › Orders. There is no separate rentals section. See Customer account portal.

Check: the order is paid, and GET storefront/rental?customerEmail=<email> lists the rental.

6. Rental › Rentals: a booking by staff

Tab Rentals (/rental?openId=<sk>). The new-booking form calls POST storefront/rental (websitemint rental-actions.ts):

{
  "sku": "<sku>",
  "quantity": 1,
  "customer": { "firstName": "", "lastName": "", "email": "", "phone": "" },
  "startDate": "2026-11-02T10:00:00",
  "endDate": "2026-11-05T10:00:00",
  "fulfillment": { "type": "pickup" },
  "return": { "type": "dropoff" },
  "optionalFees": [],
  "customerNotes": "",
  "notes": ""
}

Studio's form can also record a payment or deposit taken at the counter while booking. The booking checks dates (409 with the reason), refuses a name already used (409), runs the restrictions, and saves the price with status pending.

Stop: a booking with an email sends the customer Rental Booked (rental-confirmation). Show who, the item, dates and price, and wait for yes.

Hands on: the rental's name. Check: GET storefront/rental/<name> shows it pending with its pricing.

7. Rental › Rentals: hand over and take back

Open the rental. The buttons (websitemint rental-fulfillment.tsx 403–430), each a call_appengine PUT storefront/rental/<name>/<step>:

ButtonStepStatus afterBodyEmail to the customer
Confirm Rentalconfirmconfirmednonerental-confirmed
Mark Readyreadyreadynonenone
Mark Picked Up / Mark Deliveredpicked-up / deliveredpicked_up / delivered{ actualDate? }rental-pickedup (both)
Record Check-Outcheckoutactive{ condition, notes?, photos?, checkedBy }rental-checkout (Rental Started)
Start Rentalactiveactivenonenone
Mark Overdueoverdueoverduenonenone
Mark Returneddropped-off (or returned)returned{ actualDate? }rental-droppedoff
Record Check-Incheckinreturned{ condition, notes?, photos?, damages?, checkedBy }rental-returned
Complete Rentalcompletecompletednonenone

condition is excellent, good, fair or poor. notes (PUT …/notes) adds a note and changes nothing else.

Stop before each step with an email: it goes to the customer.

Nothing enforces the order: any step can follow any other, except cancel (step 9). Tested in order: ready, picked up, check-out (active), overdue, returned, check-in, complete.

Lists: GET storefront/rental/status/due-today and GET storefront/rental/status/overdue.

The overdue list shows only rentals still active past their end. One already marked overdue drops off it (getOverdueRentals, 1570). Nothing marks rentals overdue or sends reminders by itself.

Check: the rental's status, and its condition record after check-out and check-in.

8. Late fees

A late fee applies when the item's lateFee is on (enabled, or a rate with enabled unset), else the config's defaultLateFee (rental-late-fee.ts). It counts from the end date plus the grace period, in whole per periods, up to maxFee. It is added as the fee Late return fee, the total goes up, and the payment status is worked out again.

Tested: end Nov 5 10:00, 1 hour grace, $10 a day; returned Nov 7 12:00 (dropped-off with actualDate): $30, total $178.20 → $208.20, payment back to partial.

Record Check-In sets the return time to now and works the late fee out again from it (rental.service.ts 1034, 1047–1050). If you mark it returned with the real time, check in on the same visit, or check the fee after.

9. Payments, deposits and cancelling

Stop before every action here: they move or keep real money. Show the rental, the amount and why.

ActionCallNotes
Record PaymentPOST storefront/rental/<name>/payment { amount, method, transactionId? }method: cash, credit_card, debit_card, check, bank_transfer, other. Payment becomes partial or paid. Tested.
Refund paymentPUT …/payment/refund { amount?, reason?, cashReturned? }Stripe, PayPal and Helcim payments with a transaction id go back through the gateway. Anything else needs cashReturned: true ("Money given back").
Hold the depositPUT …/deposit/hold { transactionId, chargeId?, paymentGateway?, method? }Records the deposit as held. A second hold is refused (409). Tested.
Keep part of itPOST …/deposit/deduction { reason, amount }Up to what is held. Posts the kept money to Books. Tested: more than held is refused ("Deduct between $0.01 and $100.00").
Refund the depositPUT …/deposit/refund { amount, skipPaymentGateway?, reason? }Up to deposit − kept − already refunded. Goes back through the gateway when it was paid online. Emails rental-deposit-refunded. Tested: $100 held, $20 kept, $90 refused, $80 refunded.
CancelPUT …/cancel { reason, refundDeposit?, refundPayment?, cashReturned? }Refused once active, returned or completed ("Cannot cancel rental in current status"). Refunds nothing unless asked. Emails rental-cancellation. Tested.

A customer can cancel their own rental but can't ask for a refund; staff refund it.

10. Books

Post to Books (PUT storefront/rental/<name>/books) posts what was paid, refunded and kept: journal_entry rows (source rental) and sf_transaction rows (invoiceType: rental). Money steps post as they happen; this catches up anything missed. Each posting is keyed <name>:<kind>:<amount>, so posting twice doesn't double it.

Check: the rental's books entries in Books.

Emails a rental sends

All go to the email on the rental (tools/templates/default-email-templates.ts):

TemplateWhen
rental-confirmation (Rental Booked)Booking made, online or by staff
rental-confirmedConfirm
rental-pickedupPicked up, or delivered
rental-checkout (Rental Started)Check-out
rental-droppedoffMark Returned
rental-returnedCheck-in
rental-deposit-refundedDeposit refund
rental-cancellationCancel

Ready, active, overdue, complete, payments, deposit holds and deductions send nothing. Edit the wording in Email and SMS templates.

When it's done

  • The config and items exist, and a quote for a sample booking reads right.
  • Online: /rental lists the items and a test booking reaches checkout with the right total.
  • Each rental's status, deposit status and payment status match what happened.
  • Paid, refunded and kept money is in Books.

What it can't do yet

  • Tax can differ. The booking saves the config's taxRate (rental.service.ts 589). Checkout charges the site's tax set-up (discount.service.ts ~2125). The rental's total can then differ from what the customer paid.
  • Restrictions are thin. Online and staff bookings pass only the email to the check (729). An age rule never fails then. A location rule that allows only listed countries refuses every booking. Licence, membership and custom aren't checked.
  • Stored but not used: the config's cancellation policy, opening days and hours, lead times, cut-offs, delivery radius, and the notifications lists. Fees and deposits don't scale with quantity.
  • No reminders and no automatic overdue. Run the lists in step 7.
  • No rentals section in My Account. Customers see them inside Orders.

Don'ts

  • Don't send the customer emails, refund, keep a deposit or cancel without a yes.
  • Don't turn on the rental site feature without a yes.
  • Don't point an item at a config name that doesn't exist.
  • Don't trust a quote alone: check availability as well.
  • Don't record a cash refund until the money has been handed back.
  • Don't check in after marking returned with a real time without checking the late fee.