Crystallize logo

Bookable products: capacity and unit pools

Sell time, not just stock. Make any Crystallize product bookable, whether it's a restaurant table, a hotel room, a rental bike or a piece of equipment. For interchangeable items, set a simple capacity. For named units like "Room 204" or "Table 12 by the window", add details your storefront can use to draw seat maps and floor plans. Policy terms are locked in when you configure a product, so reservations are never changed behind your customers' backs. It all goes live the moment you publish.

Making a product bookable turns it into a reservable resource: instead of buying a fixed quantity, a shopper reserves a slot against it — a restaurant table, a hotel room, a rental bike, a piece of equipment. This page covers how you make a product bookable in the Crystallize app and the PIM API mutations behind it. It documents configuration only; how a storefront reads the published booking configuration and takes reservations is covered in the Booking APIs documentation.

Booking builds on a booking policy — the reusable set of time rules (buffers, cancellation window, how far ahead a booking may be made, how long a hold lives) that govern reservations. Policies are created and edited separately, in the tenant's booking-policy settings, and one product references one policy. This page assumes a policy already exists.

Enabling booking on a product

Open the product in the catalogue. Booking is a dedicated section on the product page titled Booking, alongside the product's other content. Before booking is enabled the section shows a single card: a short explanation and an Enable button. Nothing is configured until you enable it.

Choosing Enable turns the same card into the booking editor: a policy picker at the top, a remove menu in the card's top-right corner, and the pool editor below. There is no separate save button — edits in this section autosave a moment after you stop typing, driving the page's usual saving indicator. If the tenant has no booking policies yet, the picker shows a prompt with a link to create one in the booking-policy settings; you need at least one policy before a product can be made bookable.

Choosing a booking policy

Pick a policy from the dropdown. Each option previews the policy as a small timeline so the right one is recognisable by shape, and the selected policy is drawn in full beneath the field.

When you choose a policy, the product takes a frozen snapshot of that policy's durations — not a live link to it. From that point the product keeps the terms it was configured under. Editing the policy afterwards does not reach products that already reference it; each one keeps its snapshot until someone explicitly re-accepts the new terms (see policy drift, below).

note

Snapshot, not a live reference

This is deliberate: a reservation must be governed by the terms in force when it was made, so a policy edit never silently changes products (or live reservations) behind the shopper's back. It also means a policy edit does not reach the storefront until each affected product is re-accepted and republished.

Defining the pool

The pool is what actually gets reserved. A product has exactly one kind of pool — a capacity or a list of named units, never both. A control at the top of the pool editor switches between the two. Switching to a kind that already holds data asks you to confirm first, because it discards what the other side held — the units you defined, or the capacity you set.

One direction is also restricted after publishing: moving a published capacity pool to units is refused, because reservations already taken against a capacity pool carry no unit and would stop being counted, letting the same slot sell twice. Capacity to units is safe only once the capacity pool's booking has been cleared, published, and its outstanding reservations drained; until then the API returns BookablePoolKindChangeError (see the API section below). The reverse — units to capacity — is always allowed.

Capacity pool

A capacity pool is a single positive whole number: how many interchangeable slots exist for the same time window. The slots have no identity — a reservation takes "one of N", not a specific one. This is the right model when the resources are truly fungible and a shopper does not care which they get: N identical rental bikes, N covers at a single seating, N parallel workshop places.

Unit pool

A unit pool is a list of named units that a shopper or an operator can pin a reservation to. Use it when units are distinguishable and the choice matters: table 12 by the window, room 204, court A.

Each unit has an id that must be non-empty and unique within the product, plus optional key/value metadata. Add units with the Add unit button; a unit card carries a badge when it has metadata and expands to reveal a key/value table where you add, edit, and remove pairs.

Metadata is for the storefront. Crystallize stores it and serves it verbatim; it does not interpret it. A storefront reads it to render and let shoppers pick a specific unit — a floor, a seat number, x/y coordinates for a seat map, a room feature like "sea view" or "accessible". Anything a storefront needs to draw a plan or describe a unit lives here.

When the policy changes: policy drift

Because a product holds a frozen snapshot, editing its policy afterwards leaves the product on the old terms. When the section detects that the referenced policy's durations now differ from the product's snapshot, it shows a drift notice: which terms changed, from what to what, and a Reapply policy action. (A change to the policy's name alone does not raise the notice — only its durations affect a booking.)

Reapplying re-freezes the current policy terms onto the product and leaves the pool exactly as it is. It is deliberately a separate, explicit action rather than something a product save does silently: accepting new booking terms is a decision, and it should not ride along on an unrelated edit. Reapplying is also the only thing that clears the drift notice. Like any other edit, the re-frozen terms reach the storefront on the next publish.

Removing booking from a product

To stop a product being bookable, open the card's menu in its top-right corner and choose Remove booking. The app asks you to confirm, then clears the configuration and returns the section to its empty, Enable state. As with every change here, removal reaches the storefront only when you publish.

Publishing

Booking configuration is part of the product's content and reaches a storefront only when the product is published, per language. Until you publish, the storefront sees no booking on the product and reservations are refused. This applies to every change: enabling booking, switching pool kind, editing units, reapplying a policy, and removing booking all take effect on the storefront at the next publish of that language.

warning

Unpublishing disables booking

A storefront reads only published data. Unpublishing a product removes its booking from the storefront and new reservations against it are refused.

Permissions

Access to a product's booking configuration is governed by two permissions on the product-bookable resource:

  • Read — required to see the Booking section at all. Without it the section is hidden.
  • Update — required to change anything: enabling booking, picking a policy, editing the pool, reapplying the policy, and removing booking. With read only, the section is visible but every control is disabled.

These are separate from the product's own read/update permissions and from the booking-policy permissions used to manage policies. A custom role created before booking existed will not have them granted; add the product-bookable read and update permissions to that role. The booking mutations are not behind a feature flag — RBAC is the gate.

API

Everything the Booking section does maps to three PIM mutations: setBookable, clearBookable, and reapplyBookablePolicy. Each takes the product id and a language, and returns a result union — select the branches you care about with inline fragments. Reading a product's booking configuration back from a storefront is not one of these; it is a Catalogue or Discovery API query, documented in the Booking APIs documentation. The PIM mutations here are authoring only.

setBookable

Makes a product bookable, or overwrites its existing configuration. The input carries the policy id plus exactly one of capacity or units — supplying both or neither returns InvalidBookablePoolError. It resolves the policy at write time and stamps the snapshot of its durations onto the product.

mutation SetBookable($id: String!, $language: String!, $input: BookableInput!) {
  setBookable(id: $id, language: $language, input: $input) {
    ... on Product { id name }
    ... on BookingPolicyNotFoundError { policyId message }
    ... on BookingPolicyTenantMismatchError { policyId expectedTenantId }
    ... on InvalidBookablePoolError { reason message }
    ... on BookablePoolKindChangeError { productId message }
    ... on BasicError { message errorName }
  }
}

Capacity pool — five interchangeable slots:

{
  "id": "{productId}",
  "language": "en",
  "input": { "policyId": "{policyId}", "capacity": 5 }
}

Unit pool — named units, ids unique and non-empty, metadata optional:

{
  "id": "{productId}",
  "language": "en",
  "input": {
    "policyId": "{policyId}",
    "units": [
      { "id": "room-a" },
      { "id": "room-b", "meta": [{ "key": "floor", "value": "2" }] }
    ]
  }
}

Error results:

  • BookingPolicyNotFoundError — no policy with that id exists.
  • BookingPolicyTenantMismatchError — the policy belongs to another tenant.
  • InvalidBookablePoolError — the pool is malformed: neither or both of capacity/units supplied, a capacity that is not a positive integer, an empty unit list, or a unit with an empty or duplicate id.
  • BookablePoolKindChangeError — the product's pool would move from capacity to units while the capacity pool is still published. Clear the configuration, publish the clear, and drain the outstanding reservations before configuring a units pool.

clearBookable

Removes booking from a product, returning the product. As with any edit, publish afterwards for the removal to reach the storefront.

mutation ClearBookable($id: String!, $language: String!) {
  clearBookable(id: $id, language: $language) {
    ... on Product { id name }
    ... on BasicError { message errorName }
  }
}

reapplyBookablePolicy

Re-freezes the referenced policy's current terms onto the product and carries the pool through untouched. Use this to accept a policy change — do not re-run setBookable just to pick up new terms; it would force you to restate the whole pool. This is the API behind the app's Reapply policy action.

mutation ReapplyBookablePolicy($id: String!, $language: String!) {
  reapplyBookablePolicy(id: $id, language: $language) {
    ... on Product { id name }
    ... on ProductNotBookableError { message }
    ... on BookingPolicyNotFoundError { policyId message }
    ... on BookingPolicyTenantMismatchError { policyId expectedTenantId }
    ... on ItemNotFoundError { message }
    ... on ItemDoesNotBelongToTenantError { message }
    ... on BasicError { message errorName }
  }
}

Error results:

  • ProductNotBookableError — the product has no booking configuration to reapply to.
  • BookingPolicyNotFoundError — the referenced policy no longer exists.
  • BookingPolicyTenantMismatchError — the policy belongs to another tenant.
  • ItemNotFoundError — no product with that id exists.
  • ItemDoesNotBelongToTenantError — the product belongs to another tenant.
note

id is a String, not an ID

setBookable, clearBookable, and reapplyBookablePolicy declare their id argument as String!, unlike the item mutations beside them. A copy-pasted $id: ID! variable declaration fails validation — use $id: String!. The language argument is required on all three.