{
  "openapi": "3.0.3",
  "info": {
    "title": "Partner Booking Rules API",
    "version": "1.6.0",
    "contact": {
      "name": "Bookable",
      "url": "https://www.bookabletech.com",
      "email": "hello@bookabletech.com"
    },
    "description": "## Overview\n\nOperators (venue groups) can attach **booking condition rules** to their venues. The platform enforces these\nrules, server-side and deterministically, on every partner **booking create** (`POST /venues/{compositeId}/booking`)\nand **booking amend** (`PUT` / `PATCH /venues/bookings/{bookingId}`). A booking that breaks a rule is rejected with\nHTTP 400 and an error code `BKRUL-N-001` ... `BKRUL-N-006` or `BKRUL-N-100`; one rule (`route_to_enquiry`, or an\nexpression rule that sets `booking.type`) instead turns the booking into an **enquiry**.\n\nA third kind of expression rule does not gate a booking at all: a rule at the **`payment` hook** (moment\n`booking_payment`) changes the **amount payable** for a booking that already exists, when the platform computes its\npayment options (`GET /venues/bookings/{bookingId}/payment-options`) or creates its payment\n(`POST /venues/bookings/{bookingId}/payments`). See \"Rules at the `payment` hook\" below.\n\nThis API is **read-only and advisory**. `GET /booking-rules` returns every booking condition rule that is\n**active** for the calling partner, with enough structure and documentation for an integration, in particular one\nwritten by an AI agent, to understand which conditions the platform enforces and to avoid hitting them.\n**The booking endpoints are authoritative**: the platform always re-validates every booking server side, whatever\nthis API returned.\n\n**Which rules are returned.** Only rules that are `active` (inactive rules are never returned) and that apply to the\ncalling partner, i.e. rules that have **no partner target at all**, or that name **your partner**, or that name\n**one of your partner categories** (same partner semantics the booking path uses). An expression rule can also aim\nat partners inside its `when` (facts `partner.id`, `partner.categoryIds`): it is returned only when, with your own\npartner facts, its `when` can still be true (see Confidentiality). Rules aimed at other partners are never\nreturned; `appliesTo.partnerScope` only says *how* the rule is aimed at you through its partner targets\n(`all_partners`, `you`, `your_categories`).\n\n**Which venues are shown.** `appliesTo.venues[]` lists only the venues **you can see** (the venues `GET /venues`\nreturns to you; if your partner mapping restricts an operator to some venues, only those). A rule restricted to\nvenues none of which you can see is **not returned at all**: its restriction can never include a venue you book.\n`allVenues: false` with a non-empty `venues[]` therefore means \"only these of **your** venues\".\n\n**Who is \"you\".** The partner is identified from your credentials (OAuth2 client credentials). Bookable Agents\n(scope `agent:write`) send `X-Partner-Reference` to act for a partner, exactly as on the other partner endpoints.\nThe response field `caller` repeats your own partner id and partner category ids: those are the values of the facts\n`partner.id` and `partner.categoryIds` that expression rules read. The categories you belong to differ per operator\n(an operator can also place you in a category through its own third-party records), so the value of\n`partner.categoryIds` for a rule is `caller.categoryIdsByOperator[rule.operatorId]`, not the global\n`caller.partnerCategoryIds`.\n\n## Quick start\n\n```bash\n# 1. Get a token (OAuth2 client credentials; the client must hold the venue:read permission).\ncurl -s -X POST https://auth.bookabletech.com/oauth/token \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"grant_type\":\"client_credentials\",\"client_id\":\"YOUR_CLIENT_ID\",\"client_secret\":\"YOUR_CLIENT_SECRET\",\"audience\":\"api.bookabletech.com\"}'\n\n# 2. Every active rule that applies to you (no parameter is required).\ncurl -s 'https://api.bookabletech.com/booking-rules' \\\n  -H 'Authorization: Bearer ACCESS_TOKEN' \\\n  -H 'Accept: application/json'\n\n# 2b. Bookable Agents (scope agent:write) act for a partner by sending X-Partner-Reference.\ncurl -s 'https://api.bookabletech.com/booking-rules' \\\n  -H 'Authorization: Bearer ACCESS_TOKEN' \\\n  -H 'X-Partner-Reference: acme-travel'\n```\n\nThe answer is a JSON object `{generatedAt, caller, rules[], hooks[]}`; the named 200 examples of this operation show it in full.\nThe filters are all optional and combined with AND (a `|` inside a venue id is sent as `%7C`):\n\n| I want | Request |\n|---|---|\n| Everything that applies to me | `GET /booking-rules` |\n| Only operator 17 | `GET /booking-rules?operatorId=17` |\n| Only the rules for the venue I am about to book (venue id, or the 4-segment product `compositeId`) | `GET /booking-rules?venueId=17%7CX9%7C9d3f0b6e7a1c4e58b2c4f1a60d7e8b93` |\n| Only deposit rules | `GET /booking-rules?effect=require_deposit` |\n| Only what is checked when I create a booking | `GET /booking-rules?hook=booking.create` |\n| Only what is checked when I amend a booking | `GET /booking-rules?hook=booking.amend` |\n| Only the rules that can change the amount payable for an existing booking | `GET /booking-rules?hook=payment` |\n| All filters together | `GET /booking-rules?operatorId=17&venueId=17%7CX9%7C9d3f0b6e7a1c4e58b2c4f1a60d7e8b93&effect=same_package&hook=booking.create` |\n\n## Confidentiality\n\nOnly rules that can apply to you are returned. This includes expression rules that target partners inside `when`:\nthe platform evaluates the `partner.*` part of `when` with your own partner id and categories (every other fact\nunknown) and leaves the rule out when `when` can never be true for you. A rule aimed at several partners, you\namong them, is returned.\n\nFor the rules that are returned, expression trees (`when`, `then`) and the operator-written text (`name`,\n`description`, `message`) are returned exactly as the operator stored them. They can therefore contain numeric ids\n(for example the other partner or partner category ids of a rule that also applies to you, compared with\n`partner.id`) or words chosen by the operator. The platform itself never adds\nthe name of another partner or of another partner category to any field of the response: the generated `summary`\nnames neither partners nor partner categories, and `appliesTo` only says *how* a rule is aimed at you\n(`partnerScope`). Do not treat the content of `when`, `then`, `name`, `description` or `message` as a statement\nabout you or about any other partner.\n\n## Integration guide for AI agents\n\n1. **Fetch the rules.** Call `GET /booking-rules`. Optional filters: `operatorId` (one operator), `venueId` (only\n   rules that apply to that venue), `effect`, `hook`. Read `caller` (your partner id and categories per operator), `rules[]` and\n   `hooks[]`. The list is not paginated: you get the whole active set that applies to you.\n2. **Keep the rules that can concern your booking**, using `rules[].appliesTo`:\n   - venue: `allVenues` is true, or the first three segments of the 4-segment `compositeId` you book\n     (`{operatorId}|{rmsSlug}|{venueResourceId}`) equal one of `venues[].id` (`venues[]` lists only venues you can\n     see; a rule restricted to venues you cannot see is not returned). With `venueId` the server already did this.\n   - product (booking type): `allProducts` is true, or the `slug` of the product you book (the `slug` field of the\n     product in `GET /venues`) equals one of `products[].slug`. A rule with a product list never applies to a product\n     that has no slug.\n   - `conditions[]`, if any: **all** must hold (see \"Field conditions\").\n   - `partnerScope` already tells you the rule targets you (or everybody); you do not need to filter on it.\n   - `whenApplied`: keep rules whose list contains the operation you are about to call (`booking_create` for\n     create, `booking_amend` for PUT/PATCH). `booking_payment` is not an operation you send a request body to: it\n     means the rule runs when the platform computes the amount payable for a booking that already exists (the\n     payment options and the payment creation endpoints). Such a rule never gates your create or amend, and you\n     **cannot** reproduce its facts (`payment.bands`) from the booking request alone: do NOT try to evaluate it\n     client-side as a pass/fail gate. Use it to understand that the final balance may be the sum of the\n     minimum-spend bands the booking crosses, and read the authoritative amount from\n     `GET /venues/bookings/{bookingId}/payment-options` (operationId `getPaymentOptions` of the Payment API).\n3. **Fixed effects** (every effect except `expression`): apply the exact semantics in the effects table below to the\n   booking you intend to send, using `params`, `appliesTo.items` and the pre-order lines.\n4. **Expression rules** (`effect: expression`): pick the rules whose `hook` matches the operation (`booking.create`\n   for create, `booking.amend` for amend; the rules of hook `payment` are not evaluated by you, see step 2 and\n   \"Rules at the `payment` hook\"). Look the hook up in `hooks[]`. Build the **fact document** (a JSON object\n   with the top-level keys `booking`, `partner`, `product`, `venue`, `space`) by following `hooks[].facts[].howToObtain`\n   for every fact. Evaluate `when` against it using the language reference below. Evaluate the matching rules in the\n   order they appear in `rules[]` (priority ascending, then rule id); a `set` written by an earlier rule is visible\n   to the later rules.\n5. **Interpret the outcome.** If `when` is true, the `then` actions apply; `rules[].outcomes` already lists them\n   so you need not parse `then`: `rejects` (the booking will be refused with `BKRUL-N-100` and `rejectMessages`),\n   `writes` (values the platform will write, e.g. `booking.type = request` makes the booking an enquiry; at the\n   `payment` hook the only write is `payment.finalBalance`, the amount payable) and `flags` (named annotations without\n   any behaviour). If `when` is false the rule does nothing. If a fact is missing or has\n   the wrong type for an operator, the rule is **skipped** (does not apply), it never fails the booking.\n6. **Change the booking** (party size, date, space, pre-order lines, deposit) until no rule rejects it, or accept the\n   enquiry outcome.\n7. **When you are rejected**, read `violations[]` of the 400 response and match each `violations[].ruleId` with\n   `rules[].id` from this API: it is the **same id**. Then fix what that rule says (`summary`, `message`, `params`).\n8. **Re-fetch** whenever you start a new booking flow. Rules can be changed, added or deactivated by the operator at any\n   time; this API offers no change notification and no cache validators, so do not assume that a previous answer\n   is still current. The result is a pre-check: always handle a `BKRUL-*` rejection from the booking endpoint\n   as the final word.\n\n## Glossary\n\n| Term | Meaning |\n|---|---|\n| Operator / venue group | The company that owns the venues. `operatorId` is its numeric id (the first segment of every composite id). |\n| Rule | One booking condition configured by an operator. Identified by a UUID (`id`). |\n| Effect | The kind of rule. Seven fixed effects with fixed semantics plus `expression` (a rule defined by data). |\n| Hook | A named point in the flow where expression rules are evaluated: `booking.create` (before the booking is sent to the operator's system), `booking.amend` (before an amend) and `payment` (when the platform computes the amount payable for an existing booking). |\n| Final balance | The total amount payable for an existing booking, in integer pence (GBP), as the platform computes it when it builds the payment options. A rule at the `payment` hook can set it (`payment.finalBalance`). The 25% deposit option is computed on it. |\n| Minimum-spend band | A time window of the booked space with its own minimum spend (configured by the operator). `payment.bands` lists the bands that the booking interval [start, start + duration) overlaps. |\n| Fact | A named value an expression can read with `{\"var\": \"<path>\"}` (for example `booking.partySize`). Facts are derived from your request; `hooks[].facts[].howToObtain` says how. |\n| Expression (`when`) | A boolean condition written as JSON (JsonLogic format). |\n| Action (`then`) | What happens when `when` is true: `set` a writable value, `reject` the booking, or `flag` it. |\n| Enquiry | A booking submitted as `type: request` instead of an instant confirmation: the operator must accept it. |\n| Pre-order line | One package or menu in `preorders` of a booking, with a quantity. Rules identify packages, menus and products by their **aggregation slug** (the `slug` field in `GET /venues`), not by the venue-specific id. |\n| Composite id | `{operatorId}\\|{rmsSlug}\\|{venueResourceId}` identifies a venue (3 segments); `{operatorId}\\|{rmsSlug}\\|{venueResourceId}\\|{productId}` identifies a bookable product (4 segments). |\n| Party size | The `partySize` of the request (children are not added to it). |\n| `daysAhead` | Whole calendar days from today (Europe/London) to the booking date. |\n| Violation | One rule that rejected a booking, reported in `violations[]` of the 400 error. |\n\n## Field guide: what to do with each field\n\n| Field | What it is | What an integration should DO with it |\n|---|---|---|\n| `generatedAt` | UTC instant at which the server built the answer. | Informational. There is no cache validator and rules can change at any moment: re-fetch at the start of every booking flow instead of comparing timestamps. |\n| `caller.partnerId` | Your own partner id. | Copy it into the fact `partner.id` when you evaluate expression rules. It is NOT a literal you read inside a rule's `when`: those are whatever the operator wrote. |\n| `caller.categoryIdsByOperator` | Object keyed by operator id (as a string): the ids of the partner categories you belong to **for that operator** (your own categories plus those the operator gives you through its own third-party records). One entry for every operator that has at least one rule in `rules[]`; empty object when `rules` is empty. | When you evaluate an expression rule, copy `caller.categoryIdsByOperator[String(rule.operatorId)]` into the fact `partner.categoryIds`. This is the same value the server uses to filter the rules. |\n| `caller.partnerCategoryIds` | The partner categories assigned directly to you, the same for every operator (global). Kept for backward compatibility. | Do not use it as `partner.categoryIds` when `caller.categoryIdsByOperator` has an entry for the rule's operator: it can miss categories the operator gives you. Use it only as a fallback when the entry is missing. |\n| `rules[].id` | Rule UUID. | Keep it: the same value comes back as `violations[].ruleId` and `analyticsRuleId` in a 400 rejection. |\n| `rules[].operatorId` | Operator (venue group) that owns the rule. | Keep the rules whose `operatorId` equals the first segment of the composite id you book. |\n| `rules[].effect` | Kind of rule: one of 9 values. | Every effect except `expression` is a fixed effect: apply the effects table. `expression`: evaluate `when` and apply `then`. |\n| `rules[].appliesTo` | Venue, product and partner scope, plus the item set `items`. | Filter as in step 2 of the integration guide. `items` is the set the effect works on, not a scope test. |\n| `rules[].conditions` | Extra request-field filters (usually empty). | All entries must hold, otherwise the rule does not apply to this booking. |\n| `rules[].params` | Parameters of a fixed effect, defaults already applied. | Plug them into the formulas of \"How to evaluate each fixed effect\". Empty for expression rules. |\n| `rules[].message` | The operator's own refusal text (optional). | Show it to your user; when absent the default message of the effect is what the 400 will carry. |\n| `rules[].summary` | One generated English sentence. | Show or log it; never parse it. |\n| `rules[].whenApplied` | Moments at which the rule runs: `booking_create`, `booking_amend`, `booking_payment`, `booking_cancel`. | Keep the rule only if the list contains the operation you are about to call (`booking_cancel`: `DELETE /venues/bookings/{bookingId}`, and the cancel of a Listings order line). `booking_payment` is the moment the platform computes the amount payable for an existing booking: it is not a request you send, and the rule is not a gate for your create or amend (see \"Rules at the `payment` hook\"). |\n| `rules[].consequence` | What happens when the rule is violated or triggers. | `rejects_booking`: change the booking until the condition holds. `requires_deposit`: collect the deposit and send it as `depositAmount`. `turns_booking_into_enquiry`: nothing to fix; the booking becomes an enquiry (book further ahead to avoid it). `skips_preorder_email`: nothing to fix; below `params.threshold` guests the Collins pre-order email is not sent, the pre-order is still posted. `conditional`: read `outcomes` (for a `payment` rule: it writes the amount payable, it never refuses). |\n| `rules[].errorCode` | The `BKRUL-N-xxx` code the booking endpoint returns when the rule rejects (`AVAIL-N-007` / `AVAIL-N-008` for a `booking_window` with `action: book`). | Use it to recognise the rule type in a rejection. Absent when the rule can never reject (always absent for a `payment` rule). |\n| `rules[].hook` | Expression rules: the point of the flow where the rule runs (`booking.create`, `booking.amend`, `payment`). | Look the hook up in `hooks[]` (same `name`) to know which facts it reads. For `payment` you do not build the facts: the platform computes them from the stored booking. |\n| `rules[].priority` | Expression rules: evaluation order inside the hook, ascending, ties by id. | Nothing to sort: `rules[]` is already in evaluation order. A `set` from an earlier rule is visible to the later ones. |\n| `rules[].when`, `rules[].then` | Expression rule condition and actions, JSON. | Evaluate with the expression language reference; `then` only runs when `when` is true. |\n| `rules[].outcomes` | What `then` does, already parsed. | `rejects` with `rejectMessages`: the booking would be refused (`BKRUL-N-100`); always false at the `payment` hook, where a `reject` action has no effect. `writes`: values the platform writes (`booking.type = request` makes an enquiry; `payment.finalBalance` sets the amount payable). `flags`: named annotations without behaviour. |\n| `hooks[].facts[].howToObtain` | How to compute that fact from the request you send (create) or the booking you amend (amend). | Follow it literally. The server keeps one text per fact for create and one for amend and returns the one that belongs to the hook in `howToObtain`; there is no separate `howToObtainOnAmend` field. |\n| `hooks[].writablePaths` | The only paths a `set` action can write at that hook. | Informational: it tells you which consequences an expression rule can have (`booking.type`, `booking.enquiryReason`, `booking.sendPreorderEmail` at `booking.create`: `false` skips the Collins pre-order email, the pre-order itself is still posted; none at `booking.amend`; `payment.finalBalance` at `payment`: the total payable in integer pence, GBP, a number from 0 up, and the 25% deposit option is computed on it). |\n\n## Effects\n\n| Effect | What it enforces | Params | Applies on | Error code | Default message when the rule has no `message` |\n|---|---|---|---|---|---|\n| `same_package` | Let S be the rule's item set (`appliesTo.items`, packages and menus compared by slug). Nothing happens unless at least one chosen pre-order line is in S. With `exclusive` true (default): violated if more than one distinct S item is chosen, or if any chosen line is outside S (\"if this one is chosen, everybody has that same one and nothing else is added\"). With `exclusive` false: violated only if more than one distinct S item is chosen; lines outside S are ignored. If not violated and `wholeGroup` is true, the summed quantity of the chosen S lines must equal the party size, otherwise it is violated (same code). | `exclusive`, `wholeGroup` | create, amend | `BKRUL-N-001` | exclusive: \"If this package is chosen, everyone must have the same one and nothing else can be added.\" non-exclusive: \"Everyone in the party must choose the same item from this set — selections cannot be mixed.\" wholeGroup: \"Once chosen, this item's quantity must equal the party size (N).\" |\n| `max_quantity_party_size` | Total quantity of the chosen lines in S may not exceed the party size. An empty S means every chosen package and menu counts. With `perItem` true, each item's own quantity (lines of the same slug added together) may not exceed the party size instead of the total. | `perItem` | create, amend | `BKRUL-N-002` | \"The total quantity of these items (T) cannot exceed the party size (N).\" (`perItem`: \"No single item's quantity (T) can exceed the party size (N).\", T = the largest item quantity) |\n| `max_quantity` | Total quantity of the chosen lines in S may not exceed `max`. (An empty S counts nothing and never violates.) With `perItem` true, each item's own quantity may not exceed `max` instead of the total. | `max`, `perItem` | create, amend | `BKRUL-N-003` | \"No more than M of this item is allowed per booking.\" (`perItem`: \"No more than M of each item is allowed per booking.\") |\n| `preorder_required` | If the party size is `threshold` or more (inclusive: party size >= threshold), at least one chosen pre-order line must be in S. An empty S means any package or menu qualifies. | `threshold` | create, amend | `BKRUL-N-004` | \"Groups of N or more must include a pre-order.\" |\n| `route_to_enquiry` | A booking made fewer than `leadTimeDays` calendar days ahead (`daysAhead < leadTimeDays`) is submitted as an **enquiry** instead of being confirmed. Exactly `leadTimeDays` ahead is NOT inside the window. It never rejects. | `leadTimeDays` | **create only** (an amend never un-confirms a booking) | none (consequence is `turns_booking_into_enquiry`) | not applicable |\n| `lock_preorder_edits` | Inside the window (`daysAhead < leadTimeDays`) pre-orders may not be added or changed. On create: violated when the booking carries any pre-order line. On amend: violated when the pre-order lines (slug, kind, quantity, order ignored) differ from the stored ones. Outside the window nothing is enforced. | `leadTimeDays` | create, amend | `BKRUL-N-005` | \"Pre-order changes are not permitted less than N days before the booking date.\" |\n| `require_deposit` | The booking must already carry a deposit of at least `amount` x party size (`per: person`) or `amount` flat (`per: booking`). On create the figure checked is the request `depositAmount` (absent = 0); on amend it is the deposit stored on the booking (an amend request that carries a new `depositAmount` is not credited for this check). Tolerance of half a penny. It only checks the figure: it never collects money or creates a payment link. A rule without a usable `amount` never blocks. | `amount`, `per` | create, amend | `BKRUL-N-006` | \"A deposit of £X is required for this booking (£A per person).\" (`per: booking`: \"(£A per booking)\"; X and A with two decimals) |\n| `preorder_email` | If the party size is below `threshold` (party size < threshold), the Collins pre-order email is not sent; the pre-order itself is still posted. It never rejects. On amend: when the email was skipped on create and the amended booking has a pre-order and reaches `threshold`, the email is sent once. | `threshold` | create, amend | none (consequence is `skips_preorder_email`) | not applicable |\n| `booking_window` | You may not `book`, `amend` or `cancel` (`action`) when the reservation starts in fewer than `minHoursBefore` hours, and (book only) may not book more than `maxHoursBefore` hours ahead. Hours are real elapsed time from your request to the reservation start (booking date + start time, Europe/London); exactly `minHoursBefore` away is allowed. An amend is measured from the start **stored** on the booking, so moving it further out does not reopen the window; an amend that changes the date or time must also pass the book window, an amend that doesn't is never checked against it. Amend and cancel windows apply to confirmed bookings only (a pending request can always be changed or withdrawn). When several rules match, the strictest window wins. | `action`, `minHoursBefore`, `maxHoursBefore` | `book`: create, and amend when the date or time changes. `amend`: amend. `cancel`: cancel | `book`: `AVAIL-N-007` (too close), `AVAIL-N-008` (too far ahead); `amend`: `BKRUL-N-007`; `cancel`: `BKRUL-N-008` | \"Cancellations close 5 days (120 hours) before the reservation; they closed at 2026-10-20 20:00 Europe/London.\" (`Bookings` / `Amendments` for the other actions; past the maximum: \"Bookings open N hours before the reservation; they open at ….\") |\n| `expression` | A generic rule defined by data: at `hook`, when `when` is true, apply `then`. | none (use `hook`, `priority`, `when`, `then`) | the hook's moment: `booking.create` = create, `booking.amend` = amend, `payment` = when the amount payable of an existing booking is computed (`booking_payment`) | `BKRUL-N-100` when it rejects (never at `payment`, where a `reject` has no effect) | the message of the `reject` action, else the rule's `message`, else \"Rule '<name>' rejected the booking.\" |\n\nNumbers for each effect, with one violating and one passing booking, are in \"How to evaluate each fixed effect\" below.\n\n`params` defaults applied by the platform when a key is missing: `exclusive` true, `wholeGroup` false, `threshold` 1,\n`leadTimeDays` 0, `per` person. The values returned by this API are already resolved.\n\n### Which rules apply to a booking (scope)\n\nA rule applies to a booking when ALL of these hold: it belongs to the booked operator; its venue list is empty\n(every venue) or contains the booked venue; its product list is empty (every product) or contains the booked\nproduct's aggregation slug; it has no partner target, or targets your partner id, or targets one of your categories\n(either match is enough); and every `conditions[]` entry holds. Item lists (`appliesTo.items`) are not part of\nthe scope test of any effect: they are the *set the effect works on*. For `expression` rules the item lists are\ninformational only and are not evaluated.\n\n### Field conditions\n\n`conditions[]` is an optional extra filter: each entry compares one request field with a value and ALL entries must\nhold. Fields: on create `partySize`, `duration`, `spaceId`, `isCrossSale`, `commission`, `depositAmount`,\n`hasCustomerCompany`, `hasNotes`; on amend only `partySize`, `duration`, `commission`, `hasCustomerCompany`,\n`hasNotes`; at `payment` (read from the stored booking) `partySize`, `duration`, `spaceId`, `commission`, `depositAmount`,\n`hasCustomerCompany`, `hasNotes`. A field the booking does not carry never matches, whatever the operator (even `not_equals`).\nOperators: `equals`, `not_equals`, `gte`, `lte` (numbers), `in` (the field value is one of the listed values),\n`contains` (the field value is a list containing the value). At the time of writing the operator tooling has no way\nto create conditions, so you will normally see an empty list.\n\n### Ordering and combination\n\n- Fixed rules are all evaluated; **every** fixed violation is reported in `violations[]`, and the error shown\n  (`code`, `title`, `detail`) is the one with the highest precedence: `lock_preorder_edits`, then `preorder_required`,\n  then `same_package`, then `max_quantity_party_size`, then `max_quantity`, then `require_deposit`.\n- **Expression rules are evaluated only after all fixed rules passed.** A booking rejected by a fixed rule never\n  shows expression violations. On create, a fixed `route_to_enquiry` has already set `booking.type = request` and\n  `booking.enquiryReason = operatorRule` when the expression rules run, so expressions see those values.\n- Expression rules of one hook run by `priority` ascending (default 100), then by rule id. `rules[]` is returned in\n  that order: expression rules of `booking.create`, then of `booking.amend`, then of `payment`.\n- Rules of the `payment` hook run at their own moment (see the next section), never together with the fixed rules or\n  with the rules of the booking hooks.\n- Several `set` actions on one path: they compose in that order and **the last one wins**. Every later rule\n  (its `when` and its actions) reads the facts *plus the writes made so far*.\n- A rule is **atomic**: if one of its actions cannot be carried out (for example a `set` value is not computable), the\n  whole rule is skipped and none of its rejections, flags or writes is kept.\n- Everything an expression rule cannot evaluate is silently **skipped**, never an error for you: a fact that is\n  null or missing where an operator needs a number, string, boolean or list, a type mismatch, division by zero, a\n  `when` that is not boolean, a `set` to a path that is not writable at the hook, or a limit overrun.\n\n### Rules at the `payment` hook (moment `booking_payment`)\n\nA rule with `hook: payment` and `whenApplied: [booking_payment]` does not accept or refuse a booking. It changes\n**how much is payable** for a booking that already exists. Everything below is read from the platform's payment code.\n\n- **When it runs.** Each time the platform computes the payable amount of an existing booking: when you call\n  `GET /venues/bookings/{bookingId}/payment-options` (operationId `getPaymentOptions`, Payment API) and when you call\n  `POST /venues/bookings/{bookingId}/payments` (operationId `createPayment`). It is **not** evaluated at booking\n  create or amend, and it does not change what availability or `GET /venues` show: availability lists each\n  minimum-spend band the slot's interval [start, start + duration) crosses, each with its own value and no total, and\n  `GET /venues` shows the configured rules and overrides.\n- **Preconditions.** The booked space has an enabled minimum-spend policy and the stored booking has a duration\n  greater than 0. Without them no `payment` rule is consulted and the amount is the minimum spend at the start time,\n  as before. A rule is also subject to the scope tests above (operator, venue, product, partner, `conditions[]`),\n  evaluated on the stored booking.\n- **Facts.** All facts are computed by the platform from the **stored** booking and the space's minimum-spend policy;\n  you do not send them. The new root is `payment`: `payment.bands` (the minimum-spend bands that the interval\n  [start, start + duration) overlaps, in chronological order, each band once, each with `label`, `from`, `to`,\n  `spendType`, `spendValue` in pence, `hireFeeValue` in pence and `overlapMinutes`), `payment.bandCount`,\n  `payment.minimumSpend` (the value the options use when no rule applies: the first matching entry at the start time\n  only, null when no band contains the start time) and `payment.currency` (always `GBP`). The `booking.*`, `partner.*`,\n  `product.*`, `venue.*` and `space.*` facts are those of the stored booking; `booking.items` and\n  `booking.originalItems` are always empty, `booking.isCreate` is false, and `booking.type`, `booking.enquiryReason`\n  and `booking.isCrossSale` are null at this hook. `hooks[]` documents each of them for this hook.\n- **The only write.** `payment.finalBalance`: the total payable, an integer number of pence (GBP). The value must be a\n  number from 0 up to 2147483647; anything else (negative, not a number, too large) makes the rule be skipped. A value\n  with decimals is rounded to the nearest penny (halves away from zero). If several rules write it, the last one wins\n  (priority, then rule id). A value equal to the minimum spend at the start time changes nothing.\n- **What follows from the value.** The \"full minimum spend\" option is the final balance. The \"25% deposit\" option is\n  computed on it: the final balance times 0.25, rounded to a whole penny (a half rounds to the even penny). When the\n  resulting amount is not greater than 0 the options list is empty, and `createPayment` cannot create the payment.\n- **What has no effect.** A `reject` action at this hook is ignored by the platform (it only logs a warning), so\n  `outcomes.rejects` is always false for a `payment` rule and `errorCode` is omitted. A `flag` has no behaviour here\n  either. A rule that cannot be evaluated, or an error while evaluating, keeps the amount the platform would have\n  used without the rule; it never fails the payment call.\n- **What you should do with it.** You cannot rebuild `payment.bands`: it comes from the space's minimum-spend policy\n  (rules and date overrides configured by the operator), which is not part of your booking request and is not returned\n  by this API. Do **not** evaluate a `payment` rule client-side as a gate. Read it to know that the final balance may\n  be the **sum of the minimum-spend bands the booking crosses**, and take the authoritative amount from the payment\n  options (`getPaymentOptions`): amounts are integers in pence. The amount can change if the booking is amended or the\n  operator changes the rule, so ask again before you pay.\n\n## How to evaluate each fixed effect\n\nEvery example uses operator 17, the venue `17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93` (The Rooftop Kitchen), the product\n`bottomless-brunch` unless stated, partner 482 (category 1) and \"today\" = 2026-10-01. The rules are the ones of the\n`allRules` example of `GET /booking-rules` (cited by the first 8 characters of their id). For each effect: the inputs,\nthe computation, then one violating and one passing booking. The JSON block after each effect is the same scenarios in\nmachine-readable form: `rule` (id prefix), `partySize`, `date`, `create` (false = amend), `packages` / `menus`\n(`[slug, quantity]` pairs of the final booking), `original` (stored lines on an amend), `deposit` (the request\n`depositAmount` on create, the stored deposit on amend), `product` (slug of the booked product) and `expect`. The platform's\ntest-suite runs every scenario against the real evaluator, so these numbers cannot drift from the behaviour.\nQuantities are those of the pre-order lines (a package or menu line counts its own `quantity`, not its items).\n`daysAhead` = booking date minus 2026-10-01 in whole days.\n\n### same_package with `exclusive: true` (rule 9c1d5e72)\n\nRule: set S = {`premium-bottomless-brunch`}, `exclusive` true, `wholeGroup` false. Party of 6, date 2026-11-14.\nComputation: (1) take the chosen lines whose slug is in S; none means the rule is inert. (2) More than one distinct S\nslug: violated. (3) `exclusive` and any chosen line outside S: violated. (4) `wholeGroup` true only: the summed quantity of the S lines must equal the party size.\n\n- Violating: premium x2 + standard x4. S lines = {premium}; the standard line is outside S and `exclusive` is true: `BKRUL-N-001`, \"If this package is chosen, everyone must have the same one and nothing else can be added.\"\n- Violating: premium x2 + mocktail-pairing x4. The mocktail is outside S: `BKRUL-N-001`.\n- Passing: premium x6, or premium x2 (`wholeGroup` is false, so the quantity need not equal the party size 6).\n- Passing: standard x6. No S line is chosen, the rule is inert.\n\n#### Scenarios - same_package exclusive\n\n```json\n[\n  {\"rule\": \"9c1d5e72\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 2], [\"standard-bottomless-brunch\", 4]], \"expect\": {\"violation\": \"BKRUL-N-001\", \"message\": \"If this package is chosen, everyone must have the same one and nothing else can be added.\"}},\n  {\"rule\": \"9c1d5e72\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 2], [\"mocktail-pairing\", 4]], \"expect\": {\"violation\": \"BKRUL-N-001\", \"message\": \"If this package is chosen, everyone must have the same one and nothing else can be added.\"}},\n  {\"rule\": \"9c1d5e72\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 6]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"9c1d5e72\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 2]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"9c1d5e72\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"standard-bottomless-brunch\", 6]], \"expect\": {\"violation\": null}}\n]\n```\n\n### same_package with `exclusive: false` and `wholeGroup: true` (rule 2b7f9d40)\n\nRule: S = {`premium-bottomless-brunch`, `standard-bottomless-brunch`}. Party of 6, date 2026-11-14.\nComputation: (1) no S line chosen: inert. (2) More than one distinct S slug: violated (lines outside S are ignored\nbecause `exclusive` is false). (3) `wholeGroup`: the summed quantity of the S lines must equal the party size.\n\n- Violating (mixing): premium x3 + standard x3. Two distinct S items: `BKRUL-N-001`, \"Everyone in the party must choose the same item from this set — selections cannot be mixed.\"\n- Violating (whole group): premium x4 only. One S item but 4 is not 6: `BKRUL-N-001`, \"Once chosen, this item's quantity must equal the party size (6).\"\n- Passing: premium x6 + mocktail-pairing x2. The S lines sum to 6 and the mocktail is outside S, so it is ignored.\n- Passing: mocktail-pairing x6 only. No S line, inert.\n\n#### Scenarios - same_package non-exclusive whole group\n\n```json\n[\n  {\"rule\": \"2b7f9d40\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 3], [\"standard-bottomless-brunch\", 3]], \"expect\": {\"violation\": \"BKRUL-N-001\", \"message\": \"Everyone in the party must choose the same item from this set — selections cannot be mixed.\"}},\n  {\"rule\": \"2b7f9d40\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 4]], \"expect\": {\"violation\": \"BKRUL-N-001\", \"message\": \"Once chosen, this item's quantity must equal the party size (6).\"}},\n  {\"rule\": \"2b7f9d40\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 6], [\"mocktail-pairing\", 2]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"2b7f9d40\", \"partySize\": 6, \"date\": \"2026-11-14\", \"packages\": [[\"mocktail-pairing\", 6]], \"expect\": {\"violation\": null}}\n]\n```\n\n### max_quantity_party_size (rule 6d40c8e1)\n\nRule: S is empty, so every chosen package and menu counts. Party of 4, date 2026-11-14.\nComputation: total = sum of the quantities of the counted lines; violated when total > party size (equal is allowed).\n\n- Violating: premium x3 + standard x2. Total 5 > 4: `BKRUL-N-002`, \"The total quantity of these items (5) cannot exceed the party size (4).\"\n- Violating: premium x2 + kids-menu (a menu) x3. Menus count too: 5 > 4.\n- Passing: premium x2 + standard x2. Total 4, and 4 > 4 is false.\n- Passing: no pre-order at all. Total 0.\n\n#### Scenarios - max_quantity_party_size\n\n```json\n[\n  {\"rule\": \"6d40c8e1\", \"partySize\": 4, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 3], [\"standard-bottomless-brunch\", 2]], \"expect\": {\"violation\": \"BKRUL-N-002\", \"message\": \"The total quantity of these items (5) cannot exceed the party size (4).\"}},\n  {\"rule\": \"6d40c8e1\", \"partySize\": 4, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 2]], \"menus\": [[\"kids-menu\", 3]], \"expect\": {\"violation\": \"BKRUL-N-002\", \"message\": \"The total quantity of these items (5) cannot exceed the party size (4).\"}},\n  {\"rule\": \"6d40c8e1\", \"partySize\": 4, \"date\": \"2026-11-14\", \"packages\": [[\"premium-bottomless-brunch\", 2], [\"standard-bottomless-brunch\", 2]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"6d40c8e1\", \"partySize\": 4, \"date\": \"2026-11-14\", \"expect\": {\"violation\": null}}\n]\n```\n\n### max_quantity (rule a13e5f87)\n\nRule: S = {menu `kids-menu`}, `max` 4. Party of 10, date 2026-11-14.\nComputation: total = sum of the quantities of the chosen lines in S (lines outside S are not counted); violated when total > `max` (exactly `max` is allowed).\n\n- Violating: kids-menu x5. 5 > 4: `BKRUL-N-003`, \"No more than 4 of this item is allowed per booking.\"\n- Passing: kids-menu x4. 4 > 4 is false.\n- Passing: kids-menu x3 + sunday-roast-menu x6. Only the S line counts (3).\n\n#### Scenarios - max_quantity\n\n```json\n[\n  {\"rule\": \"a13e5f87\", \"partySize\": 10, \"date\": \"2026-11-14\", \"menus\": [[\"kids-menu\", 5]], \"expect\": {\"violation\": \"BKRUL-N-003\", \"message\": \"No more than 4 of this item is allowed per booking.\"}},\n  {\"rule\": \"a13e5f87\", \"partySize\": 10, \"date\": \"2026-11-14\", \"menus\": [[\"kids-menu\", 4]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"a13e5f87\", \"partySize\": 10, \"date\": \"2026-11-14\", \"menus\": [[\"kids-menu\", 3], [\"sunday-roast-menu\", 6]], \"expect\": {\"violation\": null}}\n]\n```\n\n### preorder_required (rules c58b0a92 with items, f2a6d3c1 without)\n\nRule c58b0a92: S = {`premium-bottomless-brunch`, `standard-bottomless-brunch`}, `threshold` 8. Date 2026-11-14.\nComputation: if party size < threshold the rule is inert (party size >= threshold is \"inside\"); otherwise at least one chosen line must be in S.\n\n- Violating: party 8, no pre-order. 8 >= 8 and no S line: `BKRUL-N-004`, \"Groups of 8 or more must include a pre-order.\"\n- Violating: party 8 with mocktail-pairing x8 only. The mocktail is outside S.\n- Passing: party 7, no pre-order. 7 < 8, inert.\n- Passing: party 8 with standard x8.\n\nRule f2a6d3c1: S is empty, so ANY chosen package or menu qualifies, `threshold` 12.\n\n- Violating: party 12, no pre-order: `BKRUL-N-004`, \"Groups of 12 or more must include a pre-order.\"\n- Passing: party 12 with mocktail-pairing x1 (any line qualifies).\n- Passing: party 11, no pre-order (11 < 12).\n\n#### Scenarios - preorder_required\n\n```json\n[\n  {\"rule\": \"c58b0a92\", \"partySize\": 8, \"date\": \"2026-11-14\", \"expect\": {\"violation\": \"BKRUL-N-004\", \"message\": \"Groups of 8 or more must include a pre-order.\"}},\n  {\"rule\": \"c58b0a92\", \"partySize\": 8, \"date\": \"2026-11-14\", \"packages\": [[\"mocktail-pairing\", 8]], \"expect\": {\"violation\": \"BKRUL-N-004\", \"message\": \"Groups of 8 or more must include a pre-order.\"}},\n  {\"rule\": \"c58b0a92\", \"partySize\": 7, \"date\": \"2026-11-14\", \"expect\": {\"violation\": null}},\n  {\"rule\": \"c58b0a92\", \"partySize\": 8, \"date\": \"2026-11-14\", \"packages\": [[\"standard-bottomless-brunch\", 8]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"f2a6d3c1\", \"partySize\": 12, \"date\": \"2026-11-14\", \"expect\": {\"violation\": \"BKRUL-N-004\", \"message\": \"Groups of 12 or more must include a pre-order.\"}},\n  {\"rule\": \"f2a6d3c1\", \"partySize\": 12, \"date\": \"2026-11-14\", \"packages\": [[\"mocktail-pairing\", 1]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"f2a6d3c1\", \"partySize\": 11, \"date\": \"2026-11-14\", \"expect\": {\"violation\": null}}\n]\n```\n\n### route_to_enquiry (rule 0e9c7b35, create only)\n\nRule: `leadTimeDays` 7. Computation: `daysAhead` = booking date minus today; the booking becomes an enquiry when `daysAhead < 7`. It never rejects and is never evaluated on an amend.\n\n- Enquiry: date 2026-10-04. `daysAhead` 3 < 7.\n- Enquiry: date 2026-10-07. `daysAhead` 6 < 7.\n- Confirmed normally: date 2026-10-08. `daysAhead` 7 and 7 < 7 is false: exactly `leadTimeDays` ahead is outside the window.\n- Not affected: an amend of a booking dated 2026-10-04 (the effect is create only).\n\n#### Scenarios - route_to_enquiry\n\n```json\n[\n  {\"rule\": \"0e9c7b35\", \"partySize\": 4, \"date\": \"2026-10-04\", \"expect\": {\"enquiry\": true}},\n  {\"rule\": \"0e9c7b35\", \"partySize\": 4, \"date\": \"2026-10-07\", \"expect\": {\"enquiry\": true}},\n  {\"rule\": \"0e9c7b35\", \"partySize\": 4, \"date\": \"2026-10-08\", \"expect\": {\"enquiry\": false}},\n  {\"rule\": \"0e9c7b35\", \"partySize\": 4, \"date\": \"2026-10-04\", \"create\": false, \"expect\": {\"enquiry\": false}}\n]\n```\n\n### lock_preorder_edits (rule 7a3d1f96)\n\nRule: `leadTimeDays` 7. Computation: inside the window (`daysAhead < 7`): on create the booking must carry no pre-order line; on amend the final lines (slug, kind, quantity; order ignored) must equal the stored lines. Outside the window nothing is enforced.\n\n- Violating (create): date 2026-10-04 (`daysAhead` 3) with premium x6: `BKRUL-N-005`, \"Pre-order changes are not permitted less than 7 days before the booking date.\"\n- Passing (create): date 2026-10-08 (`daysAhead` 7, outside the window) with premium x6.\n- Passing (create): date 2026-10-04 with no pre-order line.\n- Violating (amend): date 2026-10-04, stored premium x6, final premium x4: the lines differ.\n- Passing (amend): date 2026-10-04, stored [premium x4, standard x2], final [standard x2, premium x4]: same lines in another order.\n- Passing (amend): date 2026-10-08, stored premium x6, final premium x4: outside the window.\n\n#### Scenarios - lock_preorder_edits\n\n```json\n[\n  {\"rule\": \"7a3d1f96\", \"partySize\": 6, \"date\": \"2026-10-04\", \"packages\": [[\"premium-bottomless-brunch\", 6]], \"expect\": {\"violation\": \"BKRUL-N-005\", \"message\": \"Pre-order changes are not permitted less than 7 days before the booking date.\"}},\n  {\"rule\": \"7a3d1f96\", \"partySize\": 6, \"date\": \"2026-10-08\", \"packages\": [[\"premium-bottomless-brunch\", 6]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"7a3d1f96\", \"partySize\": 6, \"date\": \"2026-10-04\", \"expect\": {\"violation\": null}},\n  {\"rule\": \"7a3d1f96\", \"partySize\": 6, \"date\": \"2026-10-04\", \"create\": false, \"original\": {\"packages\": [[\"premium-bottomless-brunch\", 6]]}, \"packages\": [[\"premium-bottomless-brunch\", 4]], \"expect\": {\"violation\": \"BKRUL-N-005\", \"message\": \"Pre-order changes are not permitted less than 7 days before the booking date.\"}},\n  {\"rule\": \"7a3d1f96\", \"partySize\": 6, \"date\": \"2026-10-04\", \"create\": false, \"original\": {\"packages\": [[\"premium-bottomless-brunch\", 4], [\"standard-bottomless-brunch\", 2]]}, \"packages\": [[\"standard-bottomless-brunch\", 2], [\"premium-bottomless-brunch\", 4]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"7a3d1f96\", \"partySize\": 6, \"date\": \"2026-10-08\", \"create\": false, \"original\": {\"packages\": [[\"premium-bottomless-brunch\", 6]]}, \"packages\": [[\"premium-bottomless-brunch\", 4]], \"expect\": {\"violation\": null}}\n]\n```\n\n### require_deposit (rules e07a4b19 per person, d4b82e60 per booking)\n\nComputation: required = `amount` x party size (`per: person`) or `amount` (`per: booking`). Collected = the request `depositAmount` on create (absent = 0), the deposit stored on the booking on amend. Violated when collected + 0.005 < required (half a penny of tolerance). The platform never collects money: you must collect it and send it as `depositAmount`.\n\nRule e07a4b19: `amount` 10.00, `per` person, its own `message`. Party of 6, date 2026-11-14.\n\n- Violating: no `depositAmount`. Required 10.00 x 6 = 60.00, collected 0: `BKRUL-N-006` with the rule's own message.\n- Passing: `depositAmount` 60.00.\n- Violating: `depositAmount` 59.99. 59.99 + 0.005 = 59.995 < 60.00.\n- Violating: party 8 with `depositAmount` 60.00. Required 80.00.\n\nRule d4b82e60: `amount` 100.00, `per` booking, limited to the product `sunday-lunch`, no `message`. Party of 6.\n\n- Violating: product `sunday-lunch`, `depositAmount` 99.99: `BKRUL-N-006`, \"A deposit of £100.00 is required for this booking (£100.00 per booking).\"\n- Passing: `depositAmount` 100.00 (party size is irrelevant for `per: booking`).\n- Passing: product `bottomless-brunch` with no deposit (the rule is limited to `sunday-lunch`, so it does not apply).\n- Passing (amend): the stored deposit is 100.00.\n\n#### Scenarios - require_deposit\n\n```json\n[\n  {\"rule\": \"e07a4b19\", \"partySize\": 6, \"date\": \"2026-11-14\", \"expect\": {\"violation\": \"BKRUL-N-006\", \"message\": \"Please collect the 10 pound per person deposit and send it as depositAmount.\"}},\n  {\"rule\": \"e07a4b19\", \"partySize\": 6, \"date\": \"2026-11-14\", \"deposit\": 60.00, \"expect\": {\"violation\": null}},\n  {\"rule\": \"e07a4b19\", \"partySize\": 6, \"date\": \"2026-11-14\", \"deposit\": 59.99, \"expect\": {\"violation\": \"BKRUL-N-006\", \"message\": \"Please collect the 10 pound per person deposit and send it as depositAmount.\"}},\n  {\"rule\": \"e07a4b19\", \"partySize\": 8, \"date\": \"2026-11-14\", \"deposit\": 60.00, \"expect\": {\"violation\": \"BKRUL-N-006\", \"message\": \"Please collect the 10 pound per person deposit and send it as depositAmount.\"}},\n  {\"rule\": \"d4b82e60\", \"product\": \"sunday-lunch\", \"partySize\": 6, \"date\": \"2026-11-14\", \"deposit\": 99.99, \"expect\": {\"violation\": \"BKRUL-N-006\", \"message\": \"A deposit of £100.00 is required for this booking (£100.00 per booking).\"}},\n  {\"rule\": \"d4b82e60\", \"product\": \"sunday-lunch\", \"partySize\": 6, \"date\": \"2026-11-14\", \"deposit\": 100.00, \"expect\": {\"violation\": null}},\n  {\"rule\": \"d4b82e60\", \"product\": \"bottomless-brunch\", \"partySize\": 6, \"date\": \"2026-11-14\", \"expect\": {\"violation\": null}},\n  {\"rule\": \"d4b82e60\", \"product\": \"sunday-lunch\", \"partySize\": 6, \"date\": \"2026-11-14\", \"create\": false, \"deposit\": 100.00, \"expect\": {\"violation\": null}}\n]\n```\n\n### booking_window\n\nComputation: hours = (reservation start - now), real elapsed time, with the reservation start = booking date + start time in\nEurope/London. Too close when hours < `minHoursBefore`; too far ahead (book only) when hours > `maxHoursBefore`. On amend the\n`amend` window is measured from the start stored on the booking, and the `book` window from the new start (only when the date or\ntime changes). The operator's own advance-booking limits on the booking type still apply on top: a rule can only narrow them.\n\nA rule with `action` cancel and `minHoursBefore` 120, reservation 2026-10-25 19:00 (the clocks go back at 02:00 that morning):\n\n- Violating: cancel at 2026-10-20 21:00 Europe/London (119 real hours before): `BKRUL-N-008`, \"Cancellations close 5 days (120 hours) before the reservation; they closed at 2026-10-20 20:00 Europe/London.\"\n- Passing: cancel at 2026-10-20 20:00 Europe/London, exactly 120 real hours before (counting wall-clock hours would wrongly give 119, because of the clock change).\n- Passing: the booking is still a pending request.\n\nA rule with `action` amend and `minHoursBefore` 240, stored reservation 2026-11-05 19:00, now 2026-10-30 12:00:\n\n- Violating: moving it to 2026-12-20 (the stored start is 151 hours away): `BKRUL-N-007`.\n\nA rule with `action` book, `minHoursBefore` 48 and `maxHoursBefore` 2400:\n\n- Violating: a booking for 30 hours ahead: `AVAIL-N-007`. A booking for 2,500 hours ahead: `AVAIL-N-008`.\n- Passing (amend): a notes change on a confirmed booking now 30 hours away (date and time unchanged).\n\n### Expression rules: the same method, at `booking.amend` (rule 1c6e9b48)\n\nAn expression rule is evaluated by building the fact document of its hook, evaluating `when`, then applying `then` (see the\nlanguage reference). Rule 1c6e9b48 runs when you PUT or PATCH a booking: it rejects an amend that leaves no premium brunch line when the\nstored booking had one. At `booking.amend`, `booking.originalItems` holds the lines stored before the amend and `booking.items` the lines\nthe booking will have after it.\n\n- Violating: stored [premium x6, standard x4], final [standard x10]. `some(booking.originalItems, slug == premium)` is true and `none(booking.items, slug == premium)` is true, so `when` is true: `BKRUL-N-100`, \"The Premium Bottomless Brunch cannot be removed from an existing booking.\"\n- Passing: stored [premium x6, standard x4], final [premium x10]. `none(booking.items, ...)` is false.\n- Passing: stored [standard x10], final [standard x6]. The stored booking had no premium line, `some` is false.\n\n#### Checks - expression at booking.amend\n\n```json\n[\n  {\"rules\": [\"1c6e9b48\"], \"input\": {\"create\": false, \"partySize\": 10, \"original\": {\"packages\": [[\"premium-bottomless-brunch\", 6], [\"standard-bottomless-brunch\", 4]]}, \"packages\": [[\"standard-bottomless-brunch\", 10]]},\n   \"expect\": {\"rejects\": true, \"applied\": [\"1c6e9b48\"]}},\n  {\"rules\": [\"1c6e9b48\"], \"input\": {\"create\": false, \"partySize\": 10, \"original\": {\"packages\": [[\"premium-bottomless-brunch\", 6], [\"standard-bottomless-brunch\", 4]]}, \"packages\": [[\"premium-bottomless-brunch\", 10]]},\n   \"expect\": {\"rejects\": false, \"applied\": []}},\n  {\"rules\": [\"1c6e9b48\"], \"input\": {\"create\": false, \"partySize\": 6, \"original\": {\"packages\": [[\"standard-bottomless-brunch\", 10]]}, \"packages\": [[\"standard-bottomless-brunch\", 6]]},\n   \"expect\": {\"rejects\": false, \"applied\": []}}\n]\n```\n\n## Expression language reference\n\n`when` is one expression tree; the rule applies when it evaluates to the boolean `true`.\n\n**Node forms.** A literal (number, string, `true`, `false`, `null`); a list of nodes (`[\"a\", \"b\"]`);\n`{\"var\": \"<path>\"}` reads a fact by dotted path (object members by name, list elements by zero-based index);\n`{\"var\": [\"<path>\", <default>]}` supplies a default for a missing or null fact; `{\"var\": \"\"}` is the current element\ninside a list predicate; or `{\"<operator>\": [<args>]}` (a single non-array argument is accepted: `{\"!\": x}`). A\nlist-valued single argument of `count`, `sum` or `distinct` is passed unwrapped: `{\"count\": {\"var\": \"booking.items\"}}`.\nAn object literal cannot be written. Numbers are exact decimals.\n\n| Group | Operators | Semantics |\n|---|---|---|\n| Comparison | `==` `!=` | Both operands must be the same kind (number, string, boolean, list, object, null); null equals only null. A different kind is a type clash and the whole rule is skipped (there is no coercion): `null == \"Terrace\"` skips the rule, it does not give false. |\n| Comparison | `>` `>=` `<` `<=` | All operands numbers, or all strings (ordinal comparison, so ISO dates compare correctly). Three operands mean \"between\": `{\"<=\": [1, {\"var\": \"booking.partySize\"}, 8]}`. |\n| Logic | `and` `or` `!` `if` | Operands must be booleans (no truthiness). `and` / `or` short-circuit left to right, so an operand after the deciding one is never evaluated (and cannot cause a skip). `{\"if\": [cond1, v1, cond2, v2, else]}` evaluates only the branch taken. |\n| Arithmetic | `+` `-` `*` `/` `%` `min` `max` | Numbers only. `-` with one operand negates. Division or modulo by zero skips the rule. |\n| Membership | `in` | `{\"in\": [needle, haystack]}`. Haystack list: true if an element equals the needle (a different kind is simply not equal). Haystack string: ordinal substring test, the needle must be a string. Any other haystack (including null) skips the rule. |\n| List predicates | `filter` `map` `some` `all` `none` | `{\"some\": [<list>, <predicate>]}`. Inside the predicate `{\"var\": \"slug\"}` is a field of the **current element** and `{\"var\": \"\"}` is the element itself; the rest of the facts are not reachable there. The predicate of filter, some, all and none must be boolean. `some` of an empty list is false, `all` and `none` of an empty list are true. |\n| Aggregates | `count` `sum` `distinct` `pluck` | `count` and `sum` of an empty list are 0; `sum` needs numbers; `distinct` removes duplicates; `pluck` takes the named field of every object element: `{\"pluck\": [<list>, \"quantity\"]}` (the field name is a string literal; absent field gives null). |\n\n**Limits** (an expression that exceeds them is skipped at booking time): depth 12, 96 nodes, at most 8 actions in\n`then`, lists of at most 200 elements, 2000 elementary operations per evaluation.\n\n**Actions** (`then`, 1 to 8, each an object with exactly one key):\n`{\"set\": {\"path\": \"booking.type\", \"value\": \"request\"}}` writes the value (a literal, or an expression evaluated like\n`when`) to a path that is writable at the hook (`hooks[].writablePaths`); `{\"reject\": {\"message\": \"...\"}}` refuses the\nbooking (the message is optional; ignored at the `payment` hook); `{\"flag\": {\"name\": \"vip\"}}` attaches a named\nannotation with no behaviour of its own.\n\n**What `booking.type = request` plus `booking.enquiryReason = operatorRule` means for you.** The booking is not\nconfirmed instantly: it is created as an enquiry (type `request`) that the operator has to accept, whatever `type`\nyou sent. `enquiryReason = operatorRule` marks that an operator rule, not availability or your own enquiry policy,\nmade that decision (the Collins integration then skips its per-partner enquiry-policy checks). No error is returned.\nHow other table management systems treat an enquiry: not verified beyond Collins. The `route_to_enquiry` effect\ndoes the same without an expression.\n\n## Expression cheat-sheet\n\nThe ten patterns operators use most, each as a complete rule fragment (`when` + `then`) and its plain-English reading.\nFacts are the dotted paths of `hooks[].facts[]`. The checks at the end are executed by the platform's test-suite against the\nreal expression engine, so every pattern here is proven. Use them as templates to READ rules; you never send a rule.\n\n### Pattern 1 - Compare a fact with a number\n\n```json\n{\"when\": {\">\": [{\"var\": \"booking.partySize\"}, 8]},\n \"then\": [{\"reject\": {\"message\": \"Parties of more than 8 cannot book online.\"}}]}\n```\n\nReading: reject when the party size is greater than 8 (a party of exactly 8 passes).\n\n### Pattern 2 - Is a fact one of a set of values\n\n```json\n{\"when\": {\"in\": [{\"var\": \"product.slug\"}, [\"bottomless-brunch\", \"sunday-lunch\"]]},\n \"then\": [{\"reject\": {\"message\": \"This product cannot be booked through this channel.\"}}]}\n```\n\nReading: reject when the booked product is `bottomless-brunch` or `sunday-lunch`. A null `product.slug` is simply \"not in the list\" (false), it does not skip the rule.\n\n### Pattern 3 - Count the pre-order lines\n\n```json\n{\"when\": {\"==\": [{\"count\": {\"var\": \"booking.items\"}}, 0]},\n \"then\": [{\"reject\": {\"message\": \"A pre-order is required.\"}}]}\n```\n\nReading: reject when there is no pre-order line. `count` of `booking.items` equals the fact `booking.itemCount`; it counts lines, not quantities.\n\n### Pattern 4 - Sum the quantities of selected items\n\n```json\n{\"when\": {\">\": [\n   {\"sum\": {\"pluck\": [{\"filter\": [{\"var\": \"booking.items\"}, {\"==\": [{\"var\": \"kind\"}, \"package\"]}]}, \"quantity\"]}},\n   {\"var\": \"booking.partySize\"}]},\n \"then\": [{\"reject\": {\"message\": \"More packages than guests.\"}}]}\n```\n\nReading: keep the package lines (`kind` is the field of the current line), take their `quantity`, add them up and reject when the total is greater than the party size. `booking.partySize` is outside the `filter`, where it is reachable.\n\n### Pattern 5 - Check the partner category\n\n```json\n{\"when\": {\"in\": [1, {\"var\": \"partner.categoryIds\"}]},\n \"then\": [{\"reject\": {\"message\": \"Not available to this partner category.\"}}]}\n```\n\nReading: reject when partner category 1 is among the categories of the calling partner for the rule's operator (`caller.categoryIdsByOperator[rule.operatorId]`).\n\n### Pattern 6 - Days-ahead window\n\n```json\n{\"when\": {\"and\": [{\">=\": [{\"var\": \"booking.daysAhead\"}, 0]}, {\"<\": [{\"var\": \"booking.daysAhead\"}, 7]}]},\n \"then\": [{\"reject\": {\"message\": \"Bookings less than 7 days ahead are not accepted.\"}}]}\n```\n\nReading: reject when the booking date is today or within the next 6 days (0 <= daysAhead < 7); a date 7 days ahead passes.\n\n### Pattern 7 - Null-safe default\n\n```json\n{\"when\": {\">\": [{\"var\": [\"booking.commission\", 0]}, 15]},\n \"then\": [{\"reject\": {\"message\": \"Commission above 15 is not accepted.\"}}]}\n```\n\nReading: `{\"var\": [path, default]}` replaces a missing or null fact by the default (here 0). Without the default a null `booking.commission` makes `>` a type clash and the rule is skipped.\n\n### Pattern 8 - Boolean combination\n\n```json\n{\"when\": {\"and\": [\n   {\">\": [{\"var\": \"booking.partySize\"}, 10]},\n   {\"!\": {\"var\": \"booking.hasNotes\"}},\n   {\"!\": {\"var\": \"booking.hasCustomerCompany\"}}]},\n \"then\": [{\"reject\": {\"message\": \"Large parties must give notes or a company.\"}}]}\n```\n\nReading: reject a party of more than 10 that carries neither notes nor a customer company. `and` / `or` / `!` need real booleans (no truthiness) and `and` / `or` stop at the first deciding operand.\n\n### Pattern 9 - Between (inclusive)\n\n```json\n{\"when\": {\"<=\": [5, {\"var\": \"booking.partySize\"}, 8]},\n \"then\": [{\"reject\": {\"message\": \"Parties of 5 to 8 must call the venue.\"}}]}\n```\n\nReading: three operands chain: 5 <= partySize and partySize <= 8, both ends included.\n\n### Pattern 10 - Actions: set and flag (an enquiry instead of a refusal)\n\n```json\n{\"when\": {\">=\": [{\"var\": \"booking.partySize\"}, 12]},\n \"then\": [\n   {\"set\": {\"path\": \"booking.type\", \"value\": \"request\"}},\n   {\"set\": {\"path\": \"booking.enquiryReason\", \"value\": \"operatorRule\"}},\n   {\"flag\": {\"name\": \"large-party\"}}]}\n```\n\nReading: for 12 or more guests write `booking.type = request` and `booking.enquiryReason = operatorRule` (the booking becomes an enquiry, no error) and attach the annotation `large-party`. A `reject` action in a rule refuses the booking instead (patterns 1 to 9); use `set` and `flag` alone for the enquiry outcome. `set` is only allowed on the writable paths of the hook.\n\n#### Checks - cheat-sheet\n\n```json\n[\n  {\"pattern\": 1, \"input\": {\"partySize\": 9}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 1, \"input\": {\"partySize\": 8}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 2, \"input\": {\"product\": \"sunday-lunch\"}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 2, \"input\": {\"product\": \"afternoon-tea\"}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 2, \"input\": {\"product\": null}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 3, \"input\": {}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 3, \"input\": {\"packages\": [[\"premium-bottomless-brunch\", 1]]}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 4, \"input\": {\"partySize\": 4, \"packages\": [[\"premium-bottomless-brunch\", 3], [\"standard-bottomless-brunch\", 2]]}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 4, \"input\": {\"partySize\": 4, \"packages\": [[\"premium-bottomless-brunch\", 2], [\"standard-bottomless-brunch\", 2]]}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 5, \"input\": {\"categories\": [1]}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 5, \"input\": {\"categories\": [5]}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 5, \"input\": {\"categories\": []}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 6, \"input\": {\"date\": \"2026-10-04\"}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 6, \"input\": {\"date\": \"2026-10-01\"}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 6, \"input\": {\"date\": \"2026-10-08\"}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 6, \"input\": {\"date\": \"2026-09-30\"}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 7, \"input\": {\"commission\": 20}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 7, \"input\": {\"commission\": 10}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 7, \"input\": {\"commission\": null}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 8, \"input\": {\"partySize\": 12}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 8, \"input\": {\"partySize\": 12, \"hasNotes\": true}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 8, \"input\": {\"partySize\": 8}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 9, \"input\": {\"partySize\": 5}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 9, \"input\": {\"partySize\": 8}, \"expect\": {\"applies\": true}},\n  {\"pattern\": 9, \"input\": {\"partySize\": 9}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 9, \"input\": {\"partySize\": 4}, \"expect\": {\"applies\": false}},\n  {\"pattern\": 10, \"input\": {\"partySize\": 12}, \"expect\": {\"applies\": true, \"writes\": {\"booking.type\": \"request\", \"booking.enquiryReason\": \"operatorRule\"}, \"flags\": [\"large-party\"]}},\n  {\"pattern\": 10, \"input\": {\"partySize\": 4}, \"expect\": {\"applies\": false}}\n]\n```\n\n## Errors when a booking is rejected\n\nThe booking endpoints answer HTTP 400 with a problem document (schema `BookingRuleViolationProblem`).\n`violations[]` lists **every** rejecting rule, `code` / `title` / `detail` describe the primary one, and\n`analyticsRuleId` / `analyticsRuleName` identify the primary rule. `violations[].ruleId` is the same UUID as\n`rules[].id` returned by this API. `violations[].effect` is the snake_case effect (`expression` for generic rules).\nPer-violation message text is not repeated inside `violations[]`: look the rule up (its `message`, or the default\nmessage in the effects table). Example (a party of 6 chose a Premium brunch for 2 and a Standard brunch for 4,\nwhile a `same_package` rule with `exclusive: true` covers the Premium brunch):\n\n```json\n{\n  \"type\": \"https://tools.ietf.org/html/rfc9110#section-15.5.1\",\n  \"title\": \"Same Package Violation\",\n  \"status\": 400,\n  \"detail\": \"If this package is chosen, everyone must have the same one and nothing else can be added.\",\n  \"code\": \"BKRUL-N-001\",\n  \"isRetryable\": false,\n  \"violations\": [\n    {\n      \"ruleId\": \"9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36\",\n      \"ruleName\": \"Premium brunch - whole table, no mixing\",\n      \"effect\": \"same_package\",\n      \"code\": \"BKRUL-N-001\"\n    }\n  ],\n  \"analyticsRuleId\": \"9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36\",\n  \"analyticsRuleName\": \"Premium brunch - whole table, no mixing\",\n  \"traceId\": \"0HNJ2BG2TU3BU:00000001\"\n}\n```\n\nRetrying the same request will fail again (`isRetryable` is false): change the booking.\n\n`GET /booking-rules` itself answers its own refusals with the shared `ClientError` problem document, the same shape as every other\nendpoint: each refusal carries a catalog `code` (`BKRUL-N-112`, `BKRUL-N-113`, `VALID-N-001`, `OPRTR-N-002`, `PMAP-N-001`,\n`PMAP-N-004`, `VENUE-N-001`, `IDENT-N-002`, `IDENT-N-003`, `RATE-R-001`), `isRetryable` and `traceId`; the examples of each\nresponse show the exact body. The 401 is the standard bearer challenge and may have no JSON body.\n\n## Worked examples\n\n\"Today\" in all examples is 2026-10-01 (Europe/London). All ids are fictitious.\n\n### Example 1 - Terrace, party of more than 8, no pre-order (expression, rejected with BKRUL-N-100)\n\nRule (`hook: booking.create`):\n\n```json\n{\n  \"when\": {\"and\": [\n    {\"==\": [{\"var\": \"space.name\"}, \"Terrace\"]},\n    {\">\":  [{\"var\": \"booking.partySize\"}, 8]},\n    {\"==\": [{\"var\": \"booking.itemCount\"}, 0]}\n  ]},\n  \"then\": [{\"reject\": {\"message\": \"Parties of more than 8 on the Terrace must include a pre-order.\"}}]\n}\n```\n\nRequest: `POST /venues/17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9/booking` with\n`{\"partySize\": 10, \"date\": \"2026-11-14\", \"time\": \"19:00\", \"type\": \"book\", \"spaceId\": 4412, ...}` and no `preorders`.\n`spaceId` 4412 is the space named \"Terrace\" at that venue.\n\nFacts used: `space.name` = \"Terrace\" (resolved from `spaceId`), `booking.partySize` = 10, `booking.itemCount` = 0\n(no pre-order lines).\nEvaluation: `\"Terrace\" == \"Terrace\"` true, `10 > 8` true, `0 == 0` true, so `when` is true.\n**Outcome: rejected**, HTTP 400, `code: BKRUL-N-100`, `detail: \"Parties of more than 8 on the Terrace must include a pre-order.\"`,\n`violations[0].effect = \"expression\"`.\nVariants: party of 8 gives `8 > 8` false (not rejected); party of 10 with one package line gives `itemCount` 1 (not\nrejected); **no `spaceId`**: `space.name` is null, `null == \"Terrace\"` is a type clash and the rule is *skipped* (the\nplatform does not reject on the terrace rule), but do not rely on that: the operator's system then picks the area and\nother checks may apply.\n\n### Example 2 - Partner categories under 7 days become enquiries (expression, no error)\n\nRule (`hook: booking.create`, `priority: 100`), aimed at partner categories 1 and 2 through the rule's `when`:\n\n```json\n{\n  \"when\": {\"and\": [\n    {\"some\": [{\"var\": \"partner.categoryIds\"}, {\"in\": [{\"var\": \"\"}, [1, 2]]}]},\n    {\"<\": [{\"var\": \"booking.daysAhead\"}, 7]}\n  ]},\n  \"then\": [\n    {\"set\": {\"path\": \"booking.type\", \"value\": \"request\"}},\n    {\"set\": {\"path\": \"booking.enquiryReason\", \"value\": \"operatorRule\"}}\n  ]\n}\n```\n\nYou are partner 482 in category 1 for operator 17 (`caller.categoryIdsByOperator[\"17\"] = [1]`). Request: `date: \"2026-10-04\"`, `type: \"book\"`.\nFacts: `partner.categoryIds` = [1]; `booking.daysAhead` = 2026-10-04 minus 2026-10-01 = 3.\nEvaluation: `some` over [1]: `1 in [1, 2]` true; `3 < 7` true; `when` true.\n**Outcome: not rejected; the booking is created as an enquiry** (`type: request`), the writes being\n`booking.type = \"request\"` and `booking.enquiryReason = \"operatorRule\"`.\nWith `date: \"2026-10-08\"`: `daysAhead` = 7, `7 < 7` is false, so the booking is confirmed normally (exactly the lead\ntime is outside the window). With `partner.categoryIds` = [5]: `some` is false, no effect.\n\n### Example 3 - Brunch packages may not exceed the party size (expression, rejected with BKRUL-N-100)\n\nRule (`hook: booking.create`):\n\n```json\n{\n  \"when\": {\">\": [\n    {\"sum\": {\"pluck\": [\n      {\"filter\": [{\"var\": \"booking.items\"},\n                  {\"in\": [{\"var\": \"slug\"}, [\"premium-bottomless-brunch\", \"standard-bottomless-brunch\"]]}]},\n      \"quantity\"]}},\n    {\"var\": \"booking.partySize\"}\n  ]},\n  \"then\": [{\"reject\": {\"message\": \"You cannot select more brunch packages than guests.\"}}]\n}\n```\n\nRequest: `partySize: 4`, `preorders.packages`: the venue package whose slug is `premium-bottomless-brunch` with\n`quantity: 3` and the package whose slug is `standard-bottomless-brunch` with `quantity: 2`.\nFacts: `booking.partySize` = 4; `booking.items` = `[{\"kind\": \"package\", \"slug\": \"premium-bottomless-brunch\", \"quantity\": 3}, {\"kind\": \"package\", \"slug\": \"standard-bottomless-brunch\", \"quantity\": 2}]`.\nEvaluation: `filter` keeps both lines, `pluck` gives [3, 2], `sum` = 5, `5 > 4` true.\n**Outcome: rejected**, `BKRUL-N-100`, \"You cannot select more brunch packages than guests.\".\nWith quantities 2 and 2: `4 > 4` false, accepted by this rule. Inside `filter`, `{\"var\": \"slug\"}` is the slug of the\ncurrent line; `booking.partySize` is not reachable inside the predicate (it is used outside it).\n\n### Example 4 - `same_package`, exclusive (fixed effect, rejected with BKRUL-N-001)\n\nRule: `effect: same_package`, `params: {\"exclusive\": true, \"wholeGroup\": false}`, `appliesTo.items.packages` =\n[`premium-bottomless-brunch`], all venues, all products, all partners. Party size 6.\n\nRequest A: premium x2 and standard x4. S = {premium}; the chosen lines in S: premium (one distinct item), but the\nstandard line is outside S and `exclusive` is true. **Rejected** with `BKRUL-N-001`; this is the response shown above.\nRequest B: premium x6 only, or premium x2 only: **accepted** (`wholeGroup` is false, so the quantity need not equal\nthe party size). Request C: standard x6 only: **accepted** (no S item chosen, the rule is inert). Request D: premium x2\nplus a mocktail package: rejected (the mocktail is outside S); with `exclusive: false` requests A and D would both be\naccepted (the lines outside S are ignored) and only choosing two distinct S items (possible when S lists two items)\nwould be rejected.\n\n### Example 5 - The final balance is the sum of the minimum-spend bands the booking crosses (expression at `payment`, no error)\n\nRule (`hook: payment`, `priority: 100`):\n\n```json\n{\n  \"when\": {\">\": [{\"var\": \"payment.bandCount\"}, 0]},\n  \"then\": [{\"set\": {\"path\": \"payment.finalBalance\", \"value\": {\"sum\": {\"pluck\": [{\"var\": \"payment.bands\"}, \"spendValue\"]}}}}]\n}\n```\n\nThe operator configured two minimum-spend bands on the space: \"Early\" from 18:00 to 19:59 at 50000 pence (GBP 500.00) and\n\"Late\" from 20:00 to 23:00 at 80000 pence (GBP 800.00). The booking already exists: 2026-10-05, start 19:00, duration\n180 minutes, so it occupies [19:00, 22:00). You (or the venue) ask for its payment options. This is not something you send:\nthe platform builds the facts from the stored booking and the space's policy.\n\nFacts (the `payment` root; the other roots are those of the stored booking):\n\n```json\n{\n  \"payment\": {\n    \"bands\": [\n      {\"label\": \"Early\", \"from\": \"19:00\", \"to\": \"20:00\", \"spendType\": \"minimum_spend\", \"spendValue\": 50000, \"hireFeeValue\": null, \"overlapMinutes\": 60},\n      {\"label\": \"Late\", \"from\": \"20:00\", \"to\": \"22:00\", \"spendType\": \"minimum_spend\", \"spendValue\": 80000, \"hireFeeValue\": null, \"overlapMinutes\": 120}\n    ],\n    \"bandCount\": 2,\n    \"minimumSpend\": 50000,\n    \"currency\": \"GBP\"\n  }\n}\n```\n\nEvaluation: `payment.bandCount` 2 > 0 is true; `pluck` of `spendValue` gives [50000, 80000]; `sum` = 130000, so the rule writes\n`payment.finalBalance = 130000`.\n**Outcome: not rejected, nothing to fix; the amount payable is GBP 1,300.00** (130000 pence). The payment options become\n\"full minimum spend\" 130000 and \"25% deposit\" 32500 (130000 x 0.25). Without the rule they would have been 50000 and 12500\n(the minimum spend at the start time only). Read the figures from `getPaymentOptions`, never compute them yourself.\nVariants: a booking at 19:00 for 60 minutes occupies [19:00, 20:00), which only touches \"Early\" (a band that starts exactly\nwhen the booking ends is not touched): the sum is 50000, the same as today. If the space had only the \"Late\" band, a\nbooking at 19:00 for 180 minutes starts in a gap: `payment.minimumSpend` is null, `payment.bands` holds \"Late\" alone (from\n20:00 to 22:00) and the rule writes 80000. A single band that contains the start time gives the same figure as today. A rule\nwritten with `payment.bandCount > 1` does not match a single band.\n\n## End-to-end walkthrough for an AI agent\n\nGoal: book The Rooftop Kitchen (operator 17) on the Terrace for 10 guests on 2026-10-04 (today is 2026-10-01), with a\nbrunch pre-order. You are partner 482, category 1.\n\n**Step 1 - fetch the rules** for the venue and the operation:\n`GET /booking-rules?venueId=17%7CX9%7C9d3f0b6e7a1c4e58b2c4f1a60d7e8b93&hook=booking.create`. Suppose the operator has just five rules active; the named 200 example `walkthrough`\nis then your answer (5 rules, hook `booking.create`):\n\n| Rule id (prefix) | Effect | What it says |\n|---|---|---|\n| `9c1d5e72` | `same_package`, `exclusive: true` | If `premium-bottomless-brunch` is chosen, nothing else may be chosen. Product `bottomless-brunch`. |\n| `e07a4b19` | `require_deposit`, 10.00 per person | Carry a deposit of 10.00 x party size. Aimed at you (`partnerScope: you`). |\n| `3f2b8c1e` | `expression`, priority 100 | Reject: space `Terrace`, party over 8 and no pre-order line. |\n| `5a9e2c74` | `expression`, priority 100 | Partner category 1 or 2 and `daysAhead` below 7: the booking becomes an enquiry. |\n| `b8d3f6a0` | `expression`, priority 100 | Reject: brunch package quantities add up to more than the party size. Aimed at your categories. |\n\n**Step 2 - the booking you intend to send** (`POST /venues/17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9/booking`; the\npackage ids are the venue-specific ids from `GET /venues`, whose aggregation slugs are `premium-bottomless-brunch` and `standard-bottomless-brunch`):\n\n```json\n{\n  \"partySize\": 10, \"date\": \"2026-10-04\", \"time\": \"19:00\", \"type\": \"book\", \"spaceId\": 4412,\n  \"preorders\": {\"packages\": [{\"id\": \"5de64c68ce9583458c447544\", \"quantity\": 6}, {\"id\": \"5de64c68ce9583458c447545\", \"quantity\": 4}]}\n}\n```\n\nNo `depositAmount` is sent. `spaceId` 4412 is the space named \"Terrace\".\n\n**Step 3 - derive the facts** (`howToObtain` of each fact): `booking.partySize` 10; `booking.date` \"2026-10-04\";\n`booking.daysAhead` 3 (2026-10-04 minus 2026-10-01); `booking.type` \"book\"; `booking.enquiryReason` \"none\";\n`booking.items` [premium x6, standard x4]; `booking.itemCount` 2; `booking.depositAmount` null; `partner.id` 482;\n`partner.categoryIds` [1]; `product.slug` \"bottomless-brunch\"; `space.name` \"Terrace\".\n\n**Step 4 - evaluate the fixed rules first** (expression rules only run if all fixed rules pass):\n\n- `9c1d5e72` same_package: S = {premium}. The premium line is chosen; the standard line is outside S and `exclusive` is true: VIOLATED, `BKRUL-N-001`.\n- `e07a4b19` require_deposit: required 10.00 x 10 = 100.00, collected 0: VIOLATED, `BKRUL-N-006`.\n- Primary error by precedence (`same_package` before `require_deposit`): `BKRUL-N-001`. Both are listed in `violations[]`. The expression rules are not evaluated at all.\n\nThe platform would answer HTTP 400:\n\n```json\n{\n  \"type\": \"https://tools.ietf.org/html/rfc9110#section-15.5.1\",\n  \"title\": \"Same Package Violation\",\n  \"status\": 400,\n  \"detail\": \"If this package is chosen, everyone must have the same one and nothing else can be added.\",\n  \"code\": \"BKRUL-N-001\",\n  \"isRetryable\": false,\n  \"violations\": [\n    {\"ruleId\": \"9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36\", \"ruleName\": \"Premium brunch - whole table, no mixing\", \"effect\": \"same_package\", \"code\": \"BKRUL-N-001\"},\n    {\"ruleId\": \"e07a4b19-6c2d-48f3-b5a1-3d9c70f2e864\", \"ruleName\": \"Deposit of 10 pounds per person\", \"effect\": \"require_deposit\", \"code\": \"BKRUL-N-006\"}\n  ],\n  \"analyticsRuleId\": \"9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36\",\n  \"analyticsRuleName\": \"Premium brunch - whole table, no mixing\",\n  \"traceId\": \"0HNJ2BG2TU3BU:00000001\"\n}\n```\n\n**Step 5 - change the booking**: choose premium x10 only (no standard line) and collect 100.00, then send `\"depositAmount\": 100.00`.\n\n**Step 6 - evaluate again.** Fixed rules: `9c1d5e72`: the only chosen line is in S and `wholeGroup` is false: passes. `e07a4b19`: 100.00 + 0.005 >= 100.00: passes.\nExpression rules run now, in order (all priority 100, so by rule id):\n\n- `3f2b8c1e`: `space.name` \"Terrace\" equals \"Terrace\" (true); 10 > 8 (true); `booking.itemCount` is 1, not 0 (false): `when` is false, nothing happens.\n- `5a9e2c74`: partner category 1 is in [1, 2] (true) and `daysAhead` 3 < 7 (true): `when` is true, so `booking.type` = \"request\" and `booking.enquiryReason` = \"operatorRule\".\n- `b8d3f6a0`: filter keeps premium x10, `pluck` gives [10], `sum` 10; 10 > 10 is false: nothing happens.\n\n**Decision:** the booking is accepted and created as an **enquiry** (type `request`, no error): the operator has to accept it.\nTo get an instant confirmation instead, book on 2026-10-08 or later (`daysAhead` 7, and 7 < 7 is false, so `5a9e2c74` does not apply).\n\n#### Scenarios - end to end (fixed rules)\n\n```json\n[\n  {\"rule\": \"9c1d5e72\", \"partySize\": 10, \"date\": \"2026-10-04\", \"packages\": [[\"premium-bottomless-brunch\", 6], [\"standard-bottomless-brunch\", 4]], \"expect\": {\"violation\": \"BKRUL-N-001\", \"message\": \"If this package is chosen, everyone must have the same one and nothing else can be added.\"}},\n  {\"rule\": \"e07a4b19\", \"partySize\": 10, \"date\": \"2026-10-04\", \"packages\": [[\"premium-bottomless-brunch\", 6], [\"standard-bottomless-brunch\", 4]], \"expect\": {\"violation\": \"BKRUL-N-006\", \"message\": \"Please collect the 10 pound per person deposit and send it as depositAmount.\"}},\n  {\"rule\": \"9c1d5e72\", \"partySize\": 10, \"date\": \"2026-10-04\", \"packages\": [[\"premium-bottomless-brunch\", 10]], \"expect\": {\"violation\": null}},\n  {\"rule\": \"e07a4b19\", \"partySize\": 10, \"date\": \"2026-10-04\", \"packages\": [[\"premium-bottomless-brunch\", 10]], \"deposit\": 100.00, \"expect\": {\"violation\": null}}\n]\n```\n\n#### Checks - end to end (expression rules)\n\n```json\n[\n  {\"rules\": [\"3f2b8c1e\", \"5a9e2c74\", \"b8d3f6a0\"], \"input\": {\"partySize\": 10, \"date\": \"2026-10-04\", \"space\": \"Terrace\", \"packages\": [[\"premium-bottomless-brunch\", 10]]},\n   \"expect\": {\"rejects\": false, \"applied\": [\"5a9e2c74\"], \"writes\": {\"booking.type\": \"request\", \"booking.enquiryReason\": \"operatorRule\"}}},\n  {\"rules\": [\"3f2b8c1e\", \"5a9e2c74\", \"b8d3f6a0\"], \"input\": {\"partySize\": 10, \"date\": \"2026-10-08\", \"space\": \"Terrace\", \"packages\": [[\"premium-bottomless-brunch\", 10]]},\n   \"expect\": {\"rejects\": false, \"applied\": [], \"writes\": {}}},\n  {\"rules\": [\"3f2b8c1e\", \"5a9e2c74\", \"b8d3f6a0\"], \"input\": {\"partySize\": 10, \"date\": \"2026-10-08\", \"space\": \"Terrace\"},\n   \"expect\": {\"rejects\": true, \"applied\": [\"3f2b8c1e\"], \"writes\": {}}}\n]\n```\n\nThe third check shows why step 5 matters: with the same party and no pre-order line, the Terrace rule `3f2b8c1e` would reject (`BKRUL-N-100`).\n\n## Common mistakes\n\n1. **Using ids instead of slugs.** Rules identify products, packages and menus by their **aggregation slug** (`slug` in `GET /venues` and in `appliesTo.products[]` / `appliesTo.items`), never by the venue-specific `id` you send in `preorders`, and never by the `id` fields of this API (those are operator-side catalog ids, informational only).\n2. **Forgetting that items without a slug are invisible.** A pre-order line whose package or menu has no aggregation slug is dropped before any rule runs: it neither triggers nor satisfies a rule, and a rule with a product list never applies to a product that has no slug.\n3. **Reading `booking.partySize` (or any other fact) inside a list predicate.** Inside `filter`, `map`, `some`, `all`, `none`, `{\"var\": \"x\"}` reads the field of the CURRENT element; the rest of the facts are not reachable there, so the path resolves to null and the comparison clashes. Compute such comparisons outside the predicate (pattern 4).\n4. **Assuming skipped means failed.** A rule that cannot be evaluated (a null fact where an operator needs a number, a type clash, a division by zero) is SKIPPED: it does not apply and it never refuses the booking. A skip also does not mean you are compliant: the operator's system or other checks may still refuse. Do not use a skip to \"pass\" a rule; send the fact (for example `spaceId`) instead.\n5. **Assuming an amend only evaluates what you changed.** An amend is evaluated on the FINAL state of the booking against every applicable rule: a contact-only amend of a booking that already violates a rule can be rejected. For `lock_preorder_edits` the comparison is final lines against stored lines.\n6. **Expecting expression violations together with fixed ones.** Expression rules run only after every fixed rule passed. Fix the fixed violations first, then expect expression rules to speak.\n7. **Off-by-one on the boundaries.** `preorder_required` is inclusive (party size >= `threshold`); `route_to_enquiry` and `lock_preorder_edits` are exclusive (`daysAhead < leadTimeDays`; exactly `leadTimeDays` ahead is outside); `max_quantity` and `max_quantity_party_size` allow equality; deposits tolerate half a penny.\n8. **Using UTC for `daysAhead`.** It is counted in whole calendar days of the Europe/London date, time of day ignored.\n9. **Forgetting `whenApplied`.** `route_to_enquiry` never runs on an amend, an expression rule at `booking.amend` never runs on create, and a rule at the `payment` hook (`booking_payment`) runs on neither: it runs only when the amount payable of an existing booking is computed. Keep only rules whose `whenApplied` contains your operation.\n10. **Parsing `summary`, `description` or default messages.** They are English text that can change. Branch on `effect`, `params`, `errorCode` and `outcomes`.\n11. **Treating the literals in `when` as facts about you.** A number compared with `partner.id` or `partner.categoryIds` is whatever the operator wrote; use `caller` for your own values and `appliesTo.partnerScope` for how the rule is aimed at you.\n12. **Using `in` on a string as a membership test.** With a string haystack `in` is an ordinal substring test (`{\"in\": [\"a\", \"banana\"]}` is true); membership needs a list haystack.\n13. **Caching the answer across bookings.** Rules can change at any time; the 400 of the booking endpoint is the final word.\n14. **Evaluating a `payment` rule yourself.** You cannot rebuild `payment.bands` from your booking request, and a `payment` rule never refuses a booking (a `reject` there is ignored). Use it to understand why the amount payable may be the sum of several bands, then read the amount from `GET /venues/bookings/{bookingId}/payment-options` (`getPaymentOptions`).\n\n#### Scenarios - mistakes with fixed effects\n\n```json\n[\n  {\"rule\": \"9c1d5e72\", \"partySize\": 6, \"date\": \"2026-11-14\", \"create\": false, \"original\": {\"packages\": [[\"premium-bottomless-brunch\", 2], [\"standard-bottomless-brunch\", 4]]}, \"packages\": [[\"premium-bottomless-brunch\", 2], [\"standard-bottomless-brunch\", 4]], \"expect\": {\"violation\": \"BKRUL-N-001\", \"message\": \"If this package is chosen, everyone must have the same one and nothing else can be added.\"}}\n]\n```\n\nThe scenario above is mistake 5: nothing about the pre-order changes, yet the amend is evaluated on the final state and violates the rule.\n\n#### Checks - mistakes with expressions\n\n```json\n[\n  {\"rule\": {\"when\": {\"some\": [{\"var\": \"booking.items\"}, {\">\": [{\"var\": \"quantity\"}, {\"var\": \"booking.partySize\"}]}]}, \"then\": [{\"reject\": {\"message\": \"A line is larger than the party.\"}}]},\n   \"input\": {\"partySize\": 2, \"packages\": [[\"premium-bottomless-brunch\", 3]]}, \"expect\": {\"applies\": \"skipped\"}},\n  {\"rule\": {\"when\": {\"==\": [{\"var\": \"space.name\"}, \"Terrace\"]}, \"then\": [{\"reject\": {\"message\": \"No Terrace.\"}}]},\n   \"input\": {\"partySize\": 4}, \"expect\": {\"applies\": \"skipped\"}},\n  {\"rule\": {\"when\": {\"in\": [\"a\", \"banana\"]}, \"then\": [{\"flag\": {\"name\": \"substring\"}}]},\n   \"input\": {\"partySize\": 4}, \"expect\": {\"applies\": true}},\n  {\"rule\": {\"when\": {\">\": [{\"var\": \"booking.commission\"}, 15]}, \"then\": [{\"reject\": {\"message\": \"Commission above 15 is not accepted.\"}}]},\n   \"input\": {\"partySize\": 4, \"commission\": null}, \"expect\": {\"applies\": \"skipped\"}}\n]\n```\n\nThe first check is mistake 3 (`booking.partySize` inside the predicate: the rule is skipped, not true and not false), the second is mistake 4 (no `spaceId`, so `space.name` is null and the comparison with a string is a type clash), the third is mistake 12, and the fourth is a null fact without a default (pattern 7 shows the null-safe form).\n\n## What is not guaranteed\n\n- Results are a pre-check; the platform may reject for reasons not described here (availability, other validation).\n- Pre-order lines whose package or menu has no aggregation slug are invisible to every rule (the platform drops\n  them before evaluating), so they never trigger nor satisfy a rule.\n- An amend is evaluated on the **final** state of the booking against every applicable rule, whether or not the amend\n  touches the pre-order: a contact-only amend of a booking that already violates a rule can be rejected.\n- `summary`, `description` and default messages are English text generated or written by the operator; do not parse them.\n- Whether the platform evaluates a `PUT` amend with the pre-order lines stored on the booking or with none (the PUT\n  request body has no `preorders` field) is not verified; prefer `PATCH` when you change pre-orders.\n- A `payment` rule is documented from the platform's code and unit tests. Availability lists the minimum-spend bands a\n  slot crosses (each with its own value, no total) and `GET /venues` shows the configured rules, so neither is the\n  amount payable. Whether any particular operator has\n  configured a `payment` rule is only visible in this API's answer, and the amount to pay is only authoritative in the\n  payment options response.\n"
  },
  "servers": [
    {
      "url": "https://api.bookabletech.com",
      "description": "Live"
    },
    {
      "url": "https://api-sandbox.bookabletech.com",
      "description": "Sandbox"
    }
  ],
  "tags": [
    {
      "name": "PartnerBookingRules",
      "description": "Read the booking condition rules that are active for the calling partner."
    }
  ],
  "paths": {
    "/booking-rules": {
      "get": {
        "operationId": "getPartnerBookingRules",
        "summary": "List the booking condition rules that are active for the calling partner",
        "tags": [
          "PartnerBookingRules"
        ],
        "description": "Returns every **active** booking condition rule that applies to the calling partner, across all the operators\n(venue groups) the partner is linked to (the same operators `GET /venues` exposes), together with the\ndescription of every hook referenced by an expression rule in the response (`hooks[]`: its facts, how to\nobtain each fact from your booking request, and its writable paths).\n\nA rule is returned when it is active and it has no partner target, or it targets the calling partner, or it\ntargets one of the calling partner's categories. Rules aimed only at other partners are never returned, and\nno other partner is identified: `appliesTo.partnerScope` says whether the rule is aimed at everybody\n(`all_partners`), at you (`you`) or at a category you belong to (`your_categories`).\n\n`appliesTo.venues[]` lists only the venues you can see (the same visibility as `GET /venues`). A rule restricted\nto venues none of which you can see is not returned; `allVenues: false` with a non-empty `venues[]` means \"only\nthese of your venues\".\n\nThe partner is taken from the credentials (or from `X-Partner-Reference` for Bookable Agents, as on the other\npartner endpoints). There is no operator in the path. The response is not paginated.\n\nFilters narrow the response and are combined with AND:\n- `operatorId`: only rules of that operator (venue group).\n- `venueId`: only rules that apply to that venue. A rule with no venue target applies to every venue; a rule\n  with venue targets is returned only if it lists that venue.\n- `effect`: only rules with that effect.\n- `hook`: only rules that apply at that moment of the flow: `booking.create` returns the rules whose\n  `whenApplied` contains `booking_create` (fixed rules that run on create and expression rules at the\n  `booking.create` hook), `booking.amend` those whose `whenApplied` contains `booking_amend`, `payment` those\n  whose `whenApplied` contains `booking_payment` (only expression rules at the `payment` hook: they change the\n  amount payable for an existing booking, see \"Rules at the `payment` hook\" in the API description).\n\n`rules[]` is ordered by `operatorId`, then fixed effects (by the precedence order in the guide: `lock_preorder_edits`,\n`preorder_required`, `same_package`, `max_quantity_party_size`, `max_quantity`, `require_deposit`, then\n`route_to_enquiry`, which never rejects; rules of one effect by rule id), then expression rules by `hook`\n(`booking.create`, then `booking.amend`, then `payment`), `priority` ascending (no priority counts as 100) and rule id, which is\nthe order the platform evaluates expression rules.\n\nProperties whose value is null are omitted from the JSON; arrays and objects that the schema declares required\nare always present, empty when there is nothing to list.\n\nRead the \"Integration guide for AI agents\", the effects table, the expression language reference and the worked\nexamples in the API description first.\n\nResponses:\n\n| Status | Meaning | Error code |\n|---|---|---|\n| 200 | The rules (possibly none) and the hooks they reference. | |\n| 400 | Unknown `hook`, `operatorId` below 1, malformed `venueId`, `venueId` of another operator than `operatorId`, or a query value of the wrong type. | none, or `VALID-N-001` for a wrong type |\n| 401 | Missing, expired or invalid bearer token. | none |\n| 403 | Operator or venue not available to you, or unknown `X-Partner-Reference`. | none, `IDENT-N-002`, `IDENT-N-003` |\n| 404 | `venueId` does not exist. | `VENUE-N-001` |\n| 429 | Rate limit exceeded: retry after `Retry-After`. | `RATE-R-001` |\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/PartnerReference"
          },
          {
            "$ref": "#/components/parameters/CorrelationID"
          },
          {
            "name": "operatorId",
            "in": "query",
            "required": false,
            "description": "Only rules of this operator (venue group); the operator id is the first segment of every composite id (17 in `17|X9|9d3f...`). Must be at least 1 (otherwise 400) and an operator the calling partner is linked to (otherwise 403). Omit it to get the rules of every operator you are linked to.\n",
            "schema": {
              "type": "integer",
              "format": "int32",
              "minimum": 1
            },
            "example": 17
          },
          {
            "name": "venueId",
            "in": "query",
            "required": false,
            "description": "Composite venue id (`{operatorId}|{rmsSlug}|{venueResourceId}`, as returned by `GET /venues`; a 4-segment product `compositeId` is also accepted and its first three segments are used). Only rules that apply to this venue are returned: rules with no venue target (every venue) and rules that list this venue. The venue must be visible to the calling partner. When both `operatorId` and `venueId` are given the venue must belong to that operator, otherwise the call fails with 400. A venue of an operator you are not linked to, or a venue your partner mapping hides, fails with 403; a venue that does not exist fails with 404 (`VENUE-N-001`). A value that is not a composite venue id fails with 400. Send the `|` characters percent-encoded (`%7C`).\n",
            "schema": {
              "type": "string"
            },
            "examples": {
              "venue": {
                "summary": "A venue id, as returned by GET /venues",
                "value": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93"
              },
              "product": {
                "summary": "A 4-segment product compositeId (only its first three segments are used)",
                "value": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9"
              }
            }
          },
          {
            "name": "effect",
            "in": "query",
            "required": false,
            "description": "Only rules with this effect: one of `same_package`, `max_quantity_party_size`, `max_quantity`, `preorder_required`, `route_to_enquiry`, `lock_preorder_edits`, `require_deposit`, `preorder_email`, `booking_window`, `expression`. A value outside this list fails with 400 (`VALID-N-001`).",
            "schema": {
              "$ref": "#/components/schemas/PartnerBookingRuleEffect"
            },
            "example": "require_deposit"
          },
          {
            "name": "hook",
            "in": "query",
            "required": false,
            "description": "Only rules that apply at this moment of the flow: `booking.create` (rules checked when you create a booking), `booking.amend` (rules checked when you amend one with PUT or PATCH) or `payment` (expression rules that change the amount payable when the platform computes the payment options or creates the payment of an existing booking; they have `whenApplied` `booking_payment`). Free string checked against the hooks the platform knows (today those three); not an enum so that adding a hook never changes this specification. An unknown hook fails with 400. There is no cancel hook: `booking_window` rules with `action` cancel are returned without this filter, with `whenApplied` `booking_cancel`.\n",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "example": "booking.create"
          }
        ],
        "responses": {
          "200": {
            "description": "The active rules that apply to the calling partner (possibly none), and the hooks referenced by the expression rules in the response. Properties whose value is null are omitted. The named examples show every effect (`allRules` holds one rule of every kind, including a `payment` rule; the other examples reuse those rules one by one), the answer of the end-to-end walkthrough (`walkthrough`) and the empty answer (`noRules`).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartnerBookingRulesResponse"
                },
                "examples": {
                  "allRules": {
                    "summary": "A rich set of rules for a partner in category 1: every effect, all three hooks",
                    "description": "Illustrative operator 17 with one rule of every kind (a real operator has far fewer). Fixed effects come first in precedence order, then the expression rules by hook and priority; the order is the order the platform evaluates them. All the other examples below reuse these very rules (same ids). Fixed rules carry `params` and no `hook` / `when` / `then` / `outcomes`; expression rules carry the opposite. Properties whose value is null are omitted from the JSON.",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "7a3d1f96-0b58-4c2e-8e49-b6f5d0a2c817",
                          "operatorId": 17,
                          "name": "Pre-orders locked inside 7 days",
                          "effect": "lock_preorder_edits",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "your_categories"
                          },
                          "conditions": [],
                          "params": {
                            "leadTimeDays": 7
                          },
                          "summary": "Selected partners cannot add or change pre-orders less than 7 days before the booking date.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-005"
                        },
                        {
                          "id": "c58b0a92-e4d7-4f21-9a36-7b1f3d8e0c45",
                          "operatorId": 17,
                          "name": "Parties of 8 or more must pre-order brunch",
                          "effect": "preorder_required",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1257,
                                "slug": "bottomless-brunch",
                                "name": "Bottomless Brunch"
                              }
                            ],
                            "items": {
                              "packages": [
                                {
                                  "id": 201,
                                  "slug": "premium-bottomless-brunch",
                                  "name": "Premium Bottomless Brunch"
                                },
                                {
                                  "id": 202,
                                  "slug": "standard-bottomless-brunch",
                                  "name": "Standard Bottomless Brunch"
                                }
                              ],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "threshold": 8
                          },
                          "summary": "Groups of 8 or more must include a pre-order from: Premium Bottomless Brunch, Standard Bottomless Brunch — Bottomless Brunch.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-004"
                        },
                        {
                          "id": "f2a6d3c1-95b8-4e07-a4d2-1c7e0b9f6a38",
                          "operatorId": 17,
                          "name": "Parties of 12 or more must pre-order something",
                          "effect": "preorder_required",
                          "active": true,
                          "appliesTo": {
                            "allVenues": false,
                            "venues": [
                              {
                                "id": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93",
                                "name": "The Rooftop Kitchen"
                              }
                            ],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "threshold": 12
                          },
                          "summary": "Groups of 12 or more must include a pre-order at The Rooftop Kitchen.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-004"
                        },
                        {
                          "id": "2b7f9d40-8c15-4a63-b0e8-5d1c9a3f7e24",
                          "operatorId": 17,
                          "name": "Brunch - one package type per table, whole group",
                          "effect": "same_package",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1257,
                                "slug": "bottomless-brunch",
                                "name": "Bottomless Brunch"
                              }
                            ],
                            "items": {
                              "packages": [
                                {
                                  "id": 201,
                                  "slug": "premium-bottomless-brunch",
                                  "name": "Premium Bottomless Brunch"
                                },
                                {
                                  "id": 202,
                                  "slug": "standard-bottomless-brunch",
                                  "name": "Standard Bottomless Brunch"
                                }
                              ],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "exclusive": false,
                            "wholeGroup": true
                          },
                          "summary": "Everyone in the party must have the same choice from: Premium Bottomless Brunch, Standard Bottomless Brunch. Once one is chosen, its quantity must equal the party size. — Bottomless Brunch",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-001"
                        },
                        {
                          "id": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36",
                          "operatorId": 17,
                          "name": "Premium brunch - whole table, no mixing",
                          "description": "If any of: Premium Bottomless Brunch is chosen, everyone must have that same choice and nothing else can be selected.",
                          "effect": "same_package",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1257,
                                "slug": "bottomless-brunch",
                                "name": "Bottomless Brunch"
                              }
                            ],
                            "items": {
                              "packages": [
                                {
                                  "id": 201,
                                  "slug": "premium-bottomless-brunch",
                                  "name": "Premium Bottomless Brunch"
                                }
                              ],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "exclusive": true,
                            "wholeGroup": false
                          },
                          "summary": "If any of: Premium Bottomless Brunch is chosen, everyone must have that same choice and nothing else can be selected. — Bottomless Brunch",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-001"
                        },
                        {
                          "id": "6d40c8e1-7a92-4b35-8f10-c2e5a9b17d63",
                          "operatorId": 17,
                          "name": "Pre-order quantities cannot exceed the party size",
                          "effect": "max_quantity_party_size",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "The total number of packages and menus cannot exceed the party size.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-002"
                        },
                        {
                          "id": "a13e5f87-2c04-49d6-b7a1-0e8d4c6f9b52",
                          "operatorId": 17,
                          "name": "At most 4 kids menus per booking",
                          "effect": "max_quantity",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": [
                                {
                                  "id": 311,
                                  "slug": "kids-menu",
                                  "name": "Kids Menu"
                                }
                              ]
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "max": 4
                          },
                          "summary": "No more than 4 × Kids Menu per booking.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-003"
                        },
                        {
                          "id": "d4b82e60-3f19-47a5-91c7-e5a0f8b3d296",
                          "operatorId": 17,
                          "name": "Flat 100 pound deposit for Sunday lunch",
                          "effect": "require_deposit",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1258,
                                "slug": "sunday-lunch",
                                "name": "Sunday Lunch"
                              }
                            ],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "amount": 100,
                            "per": "booking"
                          },
                          "summary": "A deposit of £100.00 per booking must be collected before booking — Sunday Lunch.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "requires_deposit",
                          "errorCode": "BKRUL-N-006"
                        },
                        {
                          "id": "e07a4b19-6c2d-48f3-b5a1-3d9c70f2e864",
                          "operatorId": 17,
                          "name": "Deposit of 10 pounds per person",
                          "description": "A deposit of 10.00 per person must be collected before booking.",
                          "effect": "require_deposit",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "you"
                          },
                          "conditions": [],
                          "params": {
                            "amount": 10,
                            "per": "person"
                          },
                          "message": "Please collect the 10 pound per person deposit and send it as depositAmount.",
                          "summary": "A deposit of £10.00 per person must be collected before booking for selected partners.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "requires_deposit",
                          "errorCode": "BKRUL-N-006"
                        },
                        {
                          "id": "0e9c7b35-a1d4-4862-b5f3-9d2a6c8e1f70",
                          "operatorId": 17,
                          "name": "Inside 7 days: send as an enquiry",
                          "effect": "route_to_enquiry",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "your_categories"
                          },
                          "conditions": [],
                          "params": {
                            "leadTimeDays": 7
                          },
                          "summary": "Bookings from selected partners made less than 7 days before the booking date are sent as enquiries, not auto-confirmed.",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "turns_booking_into_enquiry"
                        },
                        {
                          "id": "84f1a7c3-6e20-4d9b-a5b8-2c0d7e9f3a61",
                          "operatorId": 17,
                          "name": "Large parties are enquiries",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When booking.partySize >= 12 then set booking.type = \"request\"; set booking.enquiryReason = \"operatorRule\"; flag: large-party",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "hook": "booking.create",
                          "priority": 50,
                          "when": {
                            ">=": [
                              {
                                "var": "booking.partySize"
                              },
                              12
                            ]
                          },
                          "then": [
                            {
                              "set": {
                                "path": "booking.type",
                                "value": "request"
                              }
                            },
                            {
                              "set": {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule"
                              }
                            },
                            {
                              "flag": {
                                "name": "large-party"
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": false,
                            "rejectMessages": [],
                            "writes": [
                              {
                                "path": "booking.type",
                                "value": "request",
                                "isExpression": false
                              },
                              {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule",
                                "isExpression": false
                              }
                            ],
                            "flags": [
                              "large-party"
                            ]
                          }
                        },
                        {
                          "id": "3f2b8c1e-5a4d-4e7b-9c10-2d6a8e41b7f3",
                          "operatorId": 17,
                          "name": "Terrace over 8 needs a pre-order",
                          "description": "When space.name = Terrace AND booking.partySize > 8 AND booking.itemCount = 0 then reject.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": false,
                            "venues": [
                              {
                                "id": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93",
                                "name": "The Rooftop Kitchen"
                              }
                            ],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When space.name = \"Terrace\" AND booking.partySize > 8 AND booking.itemCount = 0 then reject: Parties of more than 8 on the Terrace must include a pre-order. (applies to venues: The Rooftop Kitchen)",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "errorCode": "BKRUL-N-100",
                          "hook": "booking.create",
                          "priority": 100,
                          "when": {
                            "and": [
                              {
                                "==": [
                                  {
                                    "var": "space.name"
                                  },
                                  "Terrace"
                                ]
                              },
                              {
                                ">": [
                                  {
                                    "var": "booking.partySize"
                                  },
                                  8
                                ]
                              },
                              {
                                "==": [
                                  {
                                    "var": "booking.itemCount"
                                  },
                                  0
                                ]
                              }
                            ]
                          },
                          "then": [
                            {
                              "reject": {
                                "message": "Parties of more than 8 on the Terrace must include a pre-order."
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": true,
                            "rejectMessages": [
                              "Parties of more than 8 on the Terrace must include a pre-order."
                            ],
                            "writes": [],
                            "flags": []
                          }
                        },
                        {
                          "id": "5a9e2c74-d013-4b86-97f5-e8a14c3b60d2",
                          "operatorId": 17,
                          "name": "Travel/Party enquiry inside 7 days",
                          "description": "Bookings by partners in these categories made less than 7 days ahead become enquiries.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When some(partner.categoryIds, item IN [1, 2]) AND booking.daysAhead < 7 then set booking.type = \"request\"; set booking.enquiryReason = \"operatorRule\"",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "hook": "booking.create",
                          "priority": 100,
                          "when": {
                            "and": [
                              {
                                "some": [
                                  {
                                    "var": "partner.categoryIds"
                                  },
                                  {
                                    "in": [
                                      {
                                        "var": ""
                                      },
                                      [
                                        1,
                                        2
                                      ]
                                    ]
                                  }
                                ]
                              },
                              {
                                "<": [
                                  {
                                    "var": "booking.daysAhead"
                                  },
                                  7
                                ]
                              }
                            ]
                          },
                          "then": [
                            {
                              "set": {
                                "path": "booking.type",
                                "value": "request"
                              }
                            },
                            {
                              "set": {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule"
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": false,
                            "rejectMessages": [],
                            "writes": [
                              {
                                "path": "booking.type",
                                "value": "request",
                                "isExpression": false
                              },
                              {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule",
                                "isExpression": false
                              }
                            ],
                            "flags": []
                          }
                        },
                        {
                          "id": "b8d3f6a0-1e57-4c92-8a4b-6f0e2d9c5173",
                          "operatorId": 17,
                          "name": "Brunch packages cannot exceed party size",
                          "description": "The brunch packages chosen may not outnumber the guests.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "your_categories"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When sum(pluck(filter(booking.items, item.slug IN [\"premium-bottomless-brunch\", \"standard-bottomless-brunch\"]), \"quantity\")) > booking.partySize then reject: You cannot select more brunch packages than guests. (applies to partners: selected)",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "errorCode": "BKRUL-N-100",
                          "hook": "booking.create",
                          "priority": 100,
                          "when": {
                            ">": [
                              {
                                "sum": {
                                  "pluck": [
                                    {
                                      "filter": [
                                        {
                                          "var": "booking.items"
                                        },
                                        {
                                          "in": [
                                            {
                                              "var": "slug"
                                            },
                                            [
                                              "premium-bottomless-brunch",
                                              "standard-bottomless-brunch"
                                            ]
                                          ]
                                        }
                                      ]
                                    },
                                    "quantity"
                                  ]
                                }
                              },
                              {
                                "var": "booking.partySize"
                              }
                            ]
                          },
                          "then": [
                            {
                              "reject": {
                                "message": "You cannot select more brunch packages than guests."
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": true,
                            "rejectMessages": [
                              "You cannot select more brunch packages than guests."
                            ],
                            "writes": [],
                            "flags": []
                          }
                        },
                        {
                          "id": "1c6e9b48-f7a3-4025-b8d1-a9e3c5f07d82",
                          "operatorId": 17,
                          "name": "Premium brunch cannot be dropped on amend",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When some(booking.originalItems, item.slug = \"premium-bottomless-brunch\") AND none(booking.items, item.slug = \"premium-bottomless-brunch\") then reject: The Premium Bottomless Brunch cannot be removed from an existing booking.",
                          "whenApplied": [
                            "booking_amend"
                          ],
                          "consequence": "conditional",
                          "errorCode": "BKRUL-N-100",
                          "hook": "booking.amend",
                          "priority": 100,
                          "when": {
                            "and": [
                              {
                                "some": [
                                  {
                                    "var": "booking.originalItems"
                                  },
                                  {
                                    "==": [
                                      {
                                        "var": "slug"
                                      },
                                      "premium-bottomless-brunch"
                                    ]
                                  }
                                ]
                              },
                              {
                                "none": [
                                  {
                                    "var": "booking.items"
                                  },
                                  {
                                    "==": [
                                      {
                                        "var": "slug"
                                      },
                                      "premium-bottomless-brunch"
                                    ]
                                  }
                                ]
                              }
                            ]
                          },
                          "then": [
                            {
                              "reject": {
                                "message": "The Premium Bottomless Brunch cannot be removed from an existing booking."
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": true,
                            "rejectMessages": [
                              "The Premium Bottomless Brunch cannot be removed from an existing booking."
                            ],
                            "writes": [],
                            "flags": []
                          }
                        },
                        {
                          "id": "d2a4f8b1-9c37-4e60-b1a5-7f3e0c6d8a29",
                          "operatorId": 17,
                          "name": "Final balance is the sum of the minimum-spend bands crossed",
                          "description": "When a booking spans several minimum-spend bands, the amount payable is the sum of those bands.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When payment.bandCount > 0 then set payment.finalBalance = sum(pluck(payment.bands, \"spendValue\"))",
                          "whenApplied": [
                            "booking_payment"
                          ],
                          "consequence": "conditional",
                          "hook": "payment",
                          "priority": 100,
                          "when": {
                            ">": [
                              {
                                "var": "payment.bandCount"
                              },
                              0
                            ]
                          },
                          "then": [
                            {
                              "set": {
                                "path": "payment.finalBalance",
                                "value": {
                                  "sum": {
                                    "pluck": [
                                      {
                                        "var": "payment.bands"
                                      },
                                      "spendValue"
                                    ]
                                  }
                                }
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": false,
                            "rejectMessages": [],
                            "writes": [
                              {
                                "path": "payment.finalBalance",
                                "value": {
                                  "sum": {
                                    "pluck": [
                                      {
                                        "var": "payment.bands"
                                      },
                                      "spendValue"
                                    ]
                                  }
                                },
                                "isExpression": true
                              }
                            ],
                            "flags": []
                          }
                        }
                      ],
                      "hooks": [
                        {
                          "name": "booking.create",
                          "description": "Runs before a partner booking is created in the RMS. A rule can reject the booking, turn it into an enquiry (set booking.type / booking.enquiryReason) or flag it.",
                          "triggeredBy": [
                            "POST /venues/{compositeId}/booking"
                          ],
                          "writablePaths": [
                            {
                              "path": "booking.type",
                              "type": "string",
                              "allowedValues": [
                                "book",
                                "request"
                              ],
                              "meaning": "The booking type. Writing `request` makes the booking an enquiry the operator must accept instead of an instant confirmation. Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "allowedValues": [
                                "operatorRule",
                                "none"
                              ],
                              "meaning": "Why the booking is an enquiry. `operatorRule` marks the decision as made by an operator rule (the Collins integration then skips its per-partner enquiry-policy checks). Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "allowedValues": [],
                              "meaning": "Whether the Collins pre-order email is sent. Writing `false` means the email is not sent; the pre-order itself is still posted to Collins. Any non-boolean value makes the rule be skipped. At `booking.amend` nothing is writable, but when the booking is amended after a create that skipped the email, the `booking.create` rules are re-evaluated with the amended booking's facts, once, only to read this path: the email is sent at that amend if it is now `true` (never twice); rejections and other writes are ignored, and `booking.isCreate` is `false` there."
                            }
                          ],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "Copy of `partySize` of the create request body. `children` is not added to it."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "`duration` of the request. If you omit it the platform first substitutes the minimum duration computed from the product's booking rules and only then runs the rules, so the fact is rarely null; send `duration` yourself if a rule reads it."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "Copy of `date` of the request (YYYY-MM-DD). ISO dates compare correctly as strings with > < >= <=."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "Copy of `time` of the request, 24-hour HH:mm, as sent (no time zone conversion)."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(`date` of the request) minus (today's date in Europe/London when the platform processes the request), in whole calendar days; the time of day is ignored. Today 2026-10-01 and date 2026-10-04 gives 3; same day 0; past dates negative. Use the Europe/London calendar day, not UTC."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "`type` of the request: `request` stays `request`, anything else reads as `book`. If a fixed `route_to_enquiry` rule applies to your booking and `daysAhead` is below its `leadTimeDays`, the value is `request` even when you sent `book` (evaluate that rule first). Earlier expression rules may also have written it."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "`operatorRule` when a fixed `route_to_enquiry` rule has turned your booking into an enquiry (evaluate it first), otherwise `none`. The create request has no field for it."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "True unless a fixed `preorder_email` rule (party size below its `threshold`) or an earlier expression rule set `booking.sendPreorderEmail = false`. The create request has no field for it."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always true on booking.create."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "true when you send the request header `X-Is-Cross-Sale: true`, otherwise false (never null on create)."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "Copy of `commission` of the request; null when you do not send it."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "Copy of `depositAmount` of the create request; null when you do not send it."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when `notes` of the request is present and not blank (not only whitespace), otherwise false."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when `company` of the request is present and not blank. If you omit `company`, the platform fills in the operator's default company for your partner or channel, if one is configured, BEFORE rules run: the fact can be true although you sent no company."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "One element per line of `preorders.packages[]` (kind `package`) followed by one per line of `preorders.menus[]` (kind `menu`). `slug` is the aggregation slug of the package or menu whose venue-specific `id` you sent (the `slug` field of that package or menu in `GET /venues`); `quantity` is the line's `quantity` (for a menu the menu quantity, not its items). A line whose package or menu is unknown or has no aggregation slug is silently dropped. Empty list when you send no pre-order.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Length of `booking.items` (number of lines, not the sum of quantities)."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `package`."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `menu`."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "Always an empty list on booking.create.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "On create: true when `booking.items` is not empty."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "Your own partner id: `caller.partnerId` in this response (the partner of your credentials, or of `X-Partner-Reference` for agents)."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "Your own partner category ids for the rule's operator: `caller.categoryIdsByOperator[rule.operatorId]` in this response."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The `slug` of the product you book, i.e. of the product identified by the 4-segment `compositeId` in the path, as listed in `GET /venues`. Null if the product has no slug."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The `compositeId` in the path without its last segment: for 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9 it is 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93. Rule venue targets in this API use the same form."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "First segment of the `compositeId` in the path (the `operatorId`)."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "`spaceId` of the request when it resolves to a space of that venue; null when you send no `spaceId` or it cannot be resolved (then every space.* fact is null and a rule comparing them is skipped)."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space identified by `spaceId`; null when there is no resolvable space or the space has none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The display name of the space identified by `spaceId` (the space names returned in the `spaces` of the availability time slots); null when there is no resolvable space."
                            }
                          ]
                        },
                        {
                          "name": "booking.amend",
                          "description": "Runs before a partner booking is amended (PUT or PATCH). A rule can reject the change or flag it; nothing is writable, an amend never un-confirms a booking.",
                          "triggeredBy": [
                            "PUT /venues/bookings/{bookingId}",
                            "PATCH /venues/bookings/{bookingId}"
                          ],
                          "writablePaths": [],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "The party size the booking will have after the amend: `partySize` of the PUT body, or the value after applying the PATCH operations."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "`duration` of the amend request; when absent, the duration stored on the booking; null if neither exists."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "The date the booking will have after the amend (YYYY-MM-DD)."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "The time the booking will have after the amend, 24-hour HH:mm."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(the booking date AFTER the amend) minus (today's date in Europe/London when the platform processes the request), in whole calendar days."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always false on booking.amend."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "`commission` of the amend request; null when absent."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "The deposit amount STORED on the booking before the amend (an amend request cannot change it for this check); null when the booking has none."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when `notes` of the amend request (after PATCH operations) is present and not blank."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when `company` of the amend request (after PATCH operations) is present and not blank."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "The pre-order lines the booking will have AFTER the amend (for PATCH, after applying the operations on /preorders), one element per package or menu line with its aggregation slug (the `slug` in `GET /venues`) and quantity; lines without an aggregation slug are dropped. How a PUT, whose body has no `preorders` field, is evaluated: not verified.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` or `menu`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Quantity of the line."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Length of `booking.items`."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Number of `booking.items` elements of kind `package`."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Number of `booking.items` elements of kind `menu`."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "The pre-order lines currently stored on the booking (before this amend), mapped to kind, aggregation slug and quantity like `booking.items`; lines without a slug are dropped.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` or `menu`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Quantity of the line."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "true when the multiset of (kind, slug, quantity) of `booking.items` differs from that of `booking.originalItems` (order ignored)."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "`caller.partnerId` in this response."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "`caller.categoryIdsByOperator[rule.operatorId]` in this response."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The `slug` of the product of the booking (the product its `compositeId` identifies, as listed in `GET /venues`); null if it has none."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The booking's `compositeId` without its last segment ({operatorId}|{rmsSlug}|{venueId})."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "First segment of the booking's `compositeId`."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "The space STORED on the booking (an amend request does not change it); null when the booking has no resolvable space."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space stored on the booking; null when none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The name of the space stored on the booking; null when none."
                            }
                          ]
                        },
                        {
                          "name": "payment",
                          "description": "Runs when the payment amounts of an existing booking are computed (payment options and payment creation). A rule can set payment.finalBalance: the total payable minimum spend, an integer number of pence (GBP), a finite number >= 0 (any other value, negative or not a number, makes the rule be skipped). Read payment.bands (the minimum-spend bands the booked space-and-time spans), payment.bandCount and payment.minimumSpend (today's single start-time value). A deposit option is computed on the final balance. A reject or flag action has no effect here (the platform ignores it); the hook is not run on create or amend.",
                          "triggeredBy": [
                            "GET /venues/bookings/{bookingId}/payment-options",
                            "POST /venues/bookings/{bookingId}/payments"
                          ],
                          "writablePaths": [
                            {
                              "path": "payment.finalBalance",
                              "type": "number",
                              "allowedValues": [],
                              "meaning": "The total payable minimum spend, in integer pence (GBP): a finite number >= 0. Any other value (negative, not a number, above 2147483647) makes the rule be skipped; a value with decimals is rounded to the nearest penny. A deposit option is computed on this final balance."
                            }
                          ],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "Not sent by you at this hook: the party size stored on the booking."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "Not sent by you: the duration in minutes stored on the booking. Payment rules are only evaluated when it is greater than 0."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "Not sent by you: the date stored on the booking (YYYY-MM-DD)."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "Not sent by you: the start time stored on the booking, 24-hour HH:mm."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(the booking date stored on the booking) minus (today's date in Europe/London when the platform computes the amount), in whole calendar days; negative when the date is past."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "Always null at the payment hook (the platform does not pass the booking type)."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "Always null at the payment hook."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "Always null at the payment hook."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always false at the payment hook (the booking already exists)."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "Always null at the payment hook."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "Not sent by you: the commission stored on the booking; null when it has none."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "Not sent by you: the deposit amount stored on the booking; null when it has none."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when the notes stored on the booking are present and not blank."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when the customer company stored on the booking is present and not blank."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "Always an empty list at the payment hook: the platform does not load the pre-order lines for this computation.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Always 0 at the payment hook (`booking.items` is always empty)."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Always 0 at the payment hook (`booking.items` is always empty)."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Always 0 at the payment hook (`booking.items` is always empty)."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "Always an empty list at the payment hook.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "Always false at the payment hook (no pre-order lines are loaded)."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "The id of the partner stored on the booking (for a booking you made, your own partner id, `caller.partnerId`)."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "The ids of the partner categories of the partner stored on the booking (for a booking you made, `caller.categoryIdsByOperator[rule.operatorId]`)."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The aggregation slug of the product stored on the booking (the `slug` of that product in `GET /venues`); null when it has none or the product cannot be found."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The venue stored on the booking, as {operatorId}|{rmsSlug}|{venueId} (the first three segments of the booking's `compositeId`)."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "The venue group (operator) id stored on the booking (first segment of the booking's `compositeId`)."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "The database id of the space stored on the booking; null when the booking has no space."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space stored on the booking (when the space has none of its own, the first zone linked to it); null when none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The name of the space stored on the booking; null when the booking has no space or none can be found."
                            },
                            {
                              "path": "payment.bands",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The minimum-spend bands of the booked space that the booking interval [start, start + duration) overlaps, in chronological order, each band once. Computed from the space's enabled minimum-spend policy (rules and date overrides) at the booking date, start time and duration; empty when the space has no such policy, no rule matches, or the booking has no duration",
                              "howToObtain": "Not sent by the partner: the platform computes it when payment amounts are computed for an existing booking, from the stored booking date, start time and duration and the booked space's enabled minimum-spend policy. Only the bands the booking interval overlaps are listed, in chronological order.",
                              "elementFields": [
                                {
                                  "path": "label",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": true,
                                  "description": "Label of the minimum-spend rule or date override, e.g. 'Evening'; null when it has none",
                                  "howToObtain": "Not sent by the partner: the label the operator gave the minimum-spend rule or date override that defines this band; null when it has none."
                                },
                                {
                                  "path": "from",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Clock time (HH:mm, Europe/London) where the booking enters this band",
                                  "howToObtain": "Not sent by the partner: computed by the platform as the later of the booking start time and the start of the band (24-hour HH:mm)."
                                },
                                {
                                  "path": "to",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Clock time (HH:mm, Europe/London) where the booking last leaves this band; 00:00 means midnight",
                                  "howToObtain": "Not sent by the partner: computed by the platform as the earlier of the booking end (start + duration) and the end of the band (24-hour HH:mm; 00:00 is midnight)."
                                },
                                {
                                  "path": "spendType",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'minimum_spend', 'hire_fee' or 'hire_fee_plus_minimum_spend': what the band's amounts mean",
                                  "howToObtain": "Not sent by the partner: the spend type configured by the operator on the band's minimum-spend rule or date override."
                                },
                                {
                                  "path": "spendValue",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": true,
                                  "description": "Minimum spend of the band in integer pence (GBP); null for a pure hire fee",
                                  "howToObtain": "Not sent by the partner: the minimum spend configured by the operator for the band, in pence; null for a pure hire fee."
                                },
                                {
                                  "path": "hireFeeValue",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": true,
                                  "description": "Hire fee of the band in integer pence (GBP); null for a pure minimum spend",
                                  "howToObtain": "Not sent by the partner: the hire fee configured by the operator for the band, in pence; null for a pure minimum spend."
                                },
                                {
                                  "path": "overlapMinutes",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Minutes of the booking that fall inside this band",
                                  "howToObtain": "Not sent by the partner: computed by the platform as the minutes between `from` and `to` that the booking spends inside this band."
                                }
                              ]
                            },
                            {
                              "path": "payment.bandCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of bands in payment.bands (0 when none apply)",
                              "howToObtain": "Not sent by the partner: the number of entries of `payment.bands`, computed by the platform."
                            },
                            {
                              "path": "payment.minimumSpend",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Minimum spend in integer pence (GBP) that the payment options use today: the first matching entry's spend value at the booking's start time only, ignoring duration; null when the space has no minimum-spend policy or no entry matches the start time",
                              "howToObtain": "Not sent by the partner: the platform looks up the booked space's minimum-spend policy at the stored booking date and start time and takes the first matching entry's spend value (pence). Null when the space has no policy or no entry matches the start time."
                            },
                            {
                              "path": "payment.currency",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Currency of every payment amount, always 'GBP'",
                              "howToObtain": "Constant: always `GBP`; the platform does not read it from anywhere."
                            }
                          ]
                        }
                      ]
                    }
                  },
                  "samePackageExclusive": {
                    "summary": "same_package with exclusive true: if the premium brunch is chosen nothing else may be (BKRUL-N-001)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36",
                          "operatorId": 17,
                          "name": "Premium brunch - whole table, no mixing",
                          "description": "If any of: Premium Bottomless Brunch is chosen, everyone must have that same choice and nothing else can be selected.",
                          "effect": "same_package",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1257,
                                "slug": "bottomless-brunch",
                                "name": "Bottomless Brunch"
                              }
                            ],
                            "items": {
                              "packages": [
                                {
                                  "id": 201,
                                  "slug": "premium-bottomless-brunch",
                                  "name": "Premium Bottomless Brunch"
                                }
                              ],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "exclusive": true,
                            "wholeGroup": false
                          },
                          "summary": "If any of: Premium Bottomless Brunch is chosen, everyone must have that same choice and nothing else can be selected. — Bottomless Brunch",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-001"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "samePackageNonExclusiveWholeGroup": {
                    "summary": "same_package with exclusive false and wholeGroup true: no mixing inside the set, and the set lines must cover the whole party (BKRUL-N-001)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "2b7f9d40-8c15-4a63-b0e8-5d1c9a3f7e24",
                          "operatorId": 17,
                          "name": "Brunch - one package type per table, whole group",
                          "effect": "same_package",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1257,
                                "slug": "bottomless-brunch",
                                "name": "Bottomless Brunch"
                              }
                            ],
                            "items": {
                              "packages": [
                                {
                                  "id": 201,
                                  "slug": "premium-bottomless-brunch",
                                  "name": "Premium Bottomless Brunch"
                                },
                                {
                                  "id": 202,
                                  "slug": "standard-bottomless-brunch",
                                  "name": "Standard Bottomless Brunch"
                                }
                              ],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "exclusive": false,
                            "wholeGroup": true
                          },
                          "summary": "Everyone in the party must have the same choice from: Premium Bottomless Brunch, Standard Bottomless Brunch. Once one is chosen, its quantity must equal the party size. — Bottomless Brunch",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-001"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "maxQuantityPartySizeAllItems": {
                    "summary": "max_quantity_party_size with an empty item set: every package and menu counts (BKRUL-N-002)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "6d40c8e1-7a92-4b35-8f10-c2e5a9b17d63",
                          "operatorId": 17,
                          "name": "Pre-order quantities cannot exceed the party size",
                          "effect": "max_quantity_party_size",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "The total number of packages and menus cannot exceed the party size.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-002"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "maxQuantity": {
                    "summary": "max_quantity: at most 4 kids menus per booking (BKRUL-N-003)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "a13e5f87-2c04-49d6-b7a1-0e8d4c6f9b52",
                          "operatorId": 17,
                          "name": "At most 4 kids menus per booking",
                          "effect": "max_quantity",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": [
                                {
                                  "id": 311,
                                  "slug": "kids-menu",
                                  "name": "Kids Menu"
                                }
                              ]
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "max": 4
                          },
                          "summary": "No more than 4 × Kids Menu per booking.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-003"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "preorderRequiredWithItems": {
                    "summary": "preorder_required from 8 guests, satisfied only by the listed brunch packages (BKRUL-N-004)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "c58b0a92-e4d7-4f21-9a36-7b1f3d8e0c45",
                          "operatorId": 17,
                          "name": "Parties of 8 or more must pre-order brunch",
                          "effect": "preorder_required",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1257,
                                "slug": "bottomless-brunch",
                                "name": "Bottomless Brunch"
                              }
                            ],
                            "items": {
                              "packages": [
                                {
                                  "id": 201,
                                  "slug": "premium-bottomless-brunch",
                                  "name": "Premium Bottomless Brunch"
                                },
                                {
                                  "id": 202,
                                  "slug": "standard-bottomless-brunch",
                                  "name": "Standard Bottomless Brunch"
                                }
                              ],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "threshold": 8
                          },
                          "summary": "Groups of 8 or more must include a pre-order from: Premium Bottomless Brunch, Standard Bottomless Brunch — Bottomless Brunch.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-004"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "preorderRequiredAnyItem": {
                    "summary": "preorder_required from 12 guests, any package or menu satisfies it, limited to one venue (BKRUL-N-004)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "f2a6d3c1-95b8-4e07-a4d2-1c7e0b9f6a38",
                          "operatorId": 17,
                          "name": "Parties of 12 or more must pre-order something",
                          "effect": "preorder_required",
                          "active": true,
                          "appliesTo": {
                            "allVenues": false,
                            "venues": [
                              {
                                "id": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93",
                                "name": "The Rooftop Kitchen"
                              }
                            ],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "threshold": 12
                          },
                          "summary": "Groups of 12 or more must include a pre-order at The Rooftop Kitchen.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-004"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "routeToEnquiry": {
                    "summary": "route_to_enquiry: bookings made fewer than 7 days ahead become enquiries, no error (create only)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "0e9c7b35-a1d4-4862-b5f3-9d2a6c8e1f70",
                          "operatorId": 17,
                          "name": "Inside 7 days: send as an enquiry",
                          "effect": "route_to_enquiry",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "your_categories"
                          },
                          "conditions": [],
                          "params": {
                            "leadTimeDays": 7
                          },
                          "summary": "Bookings from selected partners made less than 7 days before the booking date are sent as enquiries, not auto-confirmed.",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "turns_booking_into_enquiry"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "lockPreorderEdits": {
                    "summary": "lock_preorder_edits: no pre-order changes fewer than 7 days ahead (BKRUL-N-005)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "7a3d1f96-0b58-4c2e-8e49-b6f5d0a2c817",
                          "operatorId": 17,
                          "name": "Pre-orders locked inside 7 days",
                          "effect": "lock_preorder_edits",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "your_categories"
                          },
                          "conditions": [],
                          "params": {
                            "leadTimeDays": 7
                          },
                          "summary": "Selected partners cannot add or change pre-orders less than 7 days before the booking date.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-005"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "requireDepositPerPerson": {
                    "summary": "require_deposit per person with the operator's own message (BKRUL-N-006)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "e07a4b19-6c2d-48f3-b5a1-3d9c70f2e864",
                          "operatorId": 17,
                          "name": "Deposit of 10 pounds per person",
                          "description": "A deposit of 10.00 per person must be collected before booking.",
                          "effect": "require_deposit",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "you"
                          },
                          "conditions": [],
                          "params": {
                            "amount": 10,
                            "per": "person"
                          },
                          "message": "Please collect the 10 pound per person deposit and send it as depositAmount.",
                          "summary": "A deposit of £10.00 per person must be collected before booking for selected partners.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "requires_deposit",
                          "errorCode": "BKRUL-N-006"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "requireDepositPerBooking": {
                    "summary": "require_deposit per booking (flat amount), limited to the Sunday lunch product (BKRUL-N-006)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "d4b82e60-3f19-47a5-91c7-e5a0f8b3d296",
                          "operatorId": 17,
                          "name": "Flat 100 pound deposit for Sunday lunch",
                          "effect": "require_deposit",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1258,
                                "slug": "sunday-lunch",
                                "name": "Sunday Lunch"
                              }
                            ],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "amount": 100,
                            "per": "booking"
                          },
                          "summary": "A deposit of £100.00 per booking must be collected before booking — Sunday Lunch.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "requires_deposit",
                          "errorCode": "BKRUL-N-006"
                        }
                      ],
                      "hooks": []
                    }
                  },
                  "expressionReject": {
                    "summary": "expression that rejects (BKRUL-N-100): Terrace, more than 8 guests, no pre-order",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "3f2b8c1e-5a4d-4e7b-9c10-2d6a8e41b7f3",
                          "operatorId": 17,
                          "name": "Terrace over 8 needs a pre-order",
                          "description": "When space.name = Terrace AND booking.partySize > 8 AND booking.itemCount = 0 then reject.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": false,
                            "venues": [
                              {
                                "id": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93",
                                "name": "The Rooftop Kitchen"
                              }
                            ],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When space.name = \"Terrace\" AND booking.partySize > 8 AND booking.itemCount = 0 then reject: Parties of more than 8 on the Terrace must include a pre-order. (applies to venues: The Rooftop Kitchen)",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "errorCode": "BKRUL-N-100",
                          "hook": "booking.create",
                          "priority": 100,
                          "when": {
                            "and": [
                              {
                                "==": [
                                  {
                                    "var": "space.name"
                                  },
                                  "Terrace"
                                ]
                              },
                              {
                                ">": [
                                  {
                                    "var": "booking.partySize"
                                  },
                                  8
                                ]
                              },
                              {
                                "==": [
                                  {
                                    "var": "booking.itemCount"
                                  },
                                  0
                                ]
                              }
                            ]
                          },
                          "then": [
                            {
                              "reject": {
                                "message": "Parties of more than 8 on the Terrace must include a pre-order."
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": true,
                            "rejectMessages": [
                              "Parties of more than 8 on the Terrace must include a pre-order."
                            ],
                            "writes": [],
                            "flags": []
                          }
                        }
                      ],
                      "hooks": [
                        {
                          "name": "booking.create",
                          "description": "Runs before a partner booking is created in the RMS. A rule can reject the booking, turn it into an enquiry (set booking.type / booking.enquiryReason) or flag it.",
                          "triggeredBy": [
                            "POST /venues/{compositeId}/booking"
                          ],
                          "writablePaths": [
                            {
                              "path": "booking.type",
                              "type": "string",
                              "allowedValues": [
                                "book",
                                "request"
                              ],
                              "meaning": "The booking type. Writing `request` makes the booking an enquiry the operator must accept instead of an instant confirmation. Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "allowedValues": [
                                "operatorRule",
                                "none"
                              ],
                              "meaning": "Why the booking is an enquiry. `operatorRule` marks the decision as made by an operator rule (the Collins integration then skips its per-partner enquiry-policy checks). Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "allowedValues": [],
                              "meaning": "Whether the Collins pre-order email is sent. Writing `false` means the email is not sent; the pre-order itself is still posted to Collins. Any non-boolean value makes the rule be skipped. At `booking.amend` nothing is writable, but when the booking is amended after a create that skipped the email, the `booking.create` rules are re-evaluated with the amended booking's facts, once, only to read this path: the email is sent at that amend if it is now `true` (never twice); rejections and other writes are ignored, and `booking.isCreate` is `false` there."
                            }
                          ],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "Copy of `partySize` of the create request body. `children` is not added to it."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "`duration` of the request. If you omit it the platform first substitutes the minimum duration computed from the product's booking rules and only then runs the rules, so the fact is rarely null; send `duration` yourself if a rule reads it."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "Copy of `date` of the request (YYYY-MM-DD). ISO dates compare correctly as strings with > < >= <=."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "Copy of `time` of the request, 24-hour HH:mm, as sent (no time zone conversion)."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(`date` of the request) minus (today's date in Europe/London when the platform processes the request), in whole calendar days; the time of day is ignored. Today 2026-10-01 and date 2026-10-04 gives 3; same day 0; past dates negative. Use the Europe/London calendar day, not UTC."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "`type` of the request: `request` stays `request`, anything else reads as `book`. If a fixed `route_to_enquiry` rule applies to your booking and `daysAhead` is below its `leadTimeDays`, the value is `request` even when you sent `book` (evaluate that rule first). Earlier expression rules may also have written it."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "`operatorRule` when a fixed `route_to_enquiry` rule has turned your booking into an enquiry (evaluate it first), otherwise `none`. The create request has no field for it."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "True unless a fixed `preorder_email` rule (party size below its `threshold`) or an earlier expression rule set `booking.sendPreorderEmail = false`. The create request has no field for it."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always true on booking.create."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "true when you send the request header `X-Is-Cross-Sale: true`, otherwise false (never null on create)."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "Copy of `commission` of the request; null when you do not send it."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "Copy of `depositAmount` of the create request; null when you do not send it."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when `notes` of the request is present and not blank (not only whitespace), otherwise false."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when `company` of the request is present and not blank. If you omit `company`, the platform fills in the operator's default company for your partner or channel, if one is configured, BEFORE rules run: the fact can be true although you sent no company."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "One element per line of `preorders.packages[]` (kind `package`) followed by one per line of `preorders.menus[]` (kind `menu`). `slug` is the aggregation slug of the package or menu whose venue-specific `id` you sent (the `slug` field of that package or menu in `GET /venues`); `quantity` is the line's `quantity` (for a menu the menu quantity, not its items). A line whose package or menu is unknown or has no aggregation slug is silently dropped. Empty list when you send no pre-order.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Length of `booking.items` (number of lines, not the sum of quantities)."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `package`."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `menu`."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "Always an empty list on booking.create.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "On create: true when `booking.items` is not empty."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "Your own partner id: `caller.partnerId` in this response (the partner of your credentials, or of `X-Partner-Reference` for agents)."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "Your own partner category ids for the rule's operator: `caller.categoryIdsByOperator[rule.operatorId]` in this response."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The `slug` of the product you book, i.e. of the product identified by the 4-segment `compositeId` in the path, as listed in `GET /venues`. Null if the product has no slug."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The `compositeId` in the path without its last segment: for 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9 it is 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93. Rule venue targets in this API use the same form."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "First segment of the `compositeId` in the path (the `operatorId`)."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "`spaceId` of the request when it resolves to a space of that venue; null when you send no `spaceId` or it cannot be resolved (then every space.* fact is null and a rule comparing them is skipped)."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space identified by `spaceId`; null when there is no resolvable space or the space has none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The display name of the space identified by `spaceId` (the space names returned in the `spaces` of the availability time slots); null when there is no resolvable space."
                            }
                          ]
                        }
                      ]
                    }
                  },
                  "expressionSetAndFlag": {
                    "summary": "expression that writes booking.type / booking.enquiryReason and flags the booking: no error, the booking becomes an enquiry",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "84f1a7c3-6e20-4d9b-a5b8-2c0d7e9f3a61",
                          "operatorId": 17,
                          "name": "Large parties are enquiries",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When booking.partySize >= 12 then set booking.type = \"request\"; set booking.enquiryReason = \"operatorRule\"; flag: large-party",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "hook": "booking.create",
                          "priority": 50,
                          "when": {
                            ">=": [
                              {
                                "var": "booking.partySize"
                              },
                              12
                            ]
                          },
                          "then": [
                            {
                              "set": {
                                "path": "booking.type",
                                "value": "request"
                              }
                            },
                            {
                              "set": {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule"
                              }
                            },
                            {
                              "flag": {
                                "name": "large-party"
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": false,
                            "rejectMessages": [],
                            "writes": [
                              {
                                "path": "booking.type",
                                "value": "request",
                                "isExpression": false
                              },
                              {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule",
                                "isExpression": false
                              }
                            ],
                            "flags": [
                              "large-party"
                            ]
                          }
                        }
                      ],
                      "hooks": [
                        {
                          "name": "booking.create",
                          "description": "Runs before a partner booking is created in the RMS. A rule can reject the booking, turn it into an enquiry (set booking.type / booking.enquiryReason) or flag it.",
                          "triggeredBy": [
                            "POST /venues/{compositeId}/booking"
                          ],
                          "writablePaths": [
                            {
                              "path": "booking.type",
                              "type": "string",
                              "allowedValues": [
                                "book",
                                "request"
                              ],
                              "meaning": "The booking type. Writing `request` makes the booking an enquiry the operator must accept instead of an instant confirmation. Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "allowedValues": [
                                "operatorRule",
                                "none"
                              ],
                              "meaning": "Why the booking is an enquiry. `operatorRule` marks the decision as made by an operator rule (the Collins integration then skips its per-partner enquiry-policy checks). Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "allowedValues": [],
                              "meaning": "Whether the Collins pre-order email is sent. Writing `false` means the email is not sent; the pre-order itself is still posted to Collins. Any non-boolean value makes the rule be skipped. At `booking.amend` nothing is writable, but when the booking is amended after a create that skipped the email, the `booking.create` rules are re-evaluated with the amended booking's facts, once, only to read this path: the email is sent at that amend if it is now `true` (never twice); rejections and other writes are ignored, and `booking.isCreate` is `false` there."
                            }
                          ],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "Copy of `partySize` of the create request body. `children` is not added to it."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "`duration` of the request. If you omit it the platform first substitutes the minimum duration computed from the product's booking rules and only then runs the rules, so the fact is rarely null; send `duration` yourself if a rule reads it."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "Copy of `date` of the request (YYYY-MM-DD). ISO dates compare correctly as strings with > < >= <=."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "Copy of `time` of the request, 24-hour HH:mm, as sent (no time zone conversion)."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(`date` of the request) minus (today's date in Europe/London when the platform processes the request), in whole calendar days; the time of day is ignored. Today 2026-10-01 and date 2026-10-04 gives 3; same day 0; past dates negative. Use the Europe/London calendar day, not UTC."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "`type` of the request: `request` stays `request`, anything else reads as `book`. If a fixed `route_to_enquiry` rule applies to your booking and `daysAhead` is below its `leadTimeDays`, the value is `request` even when you sent `book` (evaluate that rule first). Earlier expression rules may also have written it."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "`operatorRule` when a fixed `route_to_enquiry` rule has turned your booking into an enquiry (evaluate it first), otherwise `none`. The create request has no field for it."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "True unless a fixed `preorder_email` rule (party size below its `threshold`) or an earlier expression rule set `booking.sendPreorderEmail = false`. The create request has no field for it."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always true on booking.create."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "true when you send the request header `X-Is-Cross-Sale: true`, otherwise false (never null on create)."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "Copy of `commission` of the request; null when you do not send it."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "Copy of `depositAmount` of the create request; null when you do not send it."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when `notes` of the request is present and not blank (not only whitespace), otherwise false."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when `company` of the request is present and not blank. If you omit `company`, the platform fills in the operator's default company for your partner or channel, if one is configured, BEFORE rules run: the fact can be true although you sent no company."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "One element per line of `preorders.packages[]` (kind `package`) followed by one per line of `preorders.menus[]` (kind `menu`). `slug` is the aggregation slug of the package or menu whose venue-specific `id` you sent (the `slug` field of that package or menu in `GET /venues`); `quantity` is the line's `quantity` (for a menu the menu quantity, not its items). A line whose package or menu is unknown or has no aggregation slug is silently dropped. Empty list when you send no pre-order.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Length of `booking.items` (number of lines, not the sum of quantities)."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `package`."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `menu`."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "Always an empty list on booking.create.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "On create: true when `booking.items` is not empty."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "Your own partner id: `caller.partnerId` in this response (the partner of your credentials, or of `X-Partner-Reference` for agents)."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "Your own partner category ids for the rule's operator: `caller.categoryIdsByOperator[rule.operatorId]` in this response."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The `slug` of the product you book, i.e. of the product identified by the 4-segment `compositeId` in the path, as listed in `GET /venues`. Null if the product has no slug."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The `compositeId` in the path without its last segment: for 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9 it is 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93. Rule venue targets in this API use the same form."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "First segment of the `compositeId` in the path (the `operatorId`)."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "`spaceId` of the request when it resolves to a space of that venue; null when you send no `spaceId` or it cannot be resolved (then every space.* fact is null and a rule comparing them is skipped)."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space identified by `spaceId`; null when there is no resolvable space or the space has none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The display name of the space identified by `spaceId` (the space names returned in the `spaces` of the availability time slots); null when there is no resolvable space."
                            }
                          ]
                        }
                      ]
                    }
                  },
                  "expressionAmend": {
                    "summary": "expression at the booking.amend hook: reject an amend that removes the premium brunch (BKRUL-N-100)",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "1c6e9b48-f7a3-4025-b8d1-a9e3c5f07d82",
                          "operatorId": 17,
                          "name": "Premium brunch cannot be dropped on amend",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When some(booking.originalItems, item.slug = \"premium-bottomless-brunch\") AND none(booking.items, item.slug = \"premium-bottomless-brunch\") then reject: The Premium Bottomless Brunch cannot be removed from an existing booking.",
                          "whenApplied": [
                            "booking_amend"
                          ],
                          "consequence": "conditional",
                          "errorCode": "BKRUL-N-100",
                          "hook": "booking.amend",
                          "priority": 100,
                          "when": {
                            "and": [
                              {
                                "some": [
                                  {
                                    "var": "booking.originalItems"
                                  },
                                  {
                                    "==": [
                                      {
                                        "var": "slug"
                                      },
                                      "premium-bottomless-brunch"
                                    ]
                                  }
                                ]
                              },
                              {
                                "none": [
                                  {
                                    "var": "booking.items"
                                  },
                                  {
                                    "==": [
                                      {
                                        "var": "slug"
                                      },
                                      "premium-bottomless-brunch"
                                    ]
                                  }
                                ]
                              }
                            ]
                          },
                          "then": [
                            {
                              "reject": {
                                "message": "The Premium Bottomless Brunch cannot be removed from an existing booking."
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": true,
                            "rejectMessages": [
                              "The Premium Bottomless Brunch cannot be removed from an existing booking."
                            ],
                            "writes": [],
                            "flags": []
                          }
                        }
                      ],
                      "hooks": [
                        {
                          "name": "booking.amend",
                          "description": "Runs before a partner booking is amended (PUT or PATCH). A rule can reject the change or flag it; nothing is writable, an amend never un-confirms a booking.",
                          "triggeredBy": [
                            "PUT /venues/bookings/{bookingId}",
                            "PATCH /venues/bookings/{bookingId}"
                          ],
                          "writablePaths": [],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "The party size the booking will have after the amend: `partySize` of the PUT body, or the value after applying the PATCH operations."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "`duration` of the amend request; when absent, the duration stored on the booking; null if neither exists."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "The date the booking will have after the amend (YYYY-MM-DD)."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "The time the booking will have after the amend, 24-hour HH:mm."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(the booking date AFTER the amend) minus (today's date in Europe/London when the platform processes the request), in whole calendar days."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always false on booking.amend."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "Always null on booking.amend."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "`commission` of the amend request; null when absent."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "The deposit amount STORED on the booking before the amend (an amend request cannot change it for this check); null when the booking has none."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when `notes` of the amend request (after PATCH operations) is present and not blank."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when `company` of the amend request (after PATCH operations) is present and not blank."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "The pre-order lines the booking will have AFTER the amend (for PATCH, after applying the operations on /preorders), one element per package or menu line with its aggregation slug (the `slug` in `GET /venues`) and quantity; lines without an aggregation slug are dropped. How a PUT, whose body has no `preorders` field, is evaluated: not verified.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` or `menu`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Quantity of the line."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Length of `booking.items`."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Number of `booking.items` elements of kind `package`."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Number of `booking.items` elements of kind `menu`."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "The pre-order lines currently stored on the booking (before this amend), mapped to kind, aggregation slug and quantity like `booking.items`; lines without a slug are dropped.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` or `menu`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Quantity of the line."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "true when the multiset of (kind, slug, quantity) of `booking.items` differs from that of `booking.originalItems` (order ignored)."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "`caller.partnerId` in this response."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "`caller.categoryIdsByOperator[rule.operatorId]` in this response."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The `slug` of the product of the booking (the product its `compositeId` identifies, as listed in `GET /venues`); null if it has none."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The booking's `compositeId` without its last segment ({operatorId}|{rmsSlug}|{venueId})."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "First segment of the booking's `compositeId`."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "The space STORED on the booking (an amend request does not change it); null when the booking has no resolvable space."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space stored on the booking; null when none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The name of the space stored on the booking; null when none."
                            }
                          ]
                        }
                      ]
                    }
                  },
                  "expressionPayment": {
                    "summary": "expression at the payment hook: the amount payable is the sum of the minimum-spend bands the booking crosses (R8); no error, moment booking_payment",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "d2a4f8b1-9c37-4e60-b1a5-7f3e0c6d8a29",
                          "operatorId": 17,
                          "name": "Final balance is the sum of the minimum-spend bands crossed",
                          "description": "When a booking spans several minimum-spend bands, the amount payable is the sum of those bands.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When payment.bandCount > 0 then set payment.finalBalance = sum(pluck(payment.bands, \"spendValue\"))",
                          "whenApplied": [
                            "booking_payment"
                          ],
                          "consequence": "conditional",
                          "hook": "payment",
                          "priority": 100,
                          "when": {
                            ">": [
                              {
                                "var": "payment.bandCount"
                              },
                              0
                            ]
                          },
                          "then": [
                            {
                              "set": {
                                "path": "payment.finalBalance",
                                "value": {
                                  "sum": {
                                    "pluck": [
                                      {
                                        "var": "payment.bands"
                                      },
                                      "spendValue"
                                    ]
                                  }
                                }
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": false,
                            "rejectMessages": [],
                            "writes": [
                              {
                                "path": "payment.finalBalance",
                                "value": {
                                  "sum": {
                                    "pluck": [
                                      {
                                        "var": "payment.bands"
                                      },
                                      "spendValue"
                                    ]
                                  }
                                },
                                "isExpression": true
                              }
                            ],
                            "flags": []
                          }
                        }
                      ],
                      "hooks": [
                        {
                          "name": "payment",
                          "description": "Runs when the payment amounts of an existing booking are computed (payment options and payment creation). A rule can set payment.finalBalance: the total payable minimum spend, an integer number of pence (GBP), a finite number >= 0 (any other value, negative or not a number, makes the rule be skipped). Read payment.bands (the minimum-spend bands the booked space-and-time spans), payment.bandCount and payment.minimumSpend (today's single start-time value). A deposit option is computed on the final balance. A reject or flag action has no effect here (the platform ignores it); the hook is not run on create or amend.",
                          "triggeredBy": [
                            "GET /venues/bookings/{bookingId}/payment-options",
                            "POST /venues/bookings/{bookingId}/payments"
                          ],
                          "writablePaths": [
                            {
                              "path": "payment.finalBalance",
                              "type": "number",
                              "allowedValues": [],
                              "meaning": "The total payable minimum spend, in integer pence (GBP): a finite number >= 0. Any other value (negative, not a number, above 2147483647) makes the rule be skipped; a value with decimals is rounded to the nearest penny. A deposit option is computed on this final balance."
                            }
                          ],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "Not sent by you at this hook: the party size stored on the booking."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "Not sent by you: the duration in minutes stored on the booking. Payment rules are only evaluated when it is greater than 0."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "Not sent by you: the date stored on the booking (YYYY-MM-DD)."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "Not sent by you: the start time stored on the booking, 24-hour HH:mm."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(the booking date stored on the booking) minus (today's date in Europe/London when the platform computes the amount), in whole calendar days; negative when the date is past."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "Always null at the payment hook (the platform does not pass the booking type)."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "Always null at the payment hook."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "Always null at the payment hook."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always false at the payment hook (the booking already exists)."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "Always null at the payment hook."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "Not sent by you: the commission stored on the booking; null when it has none."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "Not sent by you: the deposit amount stored on the booking; null when it has none."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when the notes stored on the booking are present and not blank."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when the customer company stored on the booking is present and not blank."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "Always an empty list at the payment hook: the platform does not load the pre-order lines for this computation.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Always 0 at the payment hook (`booking.items` is always empty)."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Always 0 at the payment hook (`booking.items` is always empty)."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Always 0 at the payment hook (`booking.items` is always empty)."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "Always an empty list at the payment hook.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "Never present: `booking.items` and `booking.originalItems` are always empty at the payment hook."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "Always false at the payment hook (no pre-order lines are loaded)."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "The id of the partner stored on the booking (for a booking you made, your own partner id, `caller.partnerId`)."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "The ids of the partner categories of the partner stored on the booking (for a booking you made, `caller.categoryIdsByOperator[rule.operatorId]`)."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The aggregation slug of the product stored on the booking (the `slug` of that product in `GET /venues`); null when it has none or the product cannot be found."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The venue stored on the booking, as {operatorId}|{rmsSlug}|{venueId} (the first three segments of the booking's `compositeId`)."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "The venue group (operator) id stored on the booking (first segment of the booking's `compositeId`)."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "The database id of the space stored on the booking; null when the booking has no space."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space stored on the booking (when the space has none of its own, the first zone linked to it); null when none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The name of the space stored on the booking; null when the booking has no space or none can be found."
                            },
                            {
                              "path": "payment.bands",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The minimum-spend bands of the booked space that the booking interval [start, start + duration) overlaps, in chronological order, each band once. Computed from the space's enabled minimum-spend policy (rules and date overrides) at the booking date, start time and duration; empty when the space has no such policy, no rule matches, or the booking has no duration",
                              "howToObtain": "Not sent by the partner: the platform computes it when payment amounts are computed for an existing booking, from the stored booking date, start time and duration and the booked space's enabled minimum-spend policy. Only the bands the booking interval overlaps are listed, in chronological order.",
                              "elementFields": [
                                {
                                  "path": "label",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": true,
                                  "description": "Label of the minimum-spend rule or date override, e.g. 'Evening'; null when it has none",
                                  "howToObtain": "Not sent by the partner: the label the operator gave the minimum-spend rule or date override that defines this band; null when it has none."
                                },
                                {
                                  "path": "from",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Clock time (HH:mm, Europe/London) where the booking enters this band",
                                  "howToObtain": "Not sent by the partner: computed by the platform as the later of the booking start time and the start of the band (24-hour HH:mm)."
                                },
                                {
                                  "path": "to",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Clock time (HH:mm, Europe/London) where the booking last leaves this band; 00:00 means midnight",
                                  "howToObtain": "Not sent by the partner: computed by the platform as the earlier of the booking end (start + duration) and the end of the band (24-hour HH:mm; 00:00 is midnight)."
                                },
                                {
                                  "path": "spendType",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'minimum_spend', 'hire_fee' or 'hire_fee_plus_minimum_spend': what the band's amounts mean",
                                  "howToObtain": "Not sent by the partner: the spend type configured by the operator on the band's minimum-spend rule or date override."
                                },
                                {
                                  "path": "spendValue",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": true,
                                  "description": "Minimum spend of the band in integer pence (GBP); null for a pure hire fee",
                                  "howToObtain": "Not sent by the partner: the minimum spend configured by the operator for the band, in pence; null for a pure hire fee."
                                },
                                {
                                  "path": "hireFeeValue",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": true,
                                  "description": "Hire fee of the band in integer pence (GBP); null for a pure minimum spend",
                                  "howToObtain": "Not sent by the partner: the hire fee configured by the operator for the band, in pence; null for a pure minimum spend."
                                },
                                {
                                  "path": "overlapMinutes",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Minutes of the booking that fall inside this band",
                                  "howToObtain": "Not sent by the partner: computed by the platform as the minutes between `from` and `to` that the booking spends inside this band."
                                }
                              ]
                            },
                            {
                              "path": "payment.bandCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of bands in payment.bands (0 when none apply)",
                              "howToObtain": "Not sent by the partner: the number of entries of `payment.bands`, computed by the platform."
                            },
                            {
                              "path": "payment.minimumSpend",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Minimum spend in integer pence (GBP) that the payment options use today: the first matching entry's spend value at the booking's start time only, ignoring duration; null when the space has no minimum-spend policy or no entry matches the start time",
                              "howToObtain": "Not sent by the partner: the platform looks up the booked space's minimum-spend policy at the stored booking date and start time and takes the first matching entry's spend value (pence). Null when the space has no policy or no entry matches the start time."
                            },
                            {
                              "path": "payment.currency",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Currency of every payment amount, always 'GBP'",
                              "howToObtain": "Constant: always `GBP`; the platform does not read it from anywhere."
                            }
                          ]
                        }
                      ]
                    }
                  },
                  "walkthrough": {
                    "summary": "An operator with just five rules active: the answer used by the end-to-end walkthrough",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {
                          "17": [
                            1
                          ]
                        }
                      },
                      "rules": [
                        {
                          "id": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36",
                          "operatorId": 17,
                          "name": "Premium brunch - whole table, no mixing",
                          "description": "If any of: Premium Bottomless Brunch is chosen, everyone must have that same choice and nothing else can be selected.",
                          "effect": "same_package",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": false,
                            "products": [
                              {
                                "id": 1257,
                                "slug": "bottomless-brunch",
                                "name": "Bottomless Brunch"
                              }
                            ],
                            "items": {
                              "packages": [
                                {
                                  "id": 201,
                                  "slug": "premium-bottomless-brunch",
                                  "name": "Premium Bottomless Brunch"
                                }
                              ],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {
                            "exclusive": true,
                            "wholeGroup": false
                          },
                          "summary": "If any of: Premium Bottomless Brunch is chosen, everyone must have that same choice and nothing else can be selected. — Bottomless Brunch",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "rejects_booking",
                          "errorCode": "BKRUL-N-001"
                        },
                        {
                          "id": "e07a4b19-6c2d-48f3-b5a1-3d9c70f2e864",
                          "operatorId": 17,
                          "name": "Deposit of 10 pounds per person",
                          "description": "A deposit of 10.00 per person must be collected before booking.",
                          "effect": "require_deposit",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "you"
                          },
                          "conditions": [],
                          "params": {
                            "amount": 10,
                            "per": "person"
                          },
                          "message": "Please collect the 10 pound per person deposit and send it as depositAmount.",
                          "summary": "A deposit of £10.00 per person must be collected before booking for selected partners.",
                          "whenApplied": [
                            "booking_create",
                            "booking_amend"
                          ],
                          "consequence": "requires_deposit",
                          "errorCode": "BKRUL-N-006"
                        },
                        {
                          "id": "3f2b8c1e-5a4d-4e7b-9c10-2d6a8e41b7f3",
                          "operatorId": 17,
                          "name": "Terrace over 8 needs a pre-order",
                          "description": "When space.name = Terrace AND booking.partySize > 8 AND booking.itemCount = 0 then reject.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": false,
                            "venues": [
                              {
                                "id": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93",
                                "name": "The Rooftop Kitchen"
                              }
                            ],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When space.name = \"Terrace\" AND booking.partySize > 8 AND booking.itemCount = 0 then reject: Parties of more than 8 on the Terrace must include a pre-order. (applies to venues: The Rooftop Kitchen)",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "errorCode": "BKRUL-N-100",
                          "hook": "booking.create",
                          "priority": 100,
                          "when": {
                            "and": [
                              {
                                "==": [
                                  {
                                    "var": "space.name"
                                  },
                                  "Terrace"
                                ]
                              },
                              {
                                ">": [
                                  {
                                    "var": "booking.partySize"
                                  },
                                  8
                                ]
                              },
                              {
                                "==": [
                                  {
                                    "var": "booking.itemCount"
                                  },
                                  0
                                ]
                              }
                            ]
                          },
                          "then": [
                            {
                              "reject": {
                                "message": "Parties of more than 8 on the Terrace must include a pre-order."
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": true,
                            "rejectMessages": [
                              "Parties of more than 8 on the Terrace must include a pre-order."
                            ],
                            "writes": [],
                            "flags": []
                          }
                        },
                        {
                          "id": "5a9e2c74-d013-4b86-97f5-e8a14c3b60d2",
                          "operatorId": 17,
                          "name": "Travel/Party enquiry inside 7 days",
                          "description": "Bookings by partners in these categories made less than 7 days ahead become enquiries.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "all_partners"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When some(partner.categoryIds, item IN [1, 2]) AND booking.daysAhead < 7 then set booking.type = \"request\"; set booking.enquiryReason = \"operatorRule\"",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "hook": "booking.create",
                          "priority": 100,
                          "when": {
                            "and": [
                              {
                                "some": [
                                  {
                                    "var": "partner.categoryIds"
                                  },
                                  {
                                    "in": [
                                      {
                                        "var": ""
                                      },
                                      [
                                        1,
                                        2
                                      ]
                                    ]
                                  }
                                ]
                              },
                              {
                                "<": [
                                  {
                                    "var": "booking.daysAhead"
                                  },
                                  7
                                ]
                              }
                            ]
                          },
                          "then": [
                            {
                              "set": {
                                "path": "booking.type",
                                "value": "request"
                              }
                            },
                            {
                              "set": {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule"
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": false,
                            "rejectMessages": [],
                            "writes": [
                              {
                                "path": "booking.type",
                                "value": "request",
                                "isExpression": false
                              },
                              {
                                "path": "booking.enquiryReason",
                                "value": "operatorRule",
                                "isExpression": false
                              }
                            ],
                            "flags": []
                          }
                        },
                        {
                          "id": "b8d3f6a0-1e57-4c92-8a4b-6f0e2d9c5173",
                          "operatorId": 17,
                          "name": "Brunch packages cannot exceed party size",
                          "description": "The brunch packages chosen may not outnumber the guests.",
                          "effect": "expression",
                          "active": true,
                          "appliesTo": {
                            "allVenues": true,
                            "venues": [],
                            "allProducts": true,
                            "products": [],
                            "items": {
                              "packages": [],
                              "menus": []
                            },
                            "partnerScope": "your_categories"
                          },
                          "conditions": [],
                          "params": {},
                          "summary": "When sum(pluck(filter(booking.items, item.slug IN [\"premium-bottomless-brunch\", \"standard-bottomless-brunch\"]), \"quantity\")) > booking.partySize then reject: You cannot select more brunch packages than guests. (applies to partners: selected)",
                          "whenApplied": [
                            "booking_create"
                          ],
                          "consequence": "conditional",
                          "errorCode": "BKRUL-N-100",
                          "hook": "booking.create",
                          "priority": 100,
                          "when": {
                            ">": [
                              {
                                "sum": {
                                  "pluck": [
                                    {
                                      "filter": [
                                        {
                                          "var": "booking.items"
                                        },
                                        {
                                          "in": [
                                            {
                                              "var": "slug"
                                            },
                                            [
                                              "premium-bottomless-brunch",
                                              "standard-bottomless-brunch"
                                            ]
                                          ]
                                        }
                                      ]
                                    },
                                    "quantity"
                                  ]
                                }
                              },
                              {
                                "var": "booking.partySize"
                              }
                            ]
                          },
                          "then": [
                            {
                              "reject": {
                                "message": "You cannot select more brunch packages than guests."
                              }
                            }
                          ],
                          "outcomes": {
                            "rejects": true,
                            "rejectMessages": [
                              "You cannot select more brunch packages than guests."
                            ],
                            "writes": [],
                            "flags": []
                          }
                        }
                      ],
                      "hooks": [
                        {
                          "name": "booking.create",
                          "description": "Runs before a partner booking is created in the RMS. A rule can reject the booking, turn it into an enquiry (set booking.type / booking.enquiryReason) or flag it.",
                          "triggeredBy": [
                            "POST /venues/{compositeId}/booking"
                          ],
                          "writablePaths": [
                            {
                              "path": "booking.type",
                              "type": "string",
                              "allowedValues": [
                                "book",
                                "request"
                              ],
                              "meaning": "The booking type. Writing `request` makes the booking an enquiry the operator must accept instead of an instant confirmation. Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "allowedValues": [
                                "operatorRule",
                                "none"
                              ],
                              "meaning": "Why the booking is an enquiry. `operatorRule` marks the decision as made by an operator rule (the Collins integration then skips its per-partner enquiry-policy checks). Any other value makes the rule be skipped."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "allowedValues": [],
                              "meaning": "Whether the Collins pre-order email is sent. Writing `false` means the email is not sent; the pre-order itself is still posted to Collins. Any non-boolean value makes the rule be skipped. At `booking.amend` nothing is writable, but when the booking is amended after a create that skipped the email, the `booking.create` rules are re-evaluated with the amended booking's facts, once, only to read this path: the email is sent at that amend if it is now `true` (never twice); rejections and other writes are ignored, and `booking.isCreate` is `false` there."
                            }
                          ],
                          "facts": [
                            {
                              "path": "booking.partySize",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of guests on the booking (the new value on an amend)",
                              "howToObtain": "Copy of `partySize` of the create request body. `children` is not added to it."
                            },
                            {
                              "path": "booking.durationMinutes",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Booking duration in minutes; null when none was given and none could be derived",
                              "howToObtain": "`duration` of the request. If you omit it the platform first substitutes the minimum duration computed from the product's booking rules and only then runs the rules, so the fact is rarely null; send `duration` yourself if a rule reads it."
                            },
                            {
                              "path": "booking.date",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking date, ISO yyyy-MM-dd",
                              "howToObtain": "Copy of `date` of the request (YYYY-MM-DD). ISO dates compare correctly as strings with > < >= <=."
                            },
                            {
                              "path": "booking.time",
                              "type": "string",
                              "isList": false,
                              "nullable": false,
                              "description": "Booking start time, HH:mm (Europe/London local time)",
                              "howToObtain": "Copy of `time` of the request, 24-hour HH:mm, as sent (no time zone conversion)."
                            },
                            {
                              "path": "booking.daysAhead",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Whole calendar days from today (Europe/London) to the booking date; negative when the date is past",
                              "howToObtain": "(`date` of the request) minus (today's date in Europe/London when the platform processes the request), in whole calendar days; the time of day is ignored. Today 2026-10-01 and date 2026-10-04 gives 3; same day 0; past dates negative. Use the Europe/London calendar day, not UTC."
                            },
                            {
                              "path": "booking.type",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "'book' (instant confirmation) or 'request' (enquiry) on create, after any fixed route_to_enquiry rule; null on amend",
                              "howToObtain": "`type` of the request: `request` stays `request`, anything else reads as `book`. If a fixed `route_to_enquiry` rule applies to your booking and `daysAhead` is below its `leadTimeDays`, the value is `request` even when you sent `book` (evaluate that rule first). Earlier expression rules may also have written it."
                            },
                            {
                              "path": "booking.enquiryReason",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Why the booking is an enquiry: 'operatorRule' or 'none' on create; null on amend",
                              "howToObtain": "`operatorRule` when a fixed `route_to_enquiry` rule has turned your booking into an enquiry (evaluate it first), otherwise `none`. The create request has no field for it."
                            },
                            {
                              "path": "booking.sendPreorderEmail",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the Collins pre-order email will be sent: true by default, false once a rule wrote it; null on amend",
                              "howToObtain": "True unless a fixed `preorder_email` rule (party size below its `threshold`) or an earlier expression rule set `booking.sendPreorderEmail = false`. The create request has no field for it."
                            },
                            {
                              "path": "booking.isCreate",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking is being created, false on an amend (PUT/PATCH)",
                              "howToObtain": "Always true on booking.create."
                            },
                            {
                              "path": "booking.isCrossSale",
                              "type": "bool",
                              "isList": false,
                              "nullable": true,
                              "description": "Whether the booking is a cross-sale; null on amend (not carried by the amend request)",
                              "howToObtain": "true when you send the request header `X-Is-Cross-Sale: true`, otherwise false (never null on create)."
                            },
                            {
                              "path": "booking.commission",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Partner commission as submitted (same units as the request); null when absent",
                              "howToObtain": "Copy of `commission` of the request; null when you do not send it."
                            },
                            {
                              "path": "booking.depositAmount",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Deposit already collected: the create request's depositAmount, or the stored amount on amend; null when absent",
                              "howToObtain": "Copy of `depositAmount` of the create request; null when you do not send it."
                            },
                            {
                              "path": "booking.hasNotes",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries non-blank notes",
                              "howToObtain": "true when `notes` of the request is present and not blank (not only whitespace), otherwise false."
                            },
                            {
                              "path": "booking.hasCustomerCompany",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the booking carries a non-blank customer company",
                              "howToObtain": "true when `company` of the request is present and not blank. If you omit `company`, the platform fills in the operator's default company for your partner or channel, if one is configured, BEFORE rules run: the fact can be true although you sent no company."
                            },
                            {
                              "path": "booking.items",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "Selected pre-order lines (final state on amend): kind 'package' or 'menu', aggregation slug, quantity",
                              "howToObtain": "One element per line of `preorders.packages[]` (kind `package`) followed by one per line of `preorders.menus[]` (kind `menu`). `slug` is the aggregation slug of the package or menu whose venue-specific `id` you sent (the `slug` field of that package or menu in `GET /venues`); `quantity` is the line's `quantity` (for a menu the menu quantity, not its items). A line whose package or menu is unknown or has no aggregation slug is silently dropped. Empty list when you send no pre-order.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.itemCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected pre-order lines (not the sum of quantities)",
                              "howToObtain": "Length of `booking.items` (number of lines, not the sum of quantities)."
                            },
                            {
                              "path": "booking.packageCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected package lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `package`."
                            },
                            {
                              "path": "booking.menuCount",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Number of selected menu lines",
                              "howToObtain": "Number of elements of `booking.items` whose kind is `menu`."
                            },
                            {
                              "path": "booking.originalItems",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "description": "The stored pre-order lines before this amend; empty on create",
                              "howToObtain": "Always an empty list on booking.create.",
                              "elementFields": [
                                {
                                  "path": "kind",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "'package' or 'menu'",
                                  "howToObtain": "`package` for a line of `preorders.packages`, `menu` for a line of `preorders.menus`."
                                },
                                {
                                  "path": "slug",
                                  "type": "string",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Aggregation slug of the package or menu",
                                  "howToObtain": "The `slug` of the package or menu in `GET /venues`."
                                },
                                {
                                  "path": "quantity",
                                  "type": "number",
                                  "isList": false,
                                  "nullable": false,
                                  "description": "Quantity selected",
                                  "howToObtain": "`quantity` of the pre-order line."
                                }
                              ]
                            },
                            {
                              "path": "booking.preorderChanged",
                              "type": "bool",
                              "isList": false,
                              "nullable": false,
                              "description": "True when the pre-order differs from the stored one (amend), or when any item is present (create)",
                              "howToObtain": "On create: true when `booking.items` is not empty."
                            },
                            {
                              "path": "partner.id",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the booking partner",
                              "howToObtain": "Your own partner id: `caller.partnerId` in this response (the partner of your credentials, or of `X-Partner-Reference` for agents)."
                            },
                            {
                              "path": "partner.categoryIds",
                              "type": "list",
                              "isList": true,
                              "nullable": false,
                              "elementType": "number",
                              "description": "Ids of the partner categories the partner belongs to",
                              "howToObtain": "Your own partner category ids for the rule's operator: `caller.categoryIdsByOperator[rule.operatorId]` in this response."
                            },
                            {
                              "path": "product.slug",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Aggregation slug of the booked product; null when the product has none",
                              "howToObtain": "The `slug` of the product you book, i.e. of the product identified by the 4-segment `compositeId` in the path, as listed in `GET /venues`. Null if the product has no slug."
                            },
                            {
                              "path": "venue.id",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Composite id of the venue: {operatorId}|{rmsSlug}|{venueId} (the stable venue identity; a raw venue id never matches)",
                              "howToObtain": "The `compositeId` in the path without its last segment: for 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9 it is 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93. Rule venue targets in this API use the same form."
                            },
                            {
                              "path": "venue.groupId",
                              "type": "number",
                              "isList": false,
                              "nullable": false,
                              "description": "Id of the venue group (operator)",
                              "howToObtain": "First segment of the `compositeId` in the path (the `operatorId`)."
                            },
                            {
                              "path": "space.id",
                              "type": "number",
                              "isList": false,
                              "nullable": true,
                              "description": "Database id of the booked space (unstable under Zonal sync; prefer tmsZoneId + venue.id)",
                              "howToObtain": "`spaceId` of the request when it resolves to a space of that venue; null when you send no `spaceId` or it cannot be resolved (then every space.* fact is null and a rule comparing them is skipped)."
                            },
                            {
                              "path": "space.tmsZoneId",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "TMS zone id of the booked space (stable identity together with venue.id)",
                              "howToObtain": "The TMS zone id of the space identified by `spaceId`; null when there is no resolvable space or the space has none."
                            },
                            {
                              "path": "space.name",
                              "type": "string",
                              "isList": false,
                              "nullable": true,
                              "description": "Display name of the booked space, e.g. 'Terrace'",
                              "howToObtain": "The display name of the space identified by `spaceId` (the space names returned in the `spaces` of the availability time slots); null when there is no resolvable space."
                            }
                          ]
                        }
                      ]
                    }
                  },
                  "noRules": {
                    "summary": "No rule applies to the caller (or none passes the filters): rules and hooks are empty arrays",
                    "value": {
                      "generatedAt": "2026-10-01T09:30:12Z",
                      "caller": {
                        "partnerId": 482,
                        "partnerCategoryIds": [
                          1
                        ],
                        "categoryIdsByOperator": {}
                      },
                      "rules": [],
                      "hooks": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request was refused before any rule was read. The cause is in `detail`: an unknown `hook`, an `operatorId` below 1, a `venueId` that is not a composite venue id, a `venueId` that does not belong to the given `operatorId`, or a query value of the wrong type (for example `effect=foo`). Every refusal carries a catalog `code`: `BKRUL-N-112` (unknown `hook`), `BKRUL-N-113` (`venueId` of another operator than `operatorId`) or `VALID-N-001` (an `operatorId` below 1, a `venueId` that is not a composite id, or a query value of the wrong type; these carry an `errors` dictionary naming the parameter). Not retryable: fix the request.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientError"
                },
                "examples": {
                  "unknownHook": {
                    "summary": "hook is not a hook the platform knows (GET /booking-rules?hook=booking.cancel)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
                      "title": "Unknown Booking Hook",
                      "status": 400,
                      "detail": "Unknown hook 'booking.cancel'. Known hooks: booking.create, booking.amend, payment.",
                      "code": "BKRUL-N-112",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000001"
                    }
                  },
                  "invalidOperatorId": {
                    "summary": "operatorId is below 1 (GET /booking-rules?operatorId=0)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
                      "title": "Validation Failed",
                      "status": 400,
                      "detail": "operatorId must be a positive integer.",
                      "code": "VALID-N-001",
                      "isRetryable": false,
                      "errors": {
                        "operatorId": [
                          "operatorId must be a positive integer."
                        ]
                      },
                      "traceId": "0HNJ2BG2TU3BU:00000001"
                    }
                  },
                  "malformedVenueId": {
                    "summary": "venueId is not a composite venue id (GET /booking-rules?venueId=9d3f0b6e7a1c4e58b2c4f1a60d7e8b93)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
                      "title": "Validation Failed",
                      "status": 400,
                      "detail": "venueId is not a valid composite venue id ({operatorId}|{rmsSlug}|{venueResourceId}).",
                      "code": "VALID-N-001",
                      "isRetryable": false,
                      "errors": {
                        "venueId": [
                          "venueId is not a valid composite venue id ({operatorId}|{rmsSlug}|{venueResourceId})."
                        ]
                      },
                      "traceId": "0HNJ2BG2TU3BU:00000001"
                    }
                  },
                  "operatorVenueMismatch": {
                    "summary": "venueId belongs to another operator than operatorId (operatorId=18 with a venue of operator 17, both linked to you)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
                      "title": "Venue Operator Mismatch",
                      "status": 400,
                      "detail": "venueId does not belong to the operator given in operatorId.",
                      "code": "BKRUL-N-113",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000001"
                    }
                  },
                  "invalidQueryValue": {
                    "summary": "a query value of the wrong type (GET /booking-rules?effect=foo); the wording of the messages is ASP.NET Core's and is not verified",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
                      "title": "Validation Failed",
                      "status": 400,
                      "detail": "One or more validation errors occurred.",
                      "code": "VALID-N-001",
                      "isRetryable": false,
                      "errors": {
                        "effect": [
                          "The value 'foo' is not valid."
                        ]
                      },
                      "traceId": "0HNJ2BG2TU3BU:00000001"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing, expired or invalid. Standard JWT bearer challenge: the response carries a `WWW-Authenticate: Bearer` header; whether a JSON body is also sent is not verified, do not rely on one. Obtain a new token (see the Live security scheme) and retry.",
            "headers": {
              "WWW-Authenticate": {
                "description": "The bearer challenge (standard ASP.NET Core JWT bearer behaviour; exact value not verified).",
                "schema": {
                  "type": "string"
                },
                "example": "Bearer"
              }
            }
          },
          "403": {
            "description": "Authenticated, but not allowed to read these rules. Causes: the token lacks `venue:read` or belongs to an operator (venue group) identity, which has no partner, so there is no \"calling partner\" (standard 403, no body: not verified); `X-Partner-Reference` is unknown (`IDENT-N-002`) or the agent may not act for it (`IDENT-N-003`); `operatorId` is an operator you are not linked to (`OPRTR-N-002`); `venueId` belongs to an operator you are not linked to (`OPRTR-N-002`), or to a venue your partner mapping hides (`PMAP-N-001`). Every refusal of the service carries a catalog `code`. Not retryable.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientError"
                },
                "examples": {
                  "operatorNotLinked": {
                    "summary": "operatorId is an operator you are not linked to (GET /booking-rules?operatorId=99)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
                      "title": "Operator Access Denied",
                      "status": 403,
                      "detail": "The operator is not one of the operators you are linked to.",
                      "code": "OPRTR-N-002",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000002"
                    }
                  },
                  "venueOfUnlinkedOperator": {
                    "summary": "venueId belongs to an operator you are not linked to",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
                      "title": "Operator Access Denied",
                      "status": 403,
                      "detail": "The venue belongs to an operator you are not linked to.",
                      "code": "OPRTR-N-002",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000002"
                    }
                  },
                  "venueNotAvailable": {
                    "summary": "venueId is a venue your partner mapping hides",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
                      "title": "Venue Not Mapped to Partner",
                      "status": 403,
                      "detail": "The venue is not available to you.",
                      "code": "PMAP-N-001",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000002"
                    }
                  },
                  "invalidPartnerReference": {
                    "summary": "X-Partner-Reference does not resolve to a partner (Bookable Agents)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
                      "title": "Invalid Partner Reference",
                      "status": 403,
                      "detail": "Invalid partner reference: acme-travel. The partner reference could not be resolved.",
                      "code": "IDENT-N-002",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000002"
                    }
                  },
                  "impersonationNotAllowed": {
                    "summary": "the agent may not act for that partner reference (Bookable Agents)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.4",
                      "title": "Partner Impersonation Not Allowed",
                      "status": 403,
                      "detail": "Partner 482 is not permitted to impersonate partner reference 'acme-travel'.",
                      "code": "IDENT-N-003",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000002"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`venueId` is well formed and visible to you but no such venue exists (`VENUE-N-001`). The same status with `PMAP-N-004` (\"Partner Not Found\") is returned in the unexpected case that the calling partner itself no longer exists. Not retryable.",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientError"
                },
                "examples": {
                  "venueNotFound": {
                    "summary": "no such venue (GET /booking-rules?venueId=17%7CX9%7C00000000000000000000000000000000)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
                      "title": "Venue Not Found",
                      "status": 404,
                      "detail": "The requested venue could not be found",
                      "code": "VENUE-N-001",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000003"
                    }
                  },
                  "partnerNotFound": {
                    "summary": "the calling partner itself no longer exists (unexpected)",
                    "value": {
                      "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
                      "title": "Partner Not Found",
                      "status": 404,
                      "detail": "The specified partner could not be found",
                      "code": "PMAP-N-004",
                      "isRetryable": false,
                      "traceId": "0HNJ2BG2TU3BU:00000003"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (`RATE-R-001`). Retryable: wait for the number of seconds in `Retry-After` (when present) or until `X-RateLimit-Reset` (Unix time in seconds), then repeat the same request.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (sent when the limiter knows the delay).",
                "schema": {
                  "type": "integer"
                },
                "example": 30
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientError"
                },
                "examples": {
                  "rateLimited": {
                    "summary": "too many requests in the current window",
                    "value": {
                      "title": "Rate Limit Exceeded",
                      "status": 429,
                      "detail": "Too many requests have been made in a short period. Please wait and retry.",
                      "code": "RATE-R-001",
                      "isRetryable": true,
                      "traceId": "11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "default": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "security": [
          {
            "Live": [
              "venue:read"
            ]
          },
          {
            "Sandbox": [
              "venue:read"
            ]
          }
        ]
      }
    }
  },
  "components": {
    "parameters": {
      "CorrelationID": {
        "in": "header",
        "name": "X-Correlation-ID",
        "required": false,
        "description": "An optional ID which you can use to track transactions across multiple systems. It can have any value, but we recommend avoiding `.` characters.\nMirrored back in a response header.\n",
        "schema": {
          "type": "string",
          "example": "11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA"
        }
      },
      "PartnerReference": {
        "in": "header",
        "name": "X-Partner-Reference",
        "required": false,
        "description": "Required for Bookable Agents. Identifies the partner for whom the request is being made.\nThis header must be provided when making requests with the `agent:write` scope.\n\nAccepts either form:\n  * a **partner reference**, e.g. `thefork.com`\n  * a **partner id**, e.g. `288` — the same id returned by `GET /agent/impersonation-targets`\n\nA value matching a known partner reference always resolves as that reference; it is only\ntreated as a partner id when it does not. Most partners have no reference configured, so the\npartner id is generally the form to use — the one exception being a reference spelled as\ndigits, which still takes precedence over the partner whose id is that number.\n",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "PartnerBookingRulesResponse": {
        "type": "object",
        "description": "The active booking condition rules that apply to the calling partner, and the description of every hook that an expression rule in `rules` refers to. Properties whose value is null are omitted; the four properties below are always present.",
        "required": [
          "generatedAt",
          "caller",
          "rules",
          "hooks"
        ],
        "properties": {
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Instant (UTC, ISO 8601) at which the server built this answer. Informational: rules can change after this instant and there is no cache validator, so re-fetch at the start of each booking flow. The booking endpoints always validate against the current rules.",
            "example": "2026-10-01T09:30:12Z"
          },
          "caller": {
            "$ref": "#/components/schemas/PartnerBookingCaller"
          },
          "rules": {
            "type": "array",
            "description": "Active rules that apply to the caller after the query filters; an empty array when none. Ordered by `operatorId`, then fixed effects by precedence (`route_to_enquiry` last), then expression rules by hook (`booking.create` first), priority ascending and rule id: the order the platform evaluates expression rules. Not paginated. Do not re-sort: apply it as returned.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRule"
            }
          },
          "hooks": {
            "type": "array",
            "description": "One entry for every hook named by an expression rule in `rules` (the `payment` hook only when a returned rule is at that hook): its facts, how to obtain each from your request (or, at `payment`, how the platform computes it), and its writable paths. Empty when `rules` holds no expression rule. Look a hook up by `name` (equal to the rule's `hook`) before evaluating an expression rule.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingHook"
            }
          }
        }
      },
      "PartnerBookingCaller": {
        "type": "object",
        "description": "Facts about the calling partner itself (never about other partners): the values of the facts `partner.id` and `partner.categoryIds` that expression rules can read. Copy them into the fact document you build: `partner.id` from `partnerId`, `partner.categoryIds` from `categoryIdsByOperator[rule.operatorId]` of the rule you evaluate.",
        "required": [
          "partnerId",
          "partnerCategoryIds",
          "categoryIdsByOperator"
        ],
        "properties": {
          "partnerId": {
            "type": "integer",
            "format": "int32",
            "description": "Id of the calling partner, resolved from your credentials (or from `X-Partner-Reference` for Bookable Agents). It is the value of the fact `partner.id`.",
            "example": 482
          },
          "partnerCategoryIds": {
            "type": "array",
            "description": "Ids of the partner categories assigned directly to the calling partner, ascending: global, the same for every operator. Kept for backward compatibility. It is NOT the value of the fact `partner.categoryIds` of a rule: operators can also place the partner in a category through their own third-party records, which this field does not include. Use `categoryIdsByOperator[rule.operatorId]` and fall back to this field only when that entry is missing. An empty array when the partner has no direct category.",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "example": [
              1
            ]
          },
          "categoryIdsByOperator": {
            "type": "object",
            "description": "The categories of the calling partner per operator, as the server sees them when it filters the rules and when it evaluates them on a booking: the directly assigned categories plus the categories of the operator's own live third parties linked to the partner. Keys are operator ids as strings (JSON object keys); values are ascending category ids. One entry for every operator that has at least one rule in `rules[]`, so `categoryIdsByOperator[String(rule.operatorId)]` always exists; an empty object when `rules` is empty. It is the value of the fact `partner.categoryIds` for the rules of that operator.",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "integer",
                "format": "int32"
              }
            },
            "example": {
              "17": [
                1,
                5
              ]
            }
          }
        }
      },
      "PartnerBookingRuleEffect": {
        "type": "string",
        "description": "Kind of rule. The eight fixed effects have fixed semantics (see the effects table and \"How to evaluate each fixed\neffect\" in the API description); `expression` is a generic rule defined by data (`hook`, `when`, `then`,\n`priority`). These are the same wire names as `violations[].effect` in a rejected booking.\n- `same_package`: the chosen pre-order lines may not mix items of the rule's set (and, when `exclusive`, nothing else may be chosen). Refuses with `BKRUL-N-001`.\n- `max_quantity_party_size`: the total quantity of the set (empty set = every package and menu) may not exceed the party size. Refuses with `BKRUL-N-002`.\n- `max_quantity`: the total quantity of the set may not exceed `params.max`. Refuses with `BKRUL-N-003`.\n- `preorder_required`: from `params.threshold` guests a qualifying pre-order line is mandatory. Refuses with `BKRUL-N-004`.\n- `route_to_enquiry`: a booking made fewer than `params.leadTimeDays` days ahead becomes an enquiry instead of being confirmed. Never refuses, create only.\n- `lock_preorder_edits`: inside `params.leadTimeDays` days of the booking date pre-orders may not be added or changed. Refuses with `BKRUL-N-005`.\n- `require_deposit`: the booking must already carry a deposit of `params.amount` (per person or per booking). Refuses with `BKRUL-N-006`.\n- `preorder_email`: below `params.threshold` guests the Collins pre-order email is not sent; the pre-order is still posted. Never refuses.\n- `booking_window`: you may not book, amend or cancel (`params.action`) within `params.minHoursBefore` hours of the reservation start, nor book more than `params.maxHoursBefore` hours ahead. Refuses with `AVAIL-N-007` / `AVAIL-N-008` (book), `BKRUL-N-007` (amend) or `BKRUL-N-008` (cancel).\n- `expression`: a rule written as a JSON expression (`when`) with actions (`then`) evaluated at a hook. Refuses with `BKRUL-N-100` when its `then` contains a `reject` (except at the `payment` hook, where a `reject` is ignored: that hook changes the amount payable of an existing booking).",
        "enum": [
          "same_package",
          "max_quantity_party_size",
          "max_quantity",
          "preorder_required",
          "route_to_enquiry",
          "lock_preorder_edits",
          "require_deposit",
          "preorder_email",
          "expression",
          "booking_window"
        ],
        "example": "same_package"
      },
      "PartnerBookingRuleConsequence": {
        "type": "string",
        "description": "What the platform does when the rule is violated or triggers, and so what you should do:\n- `rejects_booking`: used by same_package, max_quantity_party_size, max_quantity, preorder_required, lock_preorder_edits and booking_window. The booking is refused with `errorCode`; change the booking until the condition holds.\n- `turns_booking_into_enquiry`: used by route_to_enquiry. No error: the booking is created as an enquiry (type `request`) that the operator must accept; book further ahead to avoid it.\n- `requires_deposit`: used by require_deposit. The booking is refused with `BKRUL-N-006` unless it already carries the deposit; collect it and send it as `depositAmount`.\n- `skips_preorder_email`: used by preorder_email. No error: below `params.threshold` guests the Collins pre-order email is not sent, the pre-order is still posted.\n- `conditional`: used by expression rules. The effect depends on `then`: read `outcomes` (`rejects`, `writes`, `flags`). At the `payment` hook the rule only writes the amount payable (`payment.finalBalance`) and never refuses.",
        "enum": [
          "rejects_booking",
          "turns_booking_into_enquiry",
          "requires_deposit",
          "conditional",
          "skips_preorder_email"
        ],
        "example": "rejects_booking"
      },
      "PartnerBookingRuleMoment": {
        "type": "string",
        "description": "A moment at which the rule is evaluated:\n- `booking_create`: `POST /venues/{compositeId}/booking`.\n- `booking_amend`: `PUT` or `PATCH` on `/venues/bookings/{bookingId}`.\n- `booking_cancel`: `DELETE /venues/bookings/{bookingId}` (and the cancel of a Listings order line). Only `booking_window` rules with `action` cancel run here.\n- `booking_payment`: when the platform computes the payable amount of an EXISTING booking (payment options, `GET /venues/bookings/{bookingId}/payment-options`, and payment creation, `POST /venues/bookings/{bookingId}/payments`). Expression rules at the hook `payment` can set `payment.finalBalance` there. Not evaluated at booking create or amend, and not shown in availability or `GET /venues`, which keep showing the value of the band at the start time.",
        "enum": [
          "booking_create",
          "booking_amend",
          "booking_payment",
          "booking_cancel"
        ],
        "example": "booking_create"
      },
      "PartnerBookingRule": {
        "type": "object",
        "description": "One active booking condition rule that applies to the calling partner. Inactive rules are never returned.\nFixed effects (every effect except `expression`) are fully described by `effect`, `params`, `appliesTo`\nand the effects table; `hook`, `priority`, `when`, `then` and `outcomes` are omitted for them. Expression rules\nhave an empty `params` and are described by `hook`, `priority`, `when`, `then` and `outcomes`. Properties whose\nvalue is null are omitted from the JSON.",
        "required": [
          "id",
          "operatorId",
          "name",
          "effect",
          "active",
          "appliesTo",
          "conditions",
          "params",
          "summary",
          "whenApplied",
          "consequence"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Rule identifier (UUID). This is the SAME id that appears in `violations[].ruleId` (and in `analyticsRuleId`) of the 400 response when a booking is rejected by this rule: use it to connect a rejection to the rule.",
            "example": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36"
          },
          "operatorId": {
            "type": "integer",
            "format": "int32",
            "description": "Operator (venue group) that owns the rule; the first segment of the composite ids it applies to. Keep the rule only for bookings at venues of this operator.",
            "example": 17
          },
          "name": {
            "type": "string",
            "description": "Name given by the operator, free text. Also reported as `violations[].ruleName` in a rejection. Show it to your user; do not parse it.",
            "example": "Premium brunch - whole table, no mixing"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Free text from the operator, returned as stored (it defaults to a summary the operator tooling generated when the rule was saved). Omitted when the operator wrote none. If that text names a partner or partner category other than yours, or a venue you cannot see, the platform returns `summary` instead, so another partner is never identified. Informational; do not parse.",
            "example": "If any of: Premium Bottomless Brunch is chosen, everyone must have that same choice and nothing else can be selected."
          },
          "effect": {
            "$ref": "#/components/schemas/PartnerBookingRuleEffect"
          },
          "active": {
            "type": "boolean",
            "description": "Always `true`: inactive rules are never returned by this API.",
            "example": true
          },
          "appliesTo": {
            "$ref": "#/components/schemas/PartnerBookingRuleScope"
          },
          "conditions": {
            "type": "array",
            "description": "Extra field conditions. ALL entries must hold for the rule to apply to a booking (empty = no extra gating). Normally empty today: the operator tooling cannot create conditions yet.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRuleCondition"
            }
          },
          "params": {
            "$ref": "#/components/schemas/PartnerBookingRuleParams"
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The text partners receive (`detail` of the error and, for expression rules, the fallback text of a `reject` action without its own message) when the rule rejects a booking. Omitted when the operator set none: the platform then uses the default message of the effect (see the effects table).",
            "example": "Please collect the 10 pound per person deposit and send it as depositAmount."
          },
          "summary": {
            "type": "string",
            "description": "One English sentence generated by the server from the rule's structure. It never names partners or partner categories: when the rule is aimed at some of them it says \"selected partners\" (see `appliesTo.partnerScope` for how it is aimed at you). Informational: the exact wording can change, do not parse it.",
            "example": "A deposit of £10.00 per person must be collected before booking for selected partners."
          },
          "whenApplied": {
            "type": "array",
            "description": "Moments at which the rule is evaluated; keep the rule only if it contains the operation you are about to call. Fixed effects: `booking_create` and `booking_amend`, except `route_to_enquiry`, which is `booking_create` only (an amend never un-confirms a booking). Expression rules: `booking_create` when `hook` is `booking.create`, `booking_amend` when it is `booking.amend`, `booking_payment` when it is `payment` (the moment the platform computes the amount payable of an existing booking; not a request you send and never a gate for your create or amend).",
            "minItems": 1,
            "uniqueItems": true,
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRuleMoment"
            },
            "example": [
              "booking_create",
              "booking_amend"
            ]
          },
          "consequence": {
            "$ref": "#/components/schemas/PartnerBookingRuleConsequence"
          },
          "errorCode": {
            "type": "string",
            "nullable": true,
            "description": "Error catalog code the booking endpoint returns when this rule rejects: `BKRUL-N-001` same_package, `BKRUL-N-002` max_quantity_party_size, `BKRUL-N-003` max_quantity, `BKRUL-N-004` preorder_required, `BKRUL-N-005` lock_preorder_edits, `BKRUL-N-006` require_deposit, for booking_window `AVAIL-N-007` (action book with a minimum; a book window with both bounds answers `AVAIL-N-008` when the booking is too far ahead), `AVAIL-N-008` (action book with only a maximum), `BKRUL-N-007` (amend) and `BKRUL-N-008` (cancel), `BKRUL-N-100` expression (every rejecting expression rule, with the message of its `reject`). Omitted for `route_to_enquiry` (no error), and omitted for an expression rule whose `outcomes.rejects` is false (always the case for a rule at the `payment` hook, where a `reject` action has no effect).",
            "pattern": "^(BKRUL-N-(00[1-8]|100)|AVAIL-N-00[78])$",
            "example": "BKRUL-N-001"
          },
          "hook": {
            "type": "string",
            "nullable": true,
            "description": "Expression rules only: the hook (named point of the flow) where the rule runs: `booking.create`, `booking.amend` or `payment`. The matching description (facts, how to obtain them, writable paths) is the entry of `hooks[]` with the same `name`. Omitted for fixed effects.",
            "example": "booking.create"
          },
          "priority": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "description": "Expression rules only: evaluation order within the hook, ascending (ties by rule id); the platform default is 100. A `set` written by a lower number is visible to the rules evaluated after it. Omitted for fixed effects.",
            "example": 100
          },
          "when": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true,
            "description": "Expression rules only (omitted for fixed effects): the condition, one boolean expression tree in JsonLogic\nformat, exactly as the platform evaluates it. The rule applies when it evaluates to `true`. Read facts\nwith `{\"var\": \"<path>\"}`; build the fact document from `hooks[].facts[].howToObtain` and evaluate with the\noperator reference in the API description (comparison, logic, arithmetic, `in`, list predicates\n`filter` / `map` / `some` / `all` / `none` where `var` is the element, aggregates `count` / `sum` /\n`distinct` / `pluck`; limits depth 12 and 96 nodes). A fact that is missing or of the wrong type for an\noperator makes the rule NOT apply (skipped), it never fails the booking.\nExample: `{\"and\": [{\"==\": [{\"var\": \"space.name\"}, \"Terrace\"]}, {\">\": [{\"var\": \"booking.partySize\"}, 8]}, {\"==\": [{\"var\": \"booking.itemCount\"}, 0]}]}`.",
            "example": {
              "and": [
                {
                  "==": [
                    {
                      "var": "space.name"
                    },
                    "Terrace"
                  ]
                },
                {
                  ">": [
                    {
                      "var": "booking.partySize"
                    },
                    8
                  ]
                },
                {
                  "==": [
                    {
                      "var": "booking.itemCount"
                    },
                    0
                  ]
                }
              ]
            }
          },
          "then": {
            "type": "array",
            "nullable": true,
            "maxItems": 8,
            "description": "Expression rules only (omitted for fixed effects): the actions applied when `when` is true, in order. Each\naction is an object with exactly one key: `{\"set\": {\"path\": \"booking.type\", \"value\": \"request\"}}` (writes a\nliteral or an expression to a path that is writable at the hook; several `set`s on a path: the last wins),\n`{\"reject\": {\"message\": \"...\"}}` (refuses the booking with `BKRUL-N-100`; the message is optional; ignored at the\n`payment` hook) or `{\"flag\": {\"name\": \"vip\"}}` (a named annotation with no behaviour). `outcomes` gives the same information\nalready parsed.",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "example": [
              {
                "reject": {
                  "message": "Parties of more than 8 on the Terrace must include a pre-order."
                }
              }
            ]
          },
          "outcomes": {
            "$ref": "#/components/schemas/PartnerBookingRuleOutcomes"
          }
        }
      },
      "PartnerBookingRuleScope": {
        "type": "object",
        "description": "Where and for whom the rule applies. Dimensions combine with AND: the rule applies to a booking at a venue\nin `venues` (or any, if `allVenues`), for a product in `products` (or any, if `allProducts`), by a partner\nmatching `partnerScope`. `items` is the set the effect operates on (it is not a scope test). Names are the\ncurrent catalog names. A target that no longer exists in the operator's catalog is not listed (the platform\nignores it too); a rule whose venue targets have all disappeared, or none of which you can see, is not returned.",
        "required": [
          "allVenues",
          "venues",
          "allProducts",
          "products",
          "items",
          "partnerScope"
        ],
        "properties": {
          "allVenues": {
            "type": "boolean",
            "description": "true when the rule has no venue target: it applies to every venue of the operator (`venues` is then empty). false means the rule is restricted to the venues in `venues[]`, which are only the ones you can see: only these of YOUR venues.",
            "example": false
          },
          "venues": {
            "type": "array",
            "description": "Venues the rule is narrowed to when `allVenues` is false. The rule applies only to bookings at one of them: compare each `id` with the first three segments of the composite id you book. Only venues you can see are listed (same visibility as `GET /venues`); a rule restricted to venues none of which you can see is not returned, so this list is never empty when `allVenues` is false. Empty when `allVenues` is true.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRuleVenue"
            }
          },
          "allProducts": {
            "type": "boolean",
            "description": "true when the rule has no product target: it applies to every product (booking type) (`products` is then empty). false means the rule applies only to the products in `products`; such a rule never applies to a product that has no aggregation slug.",
            "example": false
          },
          "products": {
            "type": "array",
            "description": "Products (booking types) the rule is limited to when `allProducts` is false; empty when `allProducts` is true. Match the `slug` with the `slug` of the product you book in `GET /venues` (the product of the 4-segment `compositeId`).",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRuleCatalogItem"
            }
          },
          "items": {
            "$ref": "#/components/schemas/PartnerBookingRuleItems"
          },
          "partnerScope": {
            "type": "string",
            "description": "How the rule is aimed at the calling partner. Only these three values exist: rules aimed only at other\npartners are never returned and no other partner is ever identified.\n- `all_partners`: the rule has no partner target; it applies to every partner.\n- `you`: the rule names your partner id.\n- `your_categories`: the rule names a partner category you belong to (and does not name your partner id).\nIf a rule names both your partner id and one of your categories the value is `you`. You never need to filter on it: the rule is already known to concern you.",
            "enum": [
              "all_partners",
              "you",
              "your_categories"
            ],
            "example": "all_partners"
          }
        }
      },
      "PartnerBookingRuleVenue": {
        "type": "object",
        "description": "A venue a rule is restricted to, limited to venues the calling partner can see.",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Composite venue id `{operatorId}|{rmsSlug}|{venueResourceId}` (3 segments, as returned by `GET /venues`). A booking is at this venue when the first three segments of its 4-segment `compositeId` equal this id.",
            "example": "17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Current display name of the venue (informational; may be null when the venue has no name).",
            "example": "The Rooftop Kitchen"
          }
        }
      },
      "PartnerBookingRuleCatalogItem": {
        "type": "object",
        "description": "A product, package or menu a rule targets, identified at the aggregation level of the operator's catalog (one aggregation item groups the equivalent products, packages or menus of the operator's venues).",
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32",
            "description": "Aggregation-level id of the product, package or menu (operator side id). Informational: do NOT compare it with the venue-specific ids you send in a booking; match on `slug`.",
            "example": 1257
          },
          "slug": {
            "type": "string",
            "nullable": true,
            "description": "Aggregation slug: the identifier rules use. It is the `slug` of the product, package or menu in `GET /venues`, and the `slug` of the elements of the fact `booking.items`. Omitted when the item has none; such an item can never match a booking line.",
            "example": "bottomless-brunch"
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Current name of the catalog item (informational; may be null).",
            "example": "Bottomless Brunch"
          }
        }
      },
      "PartnerBookingRuleItems": {
        "type": "object",
        "description": "The set S of packages and menus (compared by slug) that the effect works on. How it is used depends on the effect:\n`same_package`: the set whose members may not be mixed / that triggers exclusivity (needs at least one item);\n`max_quantity_party_size`: the set whose total quantity is counted (EMPTY means every package and menu counts);\n`max_quantity`: the set whose total quantity is counted;\n`preorder_required`: the pre-order lines that qualify (EMPTY means any package or menu qualifies);\n`route_to_enquiry`, `lock_preorder_edits`, `require_deposit`: not used (any items listed have no effect);\n`expression`: informational only, never evaluated.",
        "required": [
          "packages",
          "menus"
        ],
        "properties": {
          "packages": {
            "type": "array",
            "description": "Packages in S. A chosen package line is in S when its aggregation slug equals one of these `slug`s. Empty when the set has no package.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRuleCatalogItem"
            }
          },
          "menus": {
            "type": "array",
            "description": "Menus in S. A chosen menu line is in S when its aggregation slug equals one of these `slug`s. Empty when the set has no menu.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRuleCatalogItem"
            }
          }
        }
      },
      "PartnerBookingRuleCondition": {
        "type": "object",
        "description": "One extra field condition. It compares a field of the booking request with `value`; a field the booking does not carry never matches, whatever the operator. All conditions of a rule must hold for the rule to apply.",
        "required": [
          "field",
          "operator",
          "value"
        ],
        "properties": {
          "field": {
            "type": "string",
            "description": "Request field. On create: `partySize`, `duration`, `spaceId`, `isCrossSale`, `commission`, `depositAmount`, `hasCustomerCompany` (boolean: a non-blank company), `hasNotes` (boolean: non-blank notes). On amend only `partySize`, `duration`, `commission`, `hasCustomerCompany`, `hasNotes`.",
            "example": "partySize"
          },
          "operator": {
            "type": "string",
            "description": "How `field` is compared with `value`:\n- `equals`: same value (numbers compare by value, so 8 equals 8.0).\n- `not_equals`: different value (still false when the booking does not carry the field).\n- `gte`: the field is a number greater than or equal to `value` (a non-number never matches).\n- `lte`: the field is a number less than or equal to `value` (a non-number never matches).\n- `in`: the field value is one of the values of the array `value`.\n- `contains`: the field value is a list that contains `value`.",
            "enum": [
              "equals",
              "not_equals",
              "gte",
              "lte",
              "in",
              "contains"
            ],
            "example": "gte"
          },
          "value": {
            "description": "The value to compare with, any JSON value (an array for `in`). Its kind must suit the operator.",
            "example": 8
          }
        }
      },
      "PartnerBookingRuleParams": {
        "type": "object",
        "description": "Parameters of a fixed effect; only the keys of the rule's effect are present, the others are omitted.\nDefaults are already applied. Expression rules (and `max_quantity_party_size` without `perItem`) have an empty object.",
        "properties": {
          "exclusive": {
            "type": "boolean",
            "nullable": true,
            "description": "same_package only. `true` (the default): once any set item is chosen, every selected package/menu must be that same single item - a second distinct set item or any chosen line outside the set is a violation. `false`: only the set items are compared with each other (two distinct set items is a violation); items outside the set are ignored.",
            "example": true
          },
          "wholeGroup": {
            "type": "boolean",
            "nullable": true,
            "description": "same_package only. Default `false`. When `true`, the summed quantity of the chosen set lines must equal the party size, otherwise it is a violation (same code `BKRUL-N-001`).",
            "example": false
          },
          "max": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 1,
            "description": "max_quantity only. Maximum total quantity of the set per booking (inclusive: exactly `max` is allowed).",
            "example": 4
          },
          "perItem": {
            "type": "boolean",
            "nullable": true,
            "description": "max_quantity and max_quantity_party_size only. Present (and `true`) only when set: each item of the set is capped on its own (its lines added together by slug) instead of the set's total. Absent = the total is capped.",
            "example": true
          },
          "threshold": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 1,
            "description": "preorder_required and preorder_email only. preorder_required - party size from which a pre-order is required, inclusive (party size >= threshold), 1 means every booking. preorder_email - party size from which the Collins pre-order email is sent, inclusive.",
            "example": 8
          },
          "leadTimeDays": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0,
            "description": "route_to_enquiry and lock_preorder_edits only. Window in whole calendar days, counted in Europe/London: the window is `daysAhead < leadTimeDays` with `daysAhead` = booking date minus today. A booking exactly `leadTimeDays` ahead is NOT inside the window.",
            "example": 7
          },
          "amount": {
            "type": "number",
            "format": "double",
            "nullable": true,
            "minimum": 0.01,
            "description": "require_deposit only. Deposit amount in pounds sterling (GBP), per person or per booking according to `per`. Send the collected deposit as `depositAmount` in the same currency.",
            "example": 10
          },
          "per": {
            "type": "string",
            "nullable": true,
            "enum": [
              "person",
              "booking"
            ],
            "description": "require_deposit only. What `amount` is measured against:\n- `person` (the default): the required deposit is `amount` x party size.\n- `booking`: the required deposit is `amount`, flat, whatever the party size.",
            "example": "person"
          },
          "action": {
            "type": "string",
            "nullable": true,
            "enum": [
              "book",
              "amend",
              "cancel"
            ],
            "description": "booking_window only. The operation the window limits:\n- `book`: `POST /venues/{compositeId}/booking`, and an amend that changes the date or time (checked against the new start).\n- `amend`: `PUT` / `PATCH /venues/bookings/{bookingId}` on a confirmed booking (checked against the stored start).\n- `cancel`: `DELETE /venues/bookings/{bookingId}` on a confirmed booking.",
            "example": "cancel"
          },
          "minHoursBefore": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 1,
            "description": "booking_window only. The operation is refused when the reservation starts in fewer than this many hours (real elapsed time from your request to booking date + start time, Europe/London). Exactly this many hours away is allowed.",
            "example": 120
          },
          "maxHoursBefore": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 1,
            "description": "booking_window with action book only. A booking is refused when the reservation starts more than this many hours away.",
            "example": 2400
          }
        }
      },
      "PartnerBookingRuleOutcomes": {
        "type": "object",
        "nullable": true,
        "description": "Expression rules only (omitted for fixed effects). What the rule's `then` does when its `when` is true, derived from `then`, so that you need not read `then`. If several rules write the same path the last one (priority, then rule id) wins; the final value is not computed here.",
        "required": [
          "rejects",
          "rejectMessages",
          "writes",
          "flags"
        ],
        "properties": {
          "rejects": {
            "type": "boolean",
            "description": "true when `then` contains at least one `reject` action: the booking is refused with `BKRUL-N-100` (when `when` is true). When false the rule never refuses. Always false for a rule at the `payment` hook, where the platform ignores a `reject` action.",
            "example": true
          },
          "rejectMessages": {
            "type": "array",
            "description": "The message of each `reject` action (the action's own message, else the rule's `message`, else \"Rule '<name>' rejected the booking.\"). Empty when `rejects` is false (always, at the `payment` hook). The first one is the `detail` of the 400.",
            "items": {
              "type": "string"
            },
            "example": [
              "Parties of more than 8 on the Terrace must include a pre-order."
            ]
          },
          "writes": {
            "type": "array",
            "description": "The `set` actions, in order: what the platform writes when `when` is true. `booking.type = request` makes the booking an enquiry; `payment.finalBalance` (hook `payment`) sets the total payable in integer pence. Empty when there are none.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingRuleWrite"
            }
          },
          "flags": {
            "type": "array",
            "description": "Names of the `flag` actions. A flag has no behaviour of its own; it is an annotation. Empty when there are none.",
            "items": {
              "type": "string"
            },
            "example": [
              "large-party"
            ]
          }
        }
      },
      "PartnerBookingRuleWrite": {
        "type": "object",
        "description": "One `set` action of an expression rule, as the platform will carry it out when `when` is true.",
        "required": [
          "path",
          "value",
          "isExpression"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "The path written. At `booking.create`: `booking.type`, `booking.enquiryReason` or `booking.sendPreorderEmail`. At `booking.amend` no path is writable (a rule that sets one is skipped). At `payment`: `payment.finalBalance` only.",
            "example": "booking.type"
          },
          "value": {
            "description": "The value written, as JSON. When `isExpression` is false it is the literal itself (for `booking.type` `book` or `request`, for `booking.enquiryReason` `operatorRule` or `none`, for `booking.sendPreorderEmail` `true` or `false`, for `payment.finalBalance` an integer number of pence). When true it is the expression tree to evaluate against the facts, like `when` (for `payment.finalBalance`, for example the sum of the spend values of `payment.bands`).",
            "example": "request"
          },
          "isExpression": {
            "type": "boolean",
            "description": "true when `value` is an expression tree to be evaluated rather than a literal. When false, `value` is final.",
            "example": false
          }
        }
      },
      "PartnerBookingHook": {
        "type": "object",
        "description": "A named point of the booking flow where expression rules run, with the facts they can read and the paths they\ncan write. Build the fact document of the hook by following `facts[].howToObtain`; the document is a JSON\nobject whose top-level keys are `booking`, `partner`, `product`, `venue` and `space`, plus `payment` at the `payment`\nhook (`{\"booking\": {\"partySize\": 10, ...}, \"space\": {\"name\": \"Terrace\", ...}, ...}`); a fact path such as\n`booking.partySize` is `document.booking.partySize`. A null fact makes any rule that compares it with a value of\nanother kind be skipped.",
        "required": [
          "name",
          "description",
          "triggeredBy",
          "writablePaths",
          "facts"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The identifier of the hook: the value of a rule's `hook` and of the `hook` query filter (`booking.create`, `booking.amend` or `payment`).",
            "example": "booking.create"
          },
          "description": {
            "type": "string",
            "description": "When the hook runs in the booking flow and what an expression rule can do there (reject, write, flag).",
            "example": "Runs before a partner booking is created in the RMS. A rule can reject the booking, turn it into an enquiry (set booking.type / booking.enquiryReason) or flag it."
          },
          "triggeredBy": {
            "type": "array",
            "description": "The endpoint calls that run this hook. For `payment` it lists the calls that compute the amount payable of an existing booking.",
            "items": {
              "type": "string"
            },
            "example": [
              "POST /venues/{compositeId}/booking"
            ]
          },
          "writablePaths": {
            "type": "array",
            "description": "The only paths a `set` action may write at this hook. Empty = the hook can only reject or flag.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingHookWritablePath"
            }
          },
          "facts": {
            "type": "array",
            "description": "Every fact an expression can read at this hook, with how to compute it from your request.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingHookFact"
            }
          }
        }
      },
      "PartnerBookingHookWritablePath": {
        "type": "object",
        "description": "A path an expression rule may write with a `set` action at the hook, with the values the platform accepts.",
        "required": [
          "path",
          "type",
          "allowedValues",
          "meaning"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Dotted path that a `set` action may write (also readable as a fact once written).",
            "example": "booking.type"
          },
          "type": {
            "type": "string",
            "description": "Type of the value written:\n- `number`: a JSON number.\n- `string`: a JSON string.\n- `bool`: `true` or `false`.\n- `list`: a JSON array.\n- `object`: a JSON object.",
            "enum": [
              "number",
              "string",
              "bool",
              "list",
              "object"
            ],
            "example": "string"
          },
          "allowedValues": {
            "type": "array",
            "description": "The values the platform accepts at this path; any other value is ignored.",
            "items": {
              "type": "string"
            },
            "example": [
              "book",
              "request"
            ]
          },
          "meaning": {
            "type": "string",
            "description": "What writing this path means for the booking (the consequence an expression rule has by writing it).",
            "example": "The booking type. Writing `request` makes the booking an enquiry the operator must accept instead of an instant confirmation. Any other value makes the rule be skipped."
          }
        }
      },
      "PartnerBookingHookFact": {
        "type": "object",
        "description": "One value an expression can read with `{\"var\": \"<path>\"}` at the hook, with its type and the recipe to compute it from the request you send (create) or the booking you amend (amend).",
        "required": [
          "path",
          "type",
          "isList",
          "nullable",
          "description",
          "howToObtain"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Dotted path for `{\"var\": ...}`. Absolute (`booking.partySize`) at the top level of `facts`; relative to the element (`slug`) inside `elementFields`.",
            "example": "booking.partySize"
          },
          "type": {
            "type": "string",
            "description": "Type of the value:\n- `number`: a decimal number.\n- `string`: a JSON string.\n- `bool`: `true` or `false` (never a number or string).\n- `list`: an array; see `isList`, `elementType` and `elementFields` for its elements.\n- `object`: a nested object (not used by the facts of today's hooks).",
            "enum": [
              "number",
              "string",
              "bool",
              "list",
              "object"
            ],
            "example": "number"
          },
          "isList": {
            "type": "boolean",
            "description": "true when the fact is a list (then `type` is `list`): read it with the list operators (`count`, `sum`, `some`, `filter`, ...). When false the fact is a single value.",
            "example": false
          },
          "nullable": {
            "type": "boolean",
            "description": "true when the fact can be null. An operator that needs a number, string, boolean or list and receives null makes the rule skipped (it does not apply); use `{\"var\": [path, default]}` to be null-safe.",
            "example": false
          },
          "description": {
            "type": "string",
            "description": "What the fact means (for amend hooks, the state after the amend).",
            "example": "Number of guests on the booking (the new value on an amend)"
          },
          "howToObtain": {
            "type": "string",
            "description": "How to compute the value from the booking request you send (create) or the booking you amend (amend): which request field to copy or which derivation to apply. Hook specific: the same fact can be obtained differently at `booking.create` and `booking.amend`, and this text is already the one of the hook it belongs to.",
            "example": "Copy of `partySize` of the create request body. `children` is not added to it."
          },
          "elementType": {
            "type": "string",
            "nullable": true,
            "enum": [
              "number",
              "string",
              "bool"
            ],
            "description": "For a list of scalars, the type of its elements; omitted otherwise.\n- `number`: every element is a number (for example `partner.categoryIds`).\n- `string`: every element is a string.\n- `bool`: every element is `true` or `false`.",
            "example": "number"
          },
          "elementFields": {
            "type": "array",
            "nullable": true,
            "description": "For a list of objects, the fields of one element (use them inside `filter` / `map` / `some` / `all` / `none` as `{\"var\": \"<field>\"}` and as the field name of `pluck`); omitted otherwise.",
            "items": {
              "$ref": "#/components/schemas/PartnerBookingHookFact"
            }
          }
        }
      },
      "BookingRuleViolation": {
        "type": "object",
        "description": "One rule that rejected the booking, as listed in `violations[]` of the 400 problem.",
        "required": [
          "ruleId",
          "ruleName",
          "effect",
          "code"
        ],
        "properties": {
          "ruleId": {
            "type": "string",
            "format": "uuid",
            "description": "Id of the rejecting rule: the same value as `rules[].id` in `GET /booking-rules`.",
            "example": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36"
          },
          "ruleName": {
            "type": "string",
            "description": "Name of the rule (`rules[].name`).",
            "example": "Premium brunch - whole table, no mixing"
          },
          "effect": {
            "$ref": "#/components/schemas/PartnerBookingRuleEffect"
          },
          "code": {
            "type": "string",
            "description": "Error code of this violation (`BKRUL-N-001` to `BKRUL-N-008`, `AVAIL-N-007` / `AVAIL-N-008` for a closed booking_window book window, or `BKRUL-N-100` for an expression rule).",
            "pattern": "^(BKRUL-N-(00[1-8]|100)|AVAIL-N-00[78])$",
            "example": "BKRUL-N-001"
          }
        }
      },
      "BookingRuleViolationProblem": {
        "type": "object",
        "description": "The problem document that the booking endpoints (create, PUT, PATCH, and DELETE for a booking_window cancel rule) answer with HTTP 400 when one or more booking condition rules reject the booking. It is not returned by `GET /booking-rules`; it is documented here so that a client can connect a rejection to `rules[]`.",
        "required": [
          "title",
          "status",
          "code",
          "isRetryable",
          "violations"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Problem type URI (RFC 9457), the generic link of the HTTP status; it does not identify the rule.",
            "example": "https://tools.ietf.org/html/rfc9110#section-15.5.1"
          },
          "title": {
            "type": "string",
            "description": "Title of the primary violation, for example \"Same Package Violation\" or \"Generic Rule Violation\".",
            "example": "Same Package Violation"
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "description": "HTTP status, always 400 for a rule rejection.",
            "example": 400
          },
          "detail": {
            "type": "string",
            "description": "The message of the primary violation: the rule's `message` if set, else the default message of the effect (for expression rules the message of the first rejecting `reject`). Show it to your user.",
            "example": "If this package is chosen, everyone must have the same one and nothing else can be added."
          },
          "code": {
            "type": "string",
            "description": "Error code of the primary violation (the one with the highest precedence among the fixed rules, else `BKRUL-N-100`).",
            "pattern": "^(BKRUL-N-(00[1-8]|100)|AVAIL-N-00[78])$",
            "example": "BKRUL-N-001"
          },
          "isRetryable": {
            "type": "boolean",
            "description": "Always false: the same request will fail again, change the booking.",
            "example": false
          },
          "violations": {
            "type": "array",
            "description": "Every rejecting rule, not only the primary one. Fixed-rule violations and expression-rule violations never appear together (expression rules run only when all fixed rules passed).",
            "items": {
              "$ref": "#/components/schemas/BookingRuleViolation"
            }
          },
          "analyticsRuleId": {
            "type": "string",
            "format": "uuid",
            "description": "Id of the primary rejecting rule (`rules[].id`).",
            "example": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36"
          },
          "analyticsRuleName": {
            "type": "string",
            "description": "Name of the primary rejecting rule (`rules[].name`).",
            "example": "Premium brunch - whole table, no mixing"
          },
          "traceId": {
            "type": "string",
            "description": "Request trace identifier; quote it when you contact support.",
            "example": "0HNJ2BG2TU3BU:00000001"
          }
        },
        "example": {
          "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
          "title": "Same Package Violation",
          "status": 400,
          "detail": "If this package is chosen, everyone must have the same one and nothing else can be added.",
          "code": "BKRUL-N-001",
          "isRetryable": false,
          "violations": [
            {
              "ruleId": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36",
              "ruleName": "Premium brunch - whole table, no mixing",
              "effect": "same_package",
              "code": "BKRUL-N-001"
            },
            {
              "ruleId": "e07a4b19-6c2d-48f3-b5a1-3d9c70f2e864",
              "ruleName": "Deposit of 10 pounds per person",
              "effect": "require_deposit",
              "code": "BKRUL-N-006"
            }
          ],
          "analyticsRuleId": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36",
          "analyticsRuleName": "Premium brunch - whole table, no mixing",
          "traceId": "0HNJ2BG2TU3BU:00000001"
        }
      },
      "ServerError": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A URI reference [rfc9457] that identifies the problem type. This specification encourages that, when dereferenced, it provides human-readable documentation for the problem type (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be \"about:blank\"."
          },
          "title": {
            "type": "string",
            "description": "A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization."
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "description": "The HTTP status code [rfc9457, Section 3.1.12] generated by the origin server for this occurrence of the problem."
          },
          "detail": {
            "type": "string",
            "description": "A human-readable explanation specific to this occurrence of the problem."
          },
          "code": {
            "type": "string",
            "description": "Error code in the format RESOURCE-X-NNN where X is R (retryable) or N (non-retryable). See ErrorCatalog.md for all error codes.",
            "example": "BOOK-N-001"
          },
          "isRetryable": {
            "type": "boolean",
            "description": "Indicates whether the error is retryable. If true, the request may succeed if retried. If false, the request will fail again with the same input."
          },
          "traceId": {
            "type": "string",
            "description": "Request trace identifier for debugging and correlation purposes.",
            "example": "0HNJ2BG2TU3BU:00000001"
          }
        },
        "required": [
          "title",
          "status",
          "code",
          "isRetryable"
        ]
      },
      "ClientError": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A URI reference [rfc9457] that identifies the problem type. This specification encourages that, when dereferenced, it provides human-readable documentation for the problem type (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be \"about:blank\"."
          },
          "title": {
            "type": "string",
            "description": "A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization."
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "description": "The HTTP status code [rfc9457, Section 3.1.12] generated by the origin server for this occurrence of the problem."
          },
          "detail": {
            "type": "string",
            "description": "A human-readable explanation specific to this occurrence of the problem."
          },
          "code": {
            "type": "string",
            "description": "Error code in the format RESOURCE-X-NNN where X is R (retryable) or N (non-retryable). See ErrorCatalog.md for all error codes.",
            "example": "VALID-N-001"
          },
          "isRetryable": {
            "type": "boolean",
            "description": "Indicates whether the error is retryable. If true, the request may succeed if retried. If false, the request will fail again with the same input."
          },
          "traceId": {
            "type": "string",
            "description": "Request trace identifier for debugging and correlation purposes.",
            "example": "0HNJ2BG2TU3BU:00000001"
          },
          "errors": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Validation errors dictionary mapping field names to arrays of error messages. Only present for validation errors (VALID-N-001).",
            "example": {
              "Date": [
                "Availability date cannot be in the past."
              ],
              "EndTime": [
                "EndTime must be after StartTime."
              ]
            }
          }
        },
        "required": [
          "title",
          "status",
          "code",
          "isRetryable"
        ]
      }
    },
    "securitySchemes": {
      "Live": {
        "type": "oauth2",
        "description": "This API uses OAuth2 client credentials flow via Auth0. The calling partner is identified from the token.\n**Important:** Include the `audience` parameter in your token request.\nExample token request:\n```\nPOST https://auth.bookabletech.com/oauth/token\n{\n  \"grant_type\": \"client_credentials\",\n  \"client_id\": \"YOUR_CLIENT_ID\",\n  \"client_secret\": \"YOUR_CLIENT_SECRET\",\n  \"audience\": \"api.bookabletech.com\"\n}\n```\n",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://auth.bookabletech.com/oauth/token",
            "scopes": {
              "venue:read": "Grants read access to venue details, availability and the booking rules that apply to the calling partner"
            }
          }
        }
      },
      "Sandbox": {
        "type": "oauth2",
        "description": "OAuth2 client credentials flow via Auth0 (sandbox tenant). The calling partner is identified from the token.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://auth-sandbox.bookabletech.com/oauth/token",
            "scopes": {
              "venue:read": "Grants read access to venue details, availability and the booking rules that apply to the calling partner"
            }
          }
        }
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Maximum number of requests allowed per window.",
        "schema": {
          "type": "integer"
        },
        "example": 200
      },
      "RateLimitRemaining": {
        "description": "Number of requests remaining in the current window.",
        "schema": {
          "type": "integer"
        },
        "example": 150
      },
      "RateLimitReset": {
        "description": "Unix timestamp (seconds since epoch) when the current rate-limit window resets.",
        "schema": {
          "type": "integer",
          "format": "int64"
        },
        "example": 1741651200
      }
    },
    "responses": {
      "ServerError": {
        "description": "Server error",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServerError"
            }
          },
          "text/toon": {
            "schema": {
              "$ref": "#/components/schemas/ServerError"
            }
          }
        }
      },
      "InternalServerError": {
        "description": "Internal Server Error - An unexpected error occurred",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServerError"
            },
            "example": {
              "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
              "title": "Internal Server Error",
              "status": 500,
              "detail": "An unexpected error occurred while processing the request",
              "code": "SYS-N-001",
              "isRetryable": false,
              "traceId": "0HNJ2BG2TU3BU:00000007"
            }
          },
          "text/toon": {
            "schema": {
              "$ref": "#/components/schemas/ServerError"
            },
            "example": "type: \"https://tools.ietf.org/html/rfc9110#section-15.6.1\"\ntitle: Internal Server Error\nstatus: 500\ndetail: An unexpected error occurred while processing the request\ncode: SYS-N-001\nisRetryable: false\ntraceId: \"0HNJ2BG2TU3BU:00000007\"\n"
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Service Unavailable - The server is temporarily unavailable",
        "headers": {
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ServerError"
            },
            "example": {
              "type": "https://api.bookabletech.com/problems/service-unavailable",
              "title": "Service Unavailable",
              "status": 503,
              "detail": "The server is temporarily unavailable. Please try again later",
              "code": "SYS-R-503",
              "isRetryable": true
            }
          },
          "text/toon": {
            "schema": {
              "$ref": "#/components/schemas/ServerError"
            },
            "example": "type: \"https://api.bookabletech.com/problems/service-unavailable\"\ntitle: Service Unavailable\nstatus: 503\ndetail: The server is temporarily unavailable. Please try again later\ncode: SYS-R-503\nisRetryable: true\n"
          }
        }
      }
    }
  }
}