
Catalog > Price Tables
Definition
Where do I use it?
- Rental rate cards — Define
1 day @ €40,3 days @ €100,1 week @ €180, etc. - Sales pricing — Set the outright sale price for a Listing, optionally with a compare-at price
- Subscription plans — Configure commitment period, payment cycle and auto-renewal cadence
- Promotional / seasonal pricing — Attach an extra dated Price Table whose rows are considered inside its window (a cheaper promotional row is selected over the default — see Price resolution)
- Per-variant overrides — Adjust each pricing row by a
rateMultiplierfor specific variant values - Bulk re-pricing — Maintain one shared table and link it to many Listings in a category
Price Table model
A shared default Price Table returned by the API looks like this:catalogItemId is null on shared tables not bound to one Listing, and availabilityRange is null on a default table that is always valid.
A single Price Table holds three orthogonal sets of rows — booking, sales and subscription — gated by bookingsEnabled, salesEnabled and subscriptionsEnabled. Disable the modes you don’t sell to keep the editing surface focused.
Shared vs. standalone
link-catalog-item, unlink-catalog-items) so a Listing keeps a clean record of which shared tables apply.
Pricing row types
A Price Table contains three row types, each driving a different purchase mode.Booking pricing rows
Used when the order is a rental booking.Sales pricing rows
Used for outright sale orders.Subscription pricing rows
Used for subscription-mode orders. See Subscriptions for the full billing lifecycle.Per-variant pricing
If a Listing has variants, a Price Table can carry an optionalvariants array of CatalogItemVariantPricingTable entries. Each entry binds a variantName plus one or more values to a rateMultiplier and per-row multipliers for booking, sales and subscription rows.
A CatalogItemVariantPricingTable entry has the following fields:
basePricingRowId) and applies a rateMultiplier (and optional additionalRateMultiplier) on top of that base row’s price. A multiplier of 1.0 matches base; 1.2 adds 20 %; 0.8 discounts by 20 %.
The admin marks any combination with a CatalogItemVariantPricingTable entry as “Variant pricing” instead of “Base”.
Starting at pricing
Every booking pricing row has atimeBasis field that controls how duration is counted for billing. Two modes exist:
How “Starting at” billing works
The pricing engine identifies each calendar period the booking touches on the row’s own grid. A day unit is always midnight to midnight, an hour unit is clock-hour to clock-hour, and a week unit starts on the tenant’s configured first day of the week. Each touched period is billed as one unit — unless a finer rate covers the booked portion of that period more cheaply. The engine picks the cheapest valid composition recursively. Example — day and week rates: A 10-day booking starting Thursday, with a day rate of €50 and a week rate of €250:- Thursday through Sunday: 4 started days = €200
- Monday through the following Sunday: 1 started week = €250
- Total: €450
- Monday: 1 started day = €100
- Tuesday: 1 started day = €100
- Total: €200 (2 calendar days)
”Starting at” for fixed-duration packages
Fixed-duration booking rows also supporttimeBasis. A capped fixed package anchors the return date to the calendar period rather than to the exact pickup time.
A “2 days” fixed package picked up on Monday at 14:00:
- Elapsed (
rolling): return Wednesday at 14:00 — exactly 48 hours later - Started (
capped): return on Tuesday by the last available return time — the booking touches Monday and Tuesday, so two calendar days are covered
Constraints
- Sub-hour units always bill rolling. Rows using
secondsorminutesare not affected by thetimeBasissetting. They coexist freely in a “Starting at” table. - All rate-based rows on a table share one
timeBasis. The admin enforces this with a single “Starting at pricing” checkbox that applies to all eligible rate-based rows. Fixed-duration rows each have their own independenttimeBasissetting. - Week rates use the tenant’s week start day. A Saturday–Sunday booking counts as one week with a Monday start (both days are in the same Mon–Sun week), but as two weeks with a Sunday start.

Rate-based booking pricing with the Starting at pricing checkbox

Duration options on a fixed pricing row
Price resolution
availabilityRange controls which tables are candidates for a booking — it does not give a table priority. There is no per-table priority or override field. When a price is calculated:
All pricing calculations run in the Location’s timezone. Day boundaries, week boundaries, weekdays constraints, and timeOfDayRange windows are evaluated against the location’s local clock — not UTC or the customer’s timezone. A booking near local midnight is matched to the correct local day, and DST transitions are handled by calendar arithmetic in the location zone.
- Collect the active tables. A table is active when it has no
availabilityRange, or its range overlaps the booking dates. The default table (no range) is therefore always active alongside any dated table whose window the booking falls in. - Pool the rows. Every enabled row from every active table is gathered into a single candidate pool. The engine does not first pick one “winning” table — rows from different tables compete directly.
- Select the row(s).
- Rate-based (dynamic) bookings — how the pool is resolved depends on the time basis of the rows in it. See Rate-based pricing for the walk in full, and Longest-fits-first vs cheapest composition below — the two bases use genuinely different algorithms, and the difference decides whether an over-priced long row can be avoided. Under both, a row’s
weekdays,timeOfDayRangeandavailabilityRangeconstraints must pass before it is eligible for a segment, and any remainder falls to shorter rows and the additional-time rules. - Fixed-price bookings — the shopper selects a specific rate row (for example,
1 day @ €40) and that row’s price is used directly.
- Rate-based (dynamic) bookings — how the pool is resolved depends on the time basis of the rows in it. See Rate-based pricing for the walk in full, and Longest-fits-first vs cheapest composition below — the two bases use genuinely different algorithms, and the difference decides whether an over-priced long row can be avoided. Under both, a row’s
- Apply variant multipliers. If a
CatalogItemVariantPricingTableentry matches the selected variant value combination, multiply the row’s contribution by the row-level multiplier (and the table-levelrateMultiplier). - Return totals. The endpoints
…/rate-based-priceand…/fixed-pricereturn apriceBreakdownplustotalPrice, with each contribution itemised for the storefront and order summary.
Longest-fits-first vs cheapest composition
Rate-based resolution is not one algorithm. Which one runs is decided by the time basis of the rate-based rows in the pool. Elapsed duration (rolling, the default) — longest-fits-first. The engine walks the booking from its start. At each step it takes the longest row that still fits the remaining time, advances by that row’s duration, and repeats. Price is only a tiebreak between rows of equal duration. The engine never compares a long row against a composition of shorter ones, so:
- A week row wins a seven-day booking whatever it costs — even priced above seven day-rows.
- Two 1-day rows at €100 and €150 resolve to €100, because they are the same duration.
- A 1-day row at €150 and a 12-hour row at €10 resolve to the day row, because it is longer.
capped) — cheapest composition. The engine works on calendar periods instead. For each period the booking touches, it bills either one unit of that period or the cheaper finer composition of the booked portion — whichever costs less — and it compares those options recursively across every granularity on the table.
Here price does decide across durations: a week row priced above seven started days loses to the day rows.
Applying tables to Listings and Collections
A shared Price Table is linked to a Listing through dedicated endpoints:POST /pricing-tables/pricing-table/:pricingTableId/link-catalog-item— link one ListingDELETE /pricing-tables/pricing-table/:pricingTableId/unlink-catalog-items— bulk unlinkPOST /pricing-tables/add-pricing-table-to-catalog-item— discriminated union for the four linking modes:mode = 'link'— link to an existing shared tablemode = 'create'— create a new standalone table inlinemode = 'sharedToShared'— replace one shared link with anothermode = 'sharedToStandalone'— fork a shared table into a standalone one on the Listingmode = 'standaloneToShared'/'standaloneUpdate'— flip ownership or rename in place
listCatalogItems and link the table to each Listing.
Key Properties (Price Table)
Relationships
Linked to one or more Listings
Linked to one or more Listings
Holds booking, sales and subscription rows
Holds booking, sales and subscription rows
Overlays variant pricing on a Listing's variants
Overlays variant pricing on a Listing's variants
Evaluated against the Listing's purchase mode at checkout
Evaluated against the Listing's purchase mode at checkout
…/rate-based-price or …/fixed-price endpoint to compute the total.Lifecycle
Create
POST /pricing-tables/ with label and (for shared tables) isDefault: true. New tables start with empty row arrays.Can I make a dated table the default?
Can I make a dated table the default?
availabilityRange = null can be isDefault = true (enforced by the admin UI; the model permits the flag but the link logic skips dated tables when resolving the default).Add rows
/pricing-table/:id/row, …/sales-row, …/subscription-row). Add per-variant overrides via /pricing-table/:id/variant-pricing-table.Link to a Listing
add-pricing-table-to-catalog-item with the right mode. The Pricing tab on a Listing shows linked tables in a sidebar and lets you create new ones inline.Override per variant
CatalogItemVariantPricingTable block for the variant values that need different pricing.Promote or replace
Delete
DELETE /pricing-tables/delete-table/:pricingTableId. Removing a shared table unlinks it from every Listing.Frequently asked questions
Can one Listing have several Price Tables?
Can one Listing have several Price Tables?
availabilityRange decides which tables are candidates, not which one wins.Where does the rental period price come from?
Where does the rental period price come from?
How do I run a 20 % weekend promotion?
How do I run a 20 % weekend promotion?
availabilityRange covering the weekend, with rows priced 20 % below the default, and link it to the affected Listings. Because the promotional rows are cheaper and cover the same durations, the engine selects them inside the window and falls back to the default afterwards.This works precisely because the promo is cheaper — see the next question for higher/surcharge pricing.Why isn't my higher seasonal / holiday price being applied?
Why isn't my higher seasonal / holiday price being applied?
availabilityRange to exclude those dates, so only the higher row covers them.What is the difference between rolling and Starting at pricing?
What is the difference between rolling and Starting at pricing?
capped) counts every calendar period the booking touches. A “day” is midnight to midnight. The same Monday 14:00 to Wednesday 14:00 booking touches Monday, Tuesday, and Wednesday — 3 started days.Use “Starting at” when you charge per calendar day (like parking garages or hotel nights). Use rolling when you charge for exact elapsed time (like hourly equipment rental).Can prices differ per customer segment?
Can prices differ per customer segment?
Which timezone does the pricing engine use?
Which timezone does the pricing engine use?
Are existing orders re-priced when I edit a table?
Are existing orders re-priced when I edit a table?
What's the difference between priceContribution and totalPrice in the response?
What's the difference between priceContribution and totalPrice in the response?
priceContribution is what one row added to the total; totalPrice is the sum across all rows the engine selected (base row + additional-time rows + variant multipliers).Developer Reference
Price tables are exposed aspricing-tables in the API.