Skip to content

Partner Booking Rules API (1.6.0)

Overview

Operators (venue groups) can attach booking condition rules to their venues. The platform enforces these rules, server-side and deterministically, on every partner booking create (POST /venues/{compositeId}/booking) and booking amend (PUT / PATCH /venues/bookings/{bookingId}). A booking that breaks a rule is rejected with HTTP 400 and an error code BKRUL-N-001 ... BKRUL-N-006 or BKRUL-N-100; one rule (route_to_enquiry, or an expression rule that sets booking.type) instead turns the booking into an enquiry.

A third kind of expression rule does not gate a booking at all: a rule at the payment hook (moment booking_payment) changes the amount payable for a booking that already exists, when the platform computes its payment options (GET /venues/bookings/{bookingId}/payment-options) or creates its payment (POST /venues/bookings/{bookingId}/payments). See "Rules at the payment hook" below.

This API is read-only and advisory. GET /booking-rules returns every booking condition rule that is active for the calling partner, with enough structure and documentation for an integration, in particular one written by an AI agent, to understand which conditions the platform enforces and to avoid hitting them. The booking endpoints are authoritative: the platform always re-validates every booking server side, whatever this API returned.

Which rules are returned. Only rules that are active (inactive rules are never returned) and that apply to the calling partner, i.e. rules that have no partner target at all, or that name your partner, or that name one of your partner categories (same partner semantics the booking path uses). An expression rule can also aim at partners inside its when (facts partner.id, partner.categoryIds): it is returned only when, with your own partner facts, its when can still be true (see Confidentiality). Rules aimed at other partners are never returned; appliesTo.partnerScope only says how the rule is aimed at you through its partner targets (all_partners, you, your_categories).

Which venues are shown. appliesTo.venues[] lists only the venues you can see (the venues GET /venues returns to you; if your partner mapping restricts an operator to some venues, only those). A rule restricted to venues none of which you can see is not returned at all: its restriction can never include a venue you book. allVenues: false with a non-empty venues[] therefore means "only these of your venues".

Who is "you". The partner is identified from your credentials (OAuth2 client credentials). Bookable Agents (scope agent:write) send X-Partner-Reference to act for a partner, exactly as on the other partner endpoints. The response field caller repeats your own partner id and partner category ids: those are the values of the facts partner.id and partner.categoryIds that expression rules read. The categories you belong to differ per operator (an operator can also place you in a category through its own third-party records), so the value of partner.categoryIds for a rule is caller.categoryIdsByOperator[rule.operatorId], not the global caller.partnerCategoryIds.

Quick start

# 1. Get a token (OAuth2 client credentials; the client must hold the venue:read permission).
curl -s -X POST https://auth.bookabletech.com/oauth/token \
  -H 'Content-Type: application/json' \
  -d '{"grant_type":"client_credentials","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","audience":"api.bookabletech.com"}'

# 2. Every active rule that applies to you (no parameter is required).
curl -s 'https://api.bookabletech.com/booking-rules' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Accept: application/json'

# 2b. Bookable Agents (scope agent:write) act for a partner by sending X-Partner-Reference.
curl -s 'https://api.bookabletech.com/booking-rules' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Partner-Reference: acme-travel'

The answer is a JSON object {generatedAt, caller, rules[], hooks[]}; the named 200 examples of this operation show it in full. The filters are all optional and combined with AND (a | inside a venue id is sent as %7C):

I wantRequest
Everything that applies to meGET /booking-rules
Only operator 17GET /booking-rules?operatorId=17
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
Only deposit rulesGET /booking-rules?effect=require_deposit
Only what is checked when I create a bookingGET /booking-rules?hook=booking.create
Only what is checked when I amend a bookingGET /booking-rules?hook=booking.amend
Only the rules that can change the amount payable for an existing bookingGET /booking-rules?hook=payment
All filters togetherGET /booking-rules?operatorId=17&venueId=17%7CX9%7C9d3f0b6e7a1c4e58b2c4f1a60d7e8b93&effect=same_package&hook=booking.create

Confidentiality

Only rules that can apply to you are returned. This includes expression rules that target partners inside when: the platform evaluates the partner.* part of when with your own partner id and categories (every other fact unknown) and leaves the rule out when when can never be true for you. A rule aimed at several partners, you among them, is returned.

For the rules that are returned, expression trees (when, then) and the operator-written text (name, description, message) are returned exactly as the operator stored them. They can therefore contain numeric ids (for example the other partner or partner category ids of a rule that also applies to you, compared with partner.id) or words chosen by the operator. The platform itself never adds the name of another partner or of another partner category to any field of the response: the generated summary names neither partners nor partner categories, and appliesTo only says how a rule is aimed at you (partnerScope). Do not treat the content of when, then, name, description or message as a statement about you or about any other partner.

Integration guide for AI agents

  1. Fetch the rules. Call GET /booking-rules. Optional filters: operatorId (one operator), venueId (only rules that apply to that venue), effect, hook. Read caller (your partner id and categories per operator), rules[] and hooks[]. The list is not paginated: you get the whole active set that applies to you.
  2. Keep the rules that can concern your booking, using rules[].appliesTo:
    • venue: allVenues is true, or the first three segments of the 4-segment compositeId you book ({operatorId}|{rmsSlug}|{venueResourceId}) equal one of venues[].id (venues[] lists only venues you can see; a rule restricted to venues you cannot see is not returned). With venueId the server already did this.
    • product (booking type): allProducts is true, or the slug of the product you book (the slug field of the product in GET /venues) equals one of products[].slug. A rule with a product list never applies to a product that has no slug.
    • conditions[], if any: all must hold (see "Field conditions").
    • partnerScope already tells you the rule targets you (or everybody); you do not need to filter on it.
    • whenApplied: keep rules whose list contains the operation you are about to call (booking_create for create, booking_amend for PUT/PATCH). booking_payment is not an operation you send a request body to: it means the rule runs when the platform computes the amount payable for a booking that already exists (the payment options and the payment creation endpoints). Such a rule never gates your create or amend, and you cannot reproduce its facts (payment.bands) from the booking request alone: do NOT try to evaluate it client-side as a pass/fail gate. Use it to understand that the final balance may be the sum of the minimum-spend bands the booking crosses, and read the authoritative amount from GET /venues/bookings/{bookingId}/payment-options (operationId getPaymentOptions of the Payment API).
  3. Fixed effects (every effect except expression): apply the exact semantics in the effects table below to the booking you intend to send, using params, appliesTo.items and the pre-order lines.
  4. Expression rules (effect: expression): pick the rules whose hook matches the operation (booking.create for create, booking.amend for amend; the rules of hook payment are not evaluated by you, see step 2 and "Rules at the payment hook"). Look the hook up in hooks[]. Build the fact document (a JSON object with the top-level keys booking, partner, product, venue, space) by following hooks[].facts[].howToObtain for every fact. Evaluate when against it using the language reference below. Evaluate the matching rules in the order they appear in rules[] (priority ascending, then rule id); a set written by an earlier rule is visible to the later rules.
  5. Interpret the outcome. If when is true, the then actions apply; rules[].outcomes already lists them so you need not parse then: rejects (the booking will be refused with BKRUL-N-100 and rejectMessages), writes (values the platform will write, e.g. booking.type = request makes the booking an enquiry; at the payment hook the only write is payment.finalBalance, the amount payable) and flags (named annotations without any behaviour). If when is false the rule does nothing. If a fact is missing or has the wrong type for an operator, the rule is skipped (does not apply), it never fails the booking.
  6. Change the booking (party size, date, space, pre-order lines, deposit) until no rule rejects it, or accept the enquiry outcome.
  7. When you are rejected, read violations[] of the 400 response and match each violations[].ruleId with rules[].id from this API: it is the same id. Then fix what that rule says (summary, message, params).
  8. Re-fetch whenever you start a new booking flow. Rules can be changed, added or deactivated by the operator at any time; this API offers no change notification and no cache validators, so do not assume that a previous answer is still current. The result is a pre-check: always handle a BKRUL-* rejection from the booking endpoint as the final word.

Glossary

TermMeaning
Operator / venue groupThe company that owns the venues. operatorId is its numeric id (the first segment of every composite id).
RuleOne booking condition configured by an operator. Identified by a UUID (id).
EffectThe kind of rule. Seven fixed effects with fixed semantics plus expression (a rule defined by data).
HookA 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).
Final balanceThe 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.
Minimum-spend bandA 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.
FactA named value an expression can read with {"var": "<path>"} (for example booking.partySize). Facts are derived from your request; hooks[].facts[].howToObtain says how.
Expression (when)A boolean condition written as JSON (JsonLogic format).
Action (then)What happens when when is true: set a writable value, reject the booking, or flag it.
EnquiryA booking submitted as type: request instead of an instant confirmation: the operator must accept it.
Pre-order lineOne 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.
Composite id{operatorId}|{rmsSlug}|{venueResourceId} identifies a venue (3 segments); {operatorId}|{rmsSlug}|{venueResourceId}|{productId} identifies a bookable product (4 segments).
Party sizeThe partySize of the request (children are not added to it).
daysAheadWhole calendar days from today (Europe/London) to the booking date.
ViolationOne rule that rejected a booking, reported in violations[] of the 400 error.

Field guide: what to do with each field

FieldWhat it isWhat an integration should DO with it
generatedAtUTC 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.
caller.partnerIdYour 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.
caller.categoryIdsByOperatorObject 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.
caller.partnerCategoryIdsThe 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.
rules[].idRule UUID.Keep it: the same value comes back as violations[].ruleId and analyticsRuleId in a 400 rejection.
rules[].operatorIdOperator (venue group) that owns the rule.Keep the rules whose operatorId equals the first segment of the composite id you book.
rules[].effectKind of rule: one of 9 values.Every effect except expression is a fixed effect: apply the effects table. expression: evaluate when and apply then.
rules[].appliesToVenue, 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.
rules[].conditionsExtra request-field filters (usually empty).All entries must hold, otherwise the rule does not apply to this booking.
rules[].paramsParameters of a fixed effect, defaults already applied.Plug them into the formulas of "How to evaluate each fixed effect". Empty for expression rules.
rules[].messageThe 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.
rules[].summaryOne generated English sentence.Show or log it; never parse it.
rules[].whenAppliedMoments 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").
rules[].consequenceWhat 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).
rules[].errorCodeThe 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).
rules[].hookExpression 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.
rules[].priorityExpression 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.
rules[].when, rules[].thenExpression rule condition and actions, JSON.Evaluate with the expression language reference; then only runs when when is true.
rules[].outcomesWhat 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.
hooks[].facts[].howToObtainHow 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.
hooks[].writablePathsThe 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).

Effects

EffectWhat it enforcesParamsApplies onError codeDefault message when the rule has no message
same_packageLet 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, wholeGroupcreate, amendBKRUL-N-001exclusive: "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)."
max_quantity_party_sizeTotal 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.perItemcreate, amendBKRUL-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)
max_quantityTotal 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, perItemcreate, amendBKRUL-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.")
preorder_requiredIf 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.thresholdcreate, amendBKRUL-N-004"Groups of N or more must include a pre-order."
route_to_enquiryA 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.leadTimeDayscreate only (an amend never un-confirms a booking)none (consequence is turns_booking_into_enquiry)not applicable
lock_preorder_editsInside 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.leadTimeDayscreate, amendBKRUL-N-005"Pre-order changes are not permitted less than N days before the booking date."
require_depositThe 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, percreate, amendBKRUL-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)
preorder_emailIf 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.thresholdcreate, amendnone (consequence is skips_preorder_email)not applicable
booking_windowYou 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, maxHoursBeforebook: create, and amend when the date or time changes. amend: amend. cancel: cancelbook: 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 ….")
expressionA 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 '' rejected the booking."

Numbers for each effect, with one violating and one passing booking, are in "How to evaluate each fixed effect" below.

params defaults applied by the platform when a key is missing: exclusive true, wholeGroup false, threshold 1, leadTimeDays 0, per person. The values returned by this API are already resolved.

Which rules apply to a booking (scope)

A rule applies to a booking when ALL of these hold: it belongs to the booked operator; its venue list is empty (every venue) or contains the booked venue; its product list is empty (every product) or contains the booked product's aggregation slug; it has no partner target, or targets your partner id, or targets one of your categories (either match is enough); and every conditions[] entry holds. Item lists (appliesTo.items) are not part of the scope test of any effect: they are the set the effect works on. For expression rules the item lists are informational only and are not evaluated.

Field conditions

conditions[] is an optional extra filter: each entry compares one request field with a value and ALL entries must hold. Fields: on create partySize, duration, spaceId, isCrossSale, commission, depositAmount, hasCustomerCompany, hasNotes; on amend only partySize, duration, commission, hasCustomerCompany, hasNotes; at payment (read from the stored booking) partySize, duration, spaceId, commission, depositAmount, hasCustomerCompany, hasNotes. A field the booking does not carry never matches, whatever the operator (even not_equals). Operators: equals, not_equals, gte, lte (numbers), in (the field value is one of the listed values), contains (the field value is a list containing the value). At the time of writing the operator tooling has no way to create conditions, so you will normally see an empty list.

Ordering and combination

  • Fixed rules are all evaluated; every fixed violation is reported in violations[], and the error shown (code, title, detail) is the one with the highest precedence: lock_preorder_edits, then preorder_required, then same_package, then max_quantity_party_size, then max_quantity, then require_deposit.
  • Expression rules are evaluated only after all fixed rules passed. A booking rejected by a fixed rule never shows expression violations. On create, a fixed route_to_enquiry has already set booking.type = request and booking.enquiryReason = operatorRule when the expression rules run, so expressions see those values.
  • Expression rules of one hook run by priority ascending (default 100), then by rule id. rules[] is returned in that order: expression rules of booking.create, then of booking.amend, then of payment.
  • Rules of the payment hook run at their own moment (see the next section), never together with the fixed rules or with the rules of the booking hooks.
  • Several set actions on one path: they compose in that order and the last one wins. Every later rule (its when and its actions) reads the facts plus the writes made so far.
  • A rule is atomic: if one of its actions cannot be carried out (for example a set value is not computable), the whole rule is skipped and none of its rejections, flags or writes is kept.
  • Everything an expression rule cannot evaluate is silently skipped, never an error for you: a fact that is null or missing where an operator needs a number, string, boolean or list, a type mismatch, division by zero, a when that is not boolean, a set to a path that is not writable at the hook, or a limit overrun.

Rules at the payment hook (moment booking_payment)

A rule with hook: payment and whenApplied: [booking_payment] does not accept or refuse a booking. It changes how much is payable for a booking that already exists. Everything below is read from the platform's payment code.

  • When it runs. Each time the platform computes the payable amount of an existing booking: when you call GET /venues/bookings/{bookingId}/payment-options (operationId getPaymentOptions, Payment API) and when you call POST /venues/bookings/{bookingId}/payments (operationId createPayment). It is not evaluated at booking create or amend, and it does not change what availability or GET /venues show: availability lists each minimum-spend band the slot's interval [start, start + duration) crosses, each with its own value and no total, and GET /venues shows the configured rules and overrides.
  • Preconditions. The booked space has an enabled minimum-spend policy and the stored booking has a duration greater than 0. Without them no payment rule is consulted and the amount is the minimum spend at the start time, as before. A rule is also subject to the scope tests above (operator, venue, product, partner, conditions[]), evaluated on the stored booking.
  • Facts. All facts are computed by the platform from the stored booking and the space's minimum-spend policy; you do not send them. The new root is payment: payment.bands (the minimum-spend bands that the interval [start, start + duration) overlaps, in chronological order, each band once, each with label, from, to, spendType, spendValue in pence, hireFeeValue in pence and overlapMinutes), payment.bandCount, payment.minimumSpend (the value the options use when no rule applies: the first matching entry at the start time only, null when no band contains the start time) and payment.currency (always GBP). The booking.*, partner.*, product.*, venue.* and space.* facts are those of the stored booking; booking.items and booking.originalItems are always empty, booking.isCreate is false, and booking.type, booking.enquiryReason and booking.isCrossSale are null at this hook. hooks[] documents each of them for this hook.
  • The only write. payment.finalBalance: the total payable, an integer number of pence (GBP). The value must be a number from 0 up to 2147483647; anything else (negative, not a number, too large) makes the rule be skipped. A value with decimals is rounded to the nearest penny (halves away from zero). If several rules write it, the last one wins (priority, then rule id). A value equal to the minimum spend at the start time changes nothing.
  • What follows from the value. The "full minimum spend" option is the final balance. The "25% deposit" option is computed on it: the final balance times 0.25, rounded to a whole penny (a half rounds to the even penny). When the resulting amount is not greater than 0 the options list is empty, and createPayment cannot create the payment.
  • What has no effect. A reject action at this hook is ignored by the platform (it only logs a warning), so outcomes.rejects is always false for a payment rule and errorCode is omitted. A flag has no behaviour here either. A rule that cannot be evaluated, or an error while evaluating, keeps the amount the platform would have used without the rule; it never fails the payment call.
  • What you should do with it. You cannot rebuild payment.bands: it comes from the space's minimum-spend policy (rules and date overrides configured by the operator), which is not part of your booking request and is not returned by this API. Do not evaluate a payment rule client-side as a gate. Read it to know that the final balance may be the sum of the minimum-spend bands the booking crosses, and take the authoritative amount from the payment options (getPaymentOptions): amounts are integers in pence. The amount can change if the booking is amended or the operator changes the rule, so ask again before you pay.

How to evaluate each fixed effect

Every example uses operator 17, the venue 17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93 (The Rooftop Kitchen), the product bottomless-brunch unless stated, partner 482 (category 1) and "today" = 2026-10-01. The rules are the ones of the allRules example of GET /booking-rules (cited by the first 8 characters of their id). For each effect: the inputs, the computation, then one violating and one passing booking. The JSON block after each effect is the same scenarios in machine-readable form: rule (id prefix), partySize, date, create (false = amend), packages / menus ([slug, quantity] pairs of the final booking), original (stored lines on an amend), deposit (the request depositAmount on create, the stored deposit on amend), product (slug of the booked product) and expect. The platform's test-suite runs every scenario against the real evaluator, so these numbers cannot drift from the behaviour. Quantities are those of the pre-order lines (a package or menu line counts its own quantity, not its items). daysAhead = booking date minus 2026-10-01 in whole days.

same_package with exclusive: true (rule 9c1d5e72)

Rule: set S = {premium-bottomless-brunch}, exclusive true, wholeGroup false. Party of 6, date 2026-11-14. Computation: (1) take the chosen lines whose slug is in S; none means the rule is inert. (2) More than one distinct S slug: 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.

  • 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."
  • Violating: premium x2 + mocktail-pairing x4. The mocktail is outside S: BKRUL-N-001.
  • Passing: premium x6, or premium x2 (wholeGroup is false, so the quantity need not equal the party size 6).
  • Passing: standard x6. No S line is chosen, the rule is inert.

Scenarios - same_package exclusive

[
  {"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."}},
  {"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."}},
  {"rule": "9c1d5e72", "partySize": 6, "date": "2026-11-14", "packages": [["premium-bottomless-brunch", 6]], "expect": {"violation": null}},
  {"rule": "9c1d5e72", "partySize": 6, "date": "2026-11-14", "packages": [["premium-bottomless-brunch", 2]], "expect": {"violation": null}},
  {"rule": "9c1d5e72", "partySize": 6, "date": "2026-11-14", "packages": [["standard-bottomless-brunch", 6]], "expect": {"violation": null}}
]

same_package with exclusive: false and wholeGroup: true (rule 2b7f9d40)

Rule: S = {premium-bottomless-brunch, standard-bottomless-brunch}. Party of 6, date 2026-11-14. Computation: (1) no S line chosen: inert. (2) More than one distinct S slug: violated (lines outside S are ignored because exclusive is false). (3) wholeGroup: the summed quantity of the S lines must equal the party size.

  • 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."
  • 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)."
  • Passing: premium x6 + mocktail-pairing x2. The S lines sum to 6 and the mocktail is outside S, so it is ignored.
  • Passing: mocktail-pairing x6 only. No S line, inert.

Scenarios - same_package non-exclusive whole group

[
  {"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."}},
  {"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)."}},
  {"rule": "2b7f9d40", "partySize": 6, "date": "2026-11-14", "packages": [["premium-bottomless-brunch", 6], ["mocktail-pairing", 2]], "expect": {"violation": null}},
  {"rule": "2b7f9d40", "partySize": 6, "date": "2026-11-14", "packages": [["mocktail-pairing", 6]], "expect": {"violation": null}}
]

max_quantity_party_size (rule 6d40c8e1)

Rule: S is empty, so every chosen package and menu counts. Party of 4, date 2026-11-14. Computation: total = sum of the quantities of the counted lines; violated when total > party size (equal is allowed).

  • Violating: premium x3 + standard x2. Total 5 > 4: BKRUL-N-002, "The total quantity of these items (5) cannot exceed the party size (4)."
  • Violating: premium x2 + kids-menu (a menu) x3. Menus count too: 5 > 4.
  • Passing: premium x2 + standard x2. Total 4, and 4 > 4 is false.
  • Passing: no pre-order at all. Total 0.

Scenarios - max_quantity_party_size

[
  {"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)."}},
  {"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)."}},
  {"rule": "6d40c8e1", "partySize": 4, "date": "2026-11-14", "packages": [["premium-bottomless-brunch", 2], ["standard-bottomless-brunch", 2]], "expect": {"violation": null}},
  {"rule": "6d40c8e1", "partySize": 4, "date": "2026-11-14", "expect": {"violation": null}}
]

max_quantity (rule a13e5f87)

Rule: S = {menu kids-menu}, max 4. Party of 10, date 2026-11-14. Computation: 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).

  • Violating: kids-menu x5. 5 > 4: BKRUL-N-003, "No more than 4 of this item is allowed per booking."
  • Passing: kids-menu x4. 4 > 4 is false.
  • Passing: kids-menu x3 + sunday-roast-menu x6. Only the S line counts (3).

Scenarios - max_quantity

[
  {"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."}},
  {"rule": "a13e5f87", "partySize": 10, "date": "2026-11-14", "menus": [["kids-menu", 4]], "expect": {"violation": null}},
  {"rule": "a13e5f87", "partySize": 10, "date": "2026-11-14", "menus": [["kids-menu", 3], ["sunday-roast-menu", 6]], "expect": {"violation": null}}
]

preorder_required (rules c58b0a92 with items, f2a6d3c1 without)

Rule c58b0a92: S = {premium-bottomless-brunch, standard-bottomless-brunch}, threshold 8. Date 2026-11-14. Computation: if party size < threshold the rule is inert (party size >= threshold is "inside"); otherwise at least one chosen line must be in S.

  • 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."
  • Violating: party 8 with mocktail-pairing x8 only. The mocktail is outside S.
  • Passing: party 7, no pre-order. 7 < 8, inert.
  • Passing: party 8 with standard x8.

Rule f2a6d3c1: S is empty, so ANY chosen package or menu qualifies, threshold 12.

  • Violating: party 12, no pre-order: BKRUL-N-004, "Groups of 12 or more must include a pre-order."
  • Passing: party 12 with mocktail-pairing x1 (any line qualifies).
  • Passing: party 11, no pre-order (11 < 12).

Scenarios - preorder_required

[
  {"rule": "c58b0a92", "partySize": 8, "date": "2026-11-14", "expect": {"violation": "BKRUL-N-004", "message": "Groups of 8 or more must include a pre-order."}},
  {"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."}},
  {"rule": "c58b0a92", "partySize": 7, "date": "2026-11-14", "expect": {"violation": null}},
  {"rule": "c58b0a92", "partySize": 8, "date": "2026-11-14", "packages": [["standard-bottomless-brunch", 8]], "expect": {"violation": null}},
  {"rule": "f2a6d3c1", "partySize": 12, "date": "2026-11-14", "expect": {"violation": "BKRUL-N-004", "message": "Groups of 12 or more must include a pre-order."}},
  {"rule": "f2a6d3c1", "partySize": 12, "date": "2026-11-14", "packages": [["mocktail-pairing", 1]], "expect": {"violation": null}},
  {"rule": "f2a6d3c1", "partySize": 11, "date": "2026-11-14", "expect": {"violation": null}}
]

route_to_enquiry (rule 0e9c7b35, create only)

Rule: 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.

  • Enquiry: date 2026-10-04. daysAhead 3 < 7.
  • Enquiry: date 2026-10-07. daysAhead 6 < 7.
  • Confirmed normally: date 2026-10-08. daysAhead 7 and 7 < 7 is false: exactly leadTimeDays ahead is outside the window.
  • Not affected: an amend of a booking dated 2026-10-04 (the effect is create only).

Scenarios - route_to_enquiry

[
  {"rule": "0e9c7b35", "partySize": 4, "date": "2026-10-04", "expect": {"enquiry": true}},
  {"rule": "0e9c7b35", "partySize": 4, "date": "2026-10-07", "expect": {"enquiry": true}},
  {"rule": "0e9c7b35", "partySize": 4, "date": "2026-10-08", "expect": {"enquiry": false}},
  {"rule": "0e9c7b35", "partySize": 4, "date": "2026-10-04", "create": false, "expect": {"enquiry": false}}
]

lock_preorder_edits (rule 7a3d1f96)

Rule: 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.

  • 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."
  • Passing (create): date 2026-10-08 (daysAhead 7, outside the window) with premium x6.
  • Passing (create): date 2026-10-04 with no pre-order line.
  • Violating (amend): date 2026-10-04, stored premium x6, final premium x4: the lines differ.
  • Passing (amend): date 2026-10-04, stored [premium x4, standard x2], final [standard x2, premium x4]: same lines in another order.
  • Passing (amend): date 2026-10-08, stored premium x6, final premium x4: outside the window.

Scenarios - lock_preorder_edits

[
  {"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."}},
  {"rule": "7a3d1f96", "partySize": 6, "date": "2026-10-08", "packages": [["premium-bottomless-brunch", 6]], "expect": {"violation": null}},
  {"rule": "7a3d1f96", "partySize": 6, "date": "2026-10-04", "expect": {"violation": null}},
  {"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."}},
  {"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}},
  {"rule": "7a3d1f96", "partySize": 6, "date": "2026-10-08", "create": false, "original": {"packages": [["premium-bottomless-brunch", 6]]}, "packages": [["premium-bottomless-brunch", 4]], "expect": {"violation": null}}
]

require_deposit (rules e07a4b19 per person, d4b82e60 per booking)

Computation: 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.

Rule e07a4b19: amount 10.00, per person, its own message. Party of 6, date 2026-11-14.

  • Violating: no depositAmount. Required 10.00 x 6 = 60.00, collected 0: BKRUL-N-006 with the rule's own message.
  • Passing: depositAmount 60.00.
  • Violating: depositAmount 59.99. 59.99 + 0.005 = 59.995 < 60.00.
  • Violating: party 8 with depositAmount 60.00. Required 80.00.

Rule d4b82e60: amount 100.00, per booking, limited to the product sunday-lunch, no message. Party of 6.

  • Violating: product sunday-lunch, depositAmount 99.99: BKRUL-N-006, "A deposit of £100.00 is required for this booking (£100.00 per booking)."
  • Passing: depositAmount 100.00 (party size is irrelevant for per: booking).
  • Passing: product bottomless-brunch with no deposit (the rule is limited to sunday-lunch, so it does not apply).
  • Passing (amend): the stored deposit is 100.00.

Scenarios - require_deposit

[
  {"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."}},
  {"rule": "e07a4b19", "partySize": 6, "date": "2026-11-14", "deposit": 60.00, "expect": {"violation": null}},
  {"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."}},
  {"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."}},
  {"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)."}},
  {"rule": "d4b82e60", "product": "sunday-lunch", "partySize": 6, "date": "2026-11-14", "deposit": 100.00, "expect": {"violation": null}},
  {"rule": "d4b82e60", "product": "bottomless-brunch", "partySize": 6, "date": "2026-11-14", "expect": {"violation": null}},
  {"rule": "d4b82e60", "product": "sunday-lunch", "partySize": 6, "date": "2026-11-14", "create": false, "deposit": 100.00, "expect": {"violation": null}}
]

booking_window

Computation: hours = (reservation start - now), real elapsed time, with the reservation start = booking date + start time in Europe/London. Too close when hours < minHoursBefore; too far ahead (book only) when hours > maxHoursBefore. On amend the amend window is measured from the start stored on the booking, and the book window from the new start (only when the date or time changes). The operator's own advance-booking limits on the booking type still apply on top: a rule can only narrow them.

A rule with action cancel and minHoursBefore 120, reservation 2026-10-25 19:00 (the clocks go back at 02:00 that morning):

  • 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."
  • 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).
  • Passing: the booking is still a pending request.

A rule with action amend and minHoursBefore 240, stored reservation 2026-11-05 19:00, now 2026-10-30 12:00:

  • Violating: moving it to 2026-12-20 (the stored start is 151 hours away): BKRUL-N-007.

A rule with action book, minHoursBefore 48 and maxHoursBefore 2400:

  • Violating: a booking for 30 hours ahead: AVAIL-N-007. A booking for 2,500 hours ahead: AVAIL-N-008.
  • Passing (amend): a notes change on a confirmed booking now 30 hours away (date and time unchanged).

Expression rules: the same method, at booking.amend (rule 1c6e9b48)

An expression rule is evaluated by building the fact document of its hook, evaluating when, then applying then (see the language reference). Rule 1c6e9b48 runs when you PUT or PATCH a booking: it rejects an amend that leaves no premium brunch line when the stored booking had one. At booking.amend, booking.originalItems holds the lines stored before the amend and booking.items the lines the booking will have after it.

  • 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."
  • Passing: stored [premium x6, standard x4], final [premium x10]. none(booking.items, ...) is false.
  • Passing: stored [standard x10], final [standard x6]. The stored booking had no premium line, some is false.

Checks - expression at booking.amend

[
  {"rules": ["1c6e9b48"], "input": {"create": false, "partySize": 10, "original": {"packages": [["premium-bottomless-brunch", 6], ["standard-bottomless-brunch", 4]]}, "packages": [["standard-bottomless-brunch", 10]]},
   "expect": {"rejects": true, "applied": ["1c6e9b48"]}},
  {"rules": ["1c6e9b48"], "input": {"create": false, "partySize": 10, "original": {"packages": [["premium-bottomless-brunch", 6], ["standard-bottomless-brunch", 4]]}, "packages": [["premium-bottomless-brunch", 10]]},
   "expect": {"rejects": false, "applied": []}},
  {"rules": ["1c6e9b48"], "input": {"create": false, "partySize": 6, "original": {"packages": [["standard-bottomless-brunch", 10]]}, "packages": [["standard-bottomless-brunch", 6]]},
   "expect": {"rejects": false, "applied": []}}
]

Expression language reference

when is one expression tree; the rule applies when it evaluates to the boolean true.

Node forms. A literal (number, string, true, false, null); a list of nodes (["a", "b"]); {"var": "<path>"} reads a fact by dotted path (object members by name, list elements by zero-based index); {"var": ["<path>", <default>]} supplies a default for a missing or null fact; {"var": ""} is the current element inside a list predicate; or {"<operator>": [<args>]} (a single non-array argument is accepted: {"!": x}). A list-valued single argument of count, sum or distinct is passed unwrapped: {"count": {"var": "booking.items"}}. An object literal cannot be written. Numbers are exact decimals.

GroupOperatorsSemantics
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.
Comparison> >= < <=All operands numbers, or all strings (ordinal comparison, so ISO dates compare correctly). Three operands mean "between": {"<=": [1, {"var": "booking.partySize"}, 8]}.
Logicand or ! ifOperands 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.
Arithmetic+ - * / % min maxNumbers only. - with one operand negates. Division or modulo by zero skips the rule.
Membershipin{"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.
List predicatesfilter 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.
Aggregatescount sum distinct pluckcount 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).

Limits (an expression that exceeds them is skipped at booking time): depth 12, 96 nodes, at most 8 actions in then, lists of at most 200 elements, 2000 elementary operations per evaluation.

Actions (then, 1 to 8, each an object with exactly one key): {"set": {"path": "booking.type", "value": "request"}} writes the value (a literal, or an expression evaluated like when) to a path that is writable at the hook (hooks[].writablePaths); {"reject": {"message": "..."}} refuses the booking (the message is optional; ignored at the payment hook); {"flag": {"name": "vip"}} attaches a named annotation with no behaviour of its own.

What booking.type = request plus booking.enquiryReason = operatorRule means for you. The booking is not confirmed instantly: it is created as an enquiry (type request) that the operator has to accept, whatever type you sent. enquiryReason = operatorRule marks that an operator rule, not availability or your own enquiry policy, made that decision (the Collins integration then skips its per-partner enquiry-policy checks). No error is returned. How other table management systems treat an enquiry: not verified beyond Collins. The route_to_enquiry effect does the same without an expression.

Expression cheat-sheet

The ten patterns operators use most, each as a complete rule fragment (when + then) and its plain-English reading. Facts are the dotted paths of hooks[].facts[]. The checks at the end are executed by the platform's test-suite against the real expression engine, so every pattern here is proven. Use them as templates to READ rules; you never send a rule.

Pattern 1 - Compare a fact with a number

{"when": {">": [{"var": "booking.partySize"}, 8]},
 "then": [{"reject": {"message": "Parties of more than 8 cannot book online."}}]}

Reading: reject when the party size is greater than 8 (a party of exactly 8 passes).

Pattern 2 - Is a fact one of a set of values

{"when": {"in": [{"var": "product.slug"}, ["bottomless-brunch", "sunday-lunch"]]},
 "then": [{"reject": {"message": "This product cannot be booked through this channel."}}]}

Reading: 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.

Pattern 3 - Count the pre-order lines

{"when": {"==": [{"count": {"var": "booking.items"}}, 0]},
 "then": [{"reject": {"message": "A pre-order is required."}}]}

Reading: reject when there is no pre-order line. count of booking.items equals the fact booking.itemCount; it counts lines, not quantities.

Pattern 4 - Sum the quantities of selected items

{"when": {">": [
   {"sum": {"pluck": [{"filter": [{"var": "booking.items"}, {"==": [{"var": "kind"}, "package"]}]}, "quantity"]}},
   {"var": "booking.partySize"}]},
 "then": [{"reject": {"message": "More packages than guests."}}]}

Reading: 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.

Pattern 5 - Check the partner category

{"when": {"in": [1, {"var": "partner.categoryIds"}]},
 "then": [{"reject": {"message": "Not available to this partner category."}}]}

Reading: reject when partner category 1 is among the categories of the calling partner for the rule's operator (caller.categoryIdsByOperator[rule.operatorId]).

Pattern 6 - Days-ahead window

{"when": {"and": [{">=": [{"var": "booking.daysAhead"}, 0]}, {"<": [{"var": "booking.daysAhead"}, 7]}]},
 "then": [{"reject": {"message": "Bookings less than 7 days ahead are not accepted."}}]}

Reading: reject when the booking date is today or within the next 6 days (0 <= daysAhead < 7); a date 7 days ahead passes.

Pattern 7 - Null-safe default

{"when": {">": [{"var": ["booking.commission", 0]}, 15]},
 "then": [{"reject": {"message": "Commission above 15 is not accepted."}}]}

Reading: {"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.

Pattern 8 - Boolean combination

{"when": {"and": [
   {">": [{"var": "booking.partySize"}, 10]},
   {"!": {"var": "booking.hasNotes"}},
   {"!": {"var": "booking.hasCustomerCompany"}}]},
 "then": [{"reject": {"message": "Large parties must give notes or a company."}}]}

Reading: 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.

Pattern 9 - Between (inclusive)

{"when": {"<=": [5, {"var": "booking.partySize"}, 8]},
 "then": [{"reject": {"message": "Parties of 5 to 8 must call the venue."}}]}

Reading: three operands chain: 5 <= partySize and partySize <= 8, both ends included.

Pattern 10 - Actions: set and flag (an enquiry instead of a refusal)

{"when": {">=": [{"var": "booking.partySize"}, 12]},
 "then": [
   {"set": {"path": "booking.type", "value": "request"}},
   {"set": {"path": "booking.enquiryReason", "value": "operatorRule"}},
   {"flag": {"name": "large-party"}}]}

Reading: 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.

Checks - cheat-sheet

[
  {"pattern": 1, "input": {"partySize": 9}, "expect": {"applies": true}},
  {"pattern": 1, "input": {"partySize": 8}, "expect": {"applies": false}},
  {"pattern": 2, "input": {"product": "sunday-lunch"}, "expect": {"applies": true}},
  {"pattern": 2, "input": {"product": "afternoon-tea"}, "expect": {"applies": false}},
  {"pattern": 2, "input": {"product": null}, "expect": {"applies": false}},
  {"pattern": 3, "input": {}, "expect": {"applies": true}},
  {"pattern": 3, "input": {"packages": [["premium-bottomless-brunch", 1]]}, "expect": {"applies": false}},
  {"pattern": 4, "input": {"partySize": 4, "packages": [["premium-bottomless-brunch", 3], ["standard-bottomless-brunch", 2]]}, "expect": {"applies": true}},
  {"pattern": 4, "input": {"partySize": 4, "packages": [["premium-bottomless-brunch", 2], ["standard-bottomless-brunch", 2]]}, "expect": {"applies": false}},
  {"pattern": 5, "input": {"categories": [1]}, "expect": {"applies": true}},
  {"pattern": 5, "input": {"categories": [5]}, "expect": {"applies": false}},
  {"pattern": 5, "input": {"categories": []}, "expect": {"applies": false}},
  {"pattern": 6, "input": {"date": "2026-10-04"}, "expect": {"applies": true}},
  {"pattern": 6, "input": {"date": "2026-10-01"}, "expect": {"applies": true}},
  {"pattern": 6, "input": {"date": "2026-10-08"}, "expect": {"applies": false}},
  {"pattern": 6, "input": {"date": "2026-09-30"}, "expect": {"applies": false}},
  {"pattern": 7, "input": {"commission": 20}, "expect": {"applies": true}},
  {"pattern": 7, "input": {"commission": 10}, "expect": {"applies": false}},
  {"pattern": 7, "input": {"commission": null}, "expect": {"applies": false}},
  {"pattern": 8, "input": {"partySize": 12}, "expect": {"applies": true}},
  {"pattern": 8, "input": {"partySize": 12, "hasNotes": true}, "expect": {"applies": false}},
  {"pattern": 8, "input": {"partySize": 8}, "expect": {"applies": false}},
  {"pattern": 9, "input": {"partySize": 5}, "expect": {"applies": true}},
  {"pattern": 9, "input": {"partySize": 8}, "expect": {"applies": true}},
  {"pattern": 9, "input": {"partySize": 9}, "expect": {"applies": false}},
  {"pattern": 9, "input": {"partySize": 4}, "expect": {"applies": false}},
  {"pattern": 10, "input": {"partySize": 12}, "expect": {"applies": true, "writes": {"booking.type": "request", "booking.enquiryReason": "operatorRule"}, "flags": ["large-party"]}},
  {"pattern": 10, "input": {"partySize": 4}, "expect": {"applies": false}}
]

Errors when a booking is rejected

The booking endpoints answer HTTP 400 with a problem document (schema BookingRuleViolationProblem). violations[] lists every rejecting rule, code / title / detail describe the primary one, and analyticsRuleId / analyticsRuleName identify the primary rule. violations[].ruleId is the same UUID as rules[].id returned by this API. violations[].effect is the snake_case effect (expression for generic rules). Per-violation message text is not repeated inside violations[]: look the rule up (its message, or the default message in the effects table). Example (a party of 6 chose a Premium brunch for 2 and a Standard brunch for 4, while a same_package rule with exclusive: true covers the Premium brunch):

{
  "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"
    }
  ],
  "analyticsRuleId": "9c1d5e72-3b84-4f06-a2d9-17c4e8b05a36",
  "analyticsRuleName": "Premium brunch - whole table, no mixing",
  "traceId": "0HNJ2BG2TU3BU:00000001"
}

Retrying the same request will fail again (isRetryable is false): change the booking.

GET /booking-rules itself answers its own refusals with the shared ClientError problem document, the same shape as every other endpoint: each refusal carries a catalog code (BKRUL-N-112, BKRUL-N-113, VALID-N-001, OPRTR-N-002, PMAP-N-001, PMAP-N-004, VENUE-N-001, IDENT-N-002, IDENT-N-003, RATE-R-001), isRetryable and traceId; the examples of each response show the exact body. The 401 is the standard bearer challenge and may have no JSON body.

Worked examples

"Today" in all examples is 2026-10-01 (Europe/London). All ids are fictitious.

Example 1 - Terrace, party of more than 8, no pre-order (expression, rejected with BKRUL-N-100)

Rule (hook: booking.create):

{
  "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."}}]
}

Request: POST /venues/17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9/booking with {"partySize": 10, "date": "2026-11-14", "time": "19:00", "type": "book", "spaceId": 4412, ...} and no preorders. spaceId 4412 is the space named "Terrace" at that venue.

Facts used: space.name = "Terrace" (resolved from spaceId), booking.partySize = 10, booking.itemCount = 0 (no pre-order lines). Evaluation: "Terrace" == "Terrace" true, 10 > 8 true, 0 == 0 true, so when is true. Outcome: rejected, HTTP 400, code: BKRUL-N-100, detail: "Parties of more than 8 on the Terrace must include a pre-order.", violations[0].effect = "expression". Variants: party of 8 gives 8 > 8 false (not rejected); party of 10 with one package line gives itemCount 1 (not rejected); no spaceId: space.name is null, null == "Terrace" is a type clash and the rule is skipped (the platform does not reject on the terrace rule), but do not rely on that: the operator's system then picks the area and other checks may apply.

Example 2 - Partner categories under 7 days become enquiries (expression, no error)

Rule (hook: booking.create, priority: 100), aimed at partner categories 1 and 2 through the rule's when:

{
  "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"}}
  ]
}

You are partner 482 in category 1 for operator 17 (caller.categoryIdsByOperator["17"] = [1]). Request: date: "2026-10-04", type: "book". Facts: partner.categoryIds = [1]; booking.daysAhead = 2026-10-04 minus 2026-10-01 = 3. Evaluation: some over [1]: 1 in [1, 2] true; 3 < 7 true; when true. Outcome: not rejected; the booking is created as an enquiry (type: request), the writes being booking.type = "request" and booking.enquiryReason = "operatorRule". With date: "2026-10-08": daysAhead = 7, 7 < 7 is false, so the booking is confirmed normally (exactly the lead time is outside the window). With partner.categoryIds = [5]: some is false, no effect.

Example 3 - Brunch packages may not exceed the party size (expression, rejected with BKRUL-N-100)

Rule (hook: booking.create):

{
  "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."}}]
}

Request: partySize: 4, preorders.packages: the venue package whose slug is premium-bottomless-brunch with quantity: 3 and the package whose slug is standard-bottomless-brunch with quantity: 2. Facts: booking.partySize = 4; booking.items = [{"kind": "package", "slug": "premium-bottomless-brunch", "quantity": 3}, {"kind": "package", "slug": "standard-bottomless-brunch", "quantity": 2}]. Evaluation: filter keeps both lines, pluck gives [3, 2], sum = 5, 5 > 4 true. Outcome: rejected, BKRUL-N-100, "You cannot select more brunch packages than guests.". With quantities 2 and 2: 4 > 4 false, accepted by this rule. Inside filter, {"var": "slug"} is the slug of the current line; booking.partySize is not reachable inside the predicate (it is used outside it).

Example 4 - same_package, exclusive (fixed effect, rejected with BKRUL-N-001)

Rule: effect: same_package, params: {"exclusive": true, "wholeGroup": false}, appliesTo.items.packages = [premium-bottomless-brunch], all venues, all products, all partners. Party size 6.

Request A: premium x2 and standard x4. S = {premium}; the chosen lines in S: premium (one distinct item), but the standard line is outside S and exclusive is true. Rejected with BKRUL-N-001; this is the response shown above. Request B: premium x6 only, or premium x2 only: accepted (wholeGroup is false, so the quantity need not equal the party size). Request C: standard x6 only: accepted (no S item chosen, the rule is inert). Request D: premium x2 plus a mocktail package: rejected (the mocktail is outside S); with exclusive: false requests A and D would both be accepted (the lines outside S are ignored) and only choosing two distinct S items (possible when S lists two items) would be rejected.

Example 5 - The final balance is the sum of the minimum-spend bands the booking crosses (expression at payment, no error)

Rule (hook: payment, priority: 100):

{
  "when": {">": [{"var": "payment.bandCount"}, 0]},
  "then": [{"set": {"path": "payment.finalBalance", "value": {"sum": {"pluck": [{"var": "payment.bands"}, "spendValue"]}}}}]
}

The operator configured two minimum-spend bands on the space: "Early" from 18:00 to 19:59 at 50000 pence (GBP 500.00) and "Late" from 20:00 to 23:00 at 80000 pence (GBP 800.00). The booking already exists: 2026-10-05, start 19:00, duration 180 minutes, so it occupies [19:00, 22:00). You (or the venue) ask for its payment options. This is not something you send: the platform builds the facts from the stored booking and the space's policy.

Facts (the payment root; the other roots are those of the stored booking):

{
  "payment": {
    "bands": [
      {"label": "Early", "from": "19:00", "to": "20:00", "spendType": "minimum_spend", "spendValue": 50000, "hireFeeValue": null, "overlapMinutes": 60},
      {"label": "Late", "from": "20:00", "to": "22:00", "spendType": "minimum_spend", "spendValue": 80000, "hireFeeValue": null, "overlapMinutes": 120}
    ],
    "bandCount": 2,
    "minimumSpend": 50000,
    "currency": "GBP"
  }
}

Evaluation: payment.bandCount 2 > 0 is true; pluck of spendValue gives [50000, 80000]; sum = 130000, so the rule writes payment.finalBalance = 130000. Outcome: not rejected, nothing to fix; the amount payable is GBP 1,300.00 (130000 pence). The payment options become "full minimum spend" 130000 and "25% deposit" 32500 (130000 x 0.25). Without the rule they would have been 50000 and 12500 (the minimum spend at the start time only). Read the figures from getPaymentOptions, never compute them yourself. Variants: a booking at 19:00 for 60 minutes occupies [19:00, 20:00), which only touches "Early" (a band that starts exactly when the booking ends is not touched): the sum is 50000, the same as today. If the space had only the "Late" band, a booking at 19:00 for 180 minutes starts in a gap: payment.minimumSpend is null, payment.bands holds "Late" alone (from 20: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 written with payment.bandCount > 1 does not match a single band.

End-to-end walkthrough for an AI agent

Goal: book The Rooftop Kitchen (operator 17) on the Terrace for 10 guests on 2026-10-04 (today is 2026-10-01), with a brunch pre-order. You are partner 482, category 1.

Step 1 - fetch the rules for the venue and the operation: GET /booking-rules?venueId=17%7CX9%7C9d3f0b6e7a1c4e58b2c4f1a60d7e8b93&hook=booking.create. Suppose the operator has just five rules active; the named 200 example walkthrough is then your answer (5 rules, hook booking.create):

Rule id (prefix)EffectWhat it says
9c1d5e72same_package, exclusive: trueIf premium-bottomless-brunch is chosen, nothing else may be chosen. Product bottomless-brunch.
e07a4b19require_deposit, 10.00 per personCarry a deposit of 10.00 x party size. Aimed at you (partnerScope: you).
3f2b8c1eexpression, priority 100Reject: space Terrace, party over 8 and no pre-order line.
5a9e2c74expression, priority 100Partner category 1 or 2 and daysAhead below 7: the booking becomes an enquiry.
b8d3f6a0expression, priority 100Reject: brunch package quantities add up to more than the party size. Aimed at your categories.

Step 2 - the booking you intend to send (POST /venues/17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9/booking; the package ids are the venue-specific ids from GET /venues, whose aggregation slugs are premium-bottomless-brunch and standard-bottomless-brunch):

{
  "partySize": 10, "date": "2026-10-04", "time": "19:00", "type": "book", "spaceId": 4412,
  "preorders": {"packages": [{"id": "5de64c68ce9583458c447544", "quantity": 6}, {"id": "5de64c68ce9583458c447545", "quantity": 4}]}
}

No depositAmount is sent. spaceId 4412 is the space named "Terrace".

Step 3 - derive the facts (howToObtain of each fact): booking.partySize 10; booking.date "2026-10-04"; booking.daysAhead 3 (2026-10-04 minus 2026-10-01); booking.type "book"; booking.enquiryReason "none"; booking.items [premium x6, standard x4]; booking.itemCount 2; booking.depositAmount null; partner.id 482; partner.categoryIds [1]; product.slug "bottomless-brunch"; space.name "Terrace".

Step 4 - evaluate the fixed rules first (expression rules only run if all fixed rules pass):

  • 9c1d5e72 same_package: S = {premium}. The premium line is chosen; the standard line is outside S and exclusive is true: VIOLATED, BKRUL-N-001.
  • e07a4b19 require_deposit: required 10.00 x 10 = 100.00, collected 0: VIOLATED, BKRUL-N-006.
  • 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.

The platform would answer HTTP 400:

{
  "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"
}

Step 5 - change the booking: choose premium x10 only (no standard line) and collect 100.00, then send "depositAmount": 100.00.

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. Expression rules run now, in order (all priority 100, so by rule id):

  • 3f2b8c1e: space.name "Terrace" equals "Terrace" (true); 10 > 8 (true); booking.itemCount is 1, not 0 (false): when is false, nothing happens.
  • 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".
  • b8d3f6a0: filter keeps premium x10, pluck gives [10], sum 10; 10 > 10 is false: nothing happens.

Decision: the booking is accepted and created as an enquiry (type request, no error): the operator has to accept it. To get an instant confirmation instead, book on 2026-10-08 or later (daysAhead 7, and 7 < 7 is false, so 5a9e2c74 does not apply).

Scenarios - end to end (fixed rules)

[
  {"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."}},
  {"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."}},
  {"rule": "9c1d5e72", "partySize": 10, "date": "2026-10-04", "packages": [["premium-bottomless-brunch", 10]], "expect": {"violation": null}},
  {"rule": "e07a4b19", "partySize": 10, "date": "2026-10-04", "packages": [["premium-bottomless-brunch", 10]], "deposit": 100.00, "expect": {"violation": null}}
]

Checks - end to end (expression rules)

[
  {"rules": ["3f2b8c1e", "5a9e2c74", "b8d3f6a0"], "input": {"partySize": 10, "date": "2026-10-04", "space": "Terrace", "packages": [["premium-bottomless-brunch", 10]]},
   "expect": {"rejects": false, "applied": ["5a9e2c74"], "writes": {"booking.type": "request", "booking.enquiryReason": "operatorRule"}}},
  {"rules": ["3f2b8c1e", "5a9e2c74", "b8d3f6a0"], "input": {"partySize": 10, "date": "2026-10-08", "space": "Terrace", "packages": [["premium-bottomless-brunch", 10]]},
   "expect": {"rejects": false, "applied": [], "writes": {}}},
  {"rules": ["3f2b8c1e", "5a9e2c74", "b8d3f6a0"], "input": {"partySize": 10, "date": "2026-10-08", "space": "Terrace"},
   "expect": {"rejects": true, "applied": ["3f2b8c1e"], "writes": {}}}
]

The 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).

Common mistakes

  1. 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).
  2. 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.
  3. 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).
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. Using UTC for daysAhead. It is counted in whole calendar days of the Europe/London date, time of day ignored.
  9. 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.
  10. Parsing summary, description or default messages. They are English text that can change. Branch on effect, params, errorCode and outcomes.
  11. 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.
  12. 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.
  13. Caching the answer across bookings. Rules can change at any time; the 400 of the booking endpoint is the final word.
  14. 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).

Scenarios - mistakes with fixed effects

[
  {"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."}}
]

The scenario above is mistake 5: nothing about the pre-order changes, yet the amend is evaluated on the final state and violates the rule.

Checks - mistakes with expressions

[
  {"rule": {"when": {"some": [{"var": "booking.items"}, {">": [{"var": "quantity"}, {"var": "booking.partySize"}]}]}, "then": [{"reject": {"message": "A line is larger than the party."}}]},
   "input": {"partySize": 2, "packages": [["premium-bottomless-brunch", 3]]}, "expect": {"applies": "skipped"}},
  {"rule": {"when": {"==": [{"var": "space.name"}, "Terrace"]}, "then": [{"reject": {"message": "No Terrace."}}]},
   "input": {"partySize": 4}, "expect": {"applies": "skipped"}},
  {"rule": {"when": {"in": ["a", "banana"]}, "then": [{"flag": {"name": "substring"}}]},
   "input": {"partySize": 4}, "expect": {"applies": true}},
  {"rule": {"when": {">": [{"var": "booking.commission"}, 15]}, "then": [{"reject": {"message": "Commission above 15 is not accepted."}}]},
   "input": {"partySize": 4, "commission": null}, "expect": {"applies": "skipped"}}
]

The 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).

What is not guaranteed

  • Results are a pre-check; the platform may reject for reasons not described here (availability, other validation).
  • Pre-order lines whose package or menu has no aggregation slug are invisible to every rule (the platform drops them before evaluating), so they never trigger nor satisfy a rule.
  • An amend is evaluated on the final state of the booking against every applicable rule, whether or not the amend touches the pre-order: a contact-only amend of a booking that already violates a rule can be rejected.
  • summary, description and default messages are English text generated or written by the operator; do not parse them.
  • Whether the platform evaluates a PUT amend with the pre-order lines stored on the booking or with none (the PUT request body has no preorders field) is not verified; prefer PATCH when you change pre-orders.
  • A payment rule is documented from the platform's code and unit tests. Availability lists the minimum-spend bands a slot crosses (each with its own value, no total) and GET /venues shows the configured rules, so neither is the amount payable. Whether any particular operator has configured a payment rule is only visible in this API's answer, and the amount to pay is only authoritative in the payment options response.
Languages
Servers
Live
https://api.bookabletech.com
Sandbox
https://api-sandbox.bookabletech.com

PartnerBookingRules

Read the booking condition rules that are active for the calling partner.

Operations

List the booking condition rules that are active for the calling partner

Request

Returns every active booking condition rule that applies to the calling partner, across all the operators (venue groups) the partner is linked to (the same operators GET /venues exposes), together with the description of every hook referenced by an expression rule in the response (hooks[]: its facts, how to obtain each fact from your booking request, and its writable paths).

A rule is returned when it is active and it has no partner target, or it targets the calling partner, or it targets one of the calling partner's categories. Rules aimed only at other partners are never returned, and no other partner is identified: appliesTo.partnerScope says whether the rule is aimed at everybody (all_partners), at you (you) or at a category you belong to (your_categories).

appliesTo.venues[] lists only the venues you can see (the same visibility as GET /venues). A rule restricted to venues none of which you can see is not returned; allVenues: false with a non-empty venues[] means "only these of your venues".

The partner is taken from the credentials (or from X-Partner-Reference for Bookable Agents, as on the other partner endpoints). There is no operator in the path. The response is not paginated.

Filters narrow the response and are combined with AND:

  • operatorId: only rules of that operator (venue group).
  • venueId: only rules that apply to that venue. A rule with no venue target applies to every venue; a rule with venue targets is returned only if it lists that venue.
  • effect: only rules with that effect.
  • hook: only rules that apply at that moment of the flow: booking.create returns the rules whose whenApplied contains booking_create (fixed rules that run on create and expression rules at the booking.create hook), booking.amend those whose whenApplied contains booking_amend, payment those whose whenApplied contains booking_payment (only expression rules at the payment hook: they change the amount payable for an existing booking, see "Rules at the payment hook" in the API description).

rules[] is ordered by operatorId, then fixed effects (by the precedence order in the guide: lock_preorder_edits, preorder_required, same_package, max_quantity_party_size, max_quantity, require_deposit, then route_to_enquiry, which never rejects; rules of one effect by rule id), then expression rules by hook (booking.create, then booking.amend, then payment), priority ascending (no priority counts as 100) and rule id, which is the order the platform evaluates expression rules.

Properties whose value is null are omitted from the JSON; arrays and objects that the schema declares required are always present, empty when there is nothing to list.

Read the "Integration guide for AI agents", the effects table, the expression language reference and the worked examples in the API description first.

Responses:

StatusMeaningError code
200The rules (possibly none) and the hooks they reference.
400Unknown 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
401Missing, expired or invalid bearer token.none
403Operator or venue not available to you, or unknown X-Partner-Reference.none, IDENT-N-002, IDENT-N-003
404venueId does not exist.VENUE-N-001
429Rate limit exceeded: retry after Retry-After.RATE-R-001
Security
Live(Required scopes:
venue:read
)
or Sandbox(Required scopes:
venue:read
)
Query
operatorIdinteger(int32)>= 1

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.

Example: operatorId=17
venueIdstring

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).

Examples:
venueId=17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93
venueId=17|X9|9d3f0b6e7a1c4e58b2c4f1a60d7e8b93|b41e07c2d5a9
effectstring(PartnerBookingRuleEffect)

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).

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: effect=require_deposit
hookstring[ 1 .. 100 ] characters

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.

Example: hook=booking.create
Headers
X-Partner-Referencestring

Required for Bookable Agents. Identifies the partner for whom the request is being made. This header must be provided when making requests with the agent:write scope.

Accepts either form:

  • a partner reference, e.g. thefork.com
  • a partner id, e.g. 288 — the same id returned by GET /agent/impersonation-targets

A value matching a known partner reference always resolves as that reference; it is only treated as a partner id when it does not. Most partners have no reference configured, so the partner id is generally the form to use — the one exception being a reference spelled as digits, which still takes precedence over the partner whose id is that number.

X-Correlation-IDstring

An optional ID which you can use to track transactions across multiple systems. It can have any value, but we recommend avoiding . characters. Mirrored back in a response header.

Example: 11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA
curl -i -X GET \
  'https://api.bookabletech.com/booking-rules?operatorId=17&venueId=string&effect=require_deposit&hook=booking.create' \
  -H 'Authorization: Bearer <YOUR_TOKEN_HERE>' \
  -H 'X-Correlation-ID: 11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA' \
  -H 'X-Partner-Reference: string'

Responses

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).

Bodyapplication/json
generatedAtstring(date-time)required

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"
callerobject(PartnerBookingCaller)required

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.

caller.​partnerIdinteger(int32)required

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
caller.​partnerCategoryIdsArray of integers(int32)required

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.

Example: [1]
caller.​categoryIdsByOperatorobjectrequired

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.

Example: {"17":[1,5]}
caller.​categoryIdsByOperator.​property name*Array of integers(int32)additional property
rulesArray of objects(PartnerBookingRule)required

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.

rules[].​idstring(uuid)required

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"
rules[].​operatorIdinteger(int32)required

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
rules[].​namestringrequired

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"
rules[].​descriptionstring or null

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."
rules[].​effectstring(PartnerBookingRuleEffect)required

Kind of rule. The eight fixed effects have fixed semantics (see the effects table and "How to evaluate each fixed effect" in the API description); expression is a generic rule defined by data (hook, when, then, priority). These are the same wire names as violations[].effect in a rejected booking.

  • 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.
  • 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.
  • max_quantity: the total quantity of the set may not exceed params.max. Refuses with BKRUL-N-003.
  • preorder_required: from params.threshold guests a qualifying pre-order line is mandatory. Refuses with BKRUL-N-004.
  • route_to_enquiry: a booking made fewer than params.leadTimeDays days ahead becomes an enquiry instead of being confirmed. Never refuses, create only.
  • lock_preorder_edits: inside params.leadTimeDays days of the booking date pre-orders may not be added or changed. Refuses with BKRUL-N-005.
  • require_deposit: the booking must already carry a deposit of params.amount (per person or per booking). Refuses with BKRUL-N-006.
  • preorder_email: below params.threshold guests the Collins pre-order email is not sent; the pre-order is still posted. Never refuses.
  • 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).
  • 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"
rules[].​activebooleanrequired

Always true: inactive rules are never returned by this API.

Example: true
rules[].​appliesToobject(PartnerBookingRuleScope)required

Where and for whom the rule applies. Dimensions combine with AND: the rule applies to a booking at a venue in venues (or any, if allVenues), for a product in products (or any, if allProducts), by a partner matching partnerScope. items is the set the effect operates on (it is not a scope test). Names are the current catalog names. A target that no longer exists in the operator's catalog is not listed (the platform ignores it too); a rule whose venue targets have all disappeared, or none of which you can see, is not returned.

rules[].​appliesTo.​allVenuesbooleanrequired

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
rules[].​appliesTo.​venuesArray of objects(PartnerBookingRuleVenue)required

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.

rules[].​appliesTo.​venues[].​idstringrequired

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"
rules[].​appliesTo.​venues[].​namestring or nullrequired

Current display name of the venue (informational; may be null when the venue has no name).

Example: "The Rooftop Kitchen"
rules[].​appliesTo.​allProductsbooleanrequired

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
rules[].​appliesTo.​productsArray of objects(PartnerBookingRuleCatalogItem)required

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).

rules[].​appliesTo.​products[].​idinteger(int32)required

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
rules[].​appliesTo.​products[].​slugstring or null

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"
rules[].​appliesTo.​products[].​namestring or nullrequired

Current name of the catalog item (informational; may be null).

Example: "Bottomless Brunch"
rules[].​appliesTo.​itemsobject(PartnerBookingRuleItems)required

The set S of packages and menus (compared by slug) that the effect works on. How it is used depends on the effect: same_package: the set whose members may not be mixed / that triggers exclusivity (needs at least one item); max_quantity_party_size: the set whose total quantity is counted (EMPTY means every package and menu counts); max_quantity: the set whose total quantity is counted; preorder_required: the pre-order lines that qualify (EMPTY means any package or menu qualifies); route_to_enquiry, lock_preorder_edits, require_deposit: not used (any items listed have no effect); expression: informational only, never evaluated.

rules[].​appliesTo.​items.​packagesArray of objects(PartnerBookingRuleCatalogItem)required

Packages in S. A chosen package line is in S when its aggregation slug equals one of these slugs. Empty when the set has no package.

rules[].​appliesTo.​items.​packages[].​idinteger(int32)required

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
rules[].​appliesTo.​items.​packages[].​slugstring or null

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"
rules[].​appliesTo.​items.​packages[].​namestring or nullrequired

Current name of the catalog item (informational; may be null).

Example: "Bottomless Brunch"
rules[].​appliesTo.​items.​menusArray of objects(PartnerBookingRuleCatalogItem)required

Menus in S. A chosen menu line is in S when its aggregation slug equals one of these slugs. Empty when the set has no menu.

rules[].​appliesTo.​items.​menus[].​idinteger(int32)required

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
rules[].​appliesTo.​items.​menus[].​slugstring or null

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"
rules[].​appliesTo.​items.​menus[].​namestring or nullrequired

Current name of the catalog item (informational; may be null).

Example: "Bottomless Brunch"
rules[].​appliesTo.​partnerScopestringrequired

How the rule is aimed at the calling partner. Only these three values exist: rules aimed only at other partners are never returned and no other partner is ever identified.

  • all_partners: the rule has no partner target; it applies to every partner.
  • you: the rule names your partner id.
  • your_categories: the rule names a partner category you belong to (and does not name your partner id). If 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"
rules[].​conditionsArray of objects(PartnerBookingRuleCondition)required

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.

rules[].​conditions[].​fieldstringrequired

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"
rules[].​conditions[].​operatorstringrequired

How field is compared with value:

  • equals: same value (numbers compare by value, so 8 equals 8.0).
  • not_equals: different value (still false when the booking does not carry the field).
  • gte: the field is a number greater than or equal to value (a non-number never matches).
  • lte: the field is a number less than or equal to value (a non-number never matches).
  • in: the field value is one of the values of the array value.
  • contains: the field value is a list that contains value.
Enum"equals""not_equals""gte""lte""in""contains"
Example: "gte"
rules[].​conditions[].​valueanyrequired

The value to compare with, any JSON value (an array for in). Its kind must suit the operator.

Example: 8
rules[].​paramsobject(PartnerBookingRuleParams)required

Parameters of a fixed effect; only the keys of the rule's effect are present, the others are omitted. Defaults are already applied. Expression rules (and max_quantity_party_size without perItem) have an empty object.

rules[].​params.​exclusiveboolean or null

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
rules[].​params.​wholeGroupboolean or null

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
rules[].​params.​maxinteger or null(int32)>= 1

max_quantity only. Maximum total quantity of the set per booking (inclusive: exactly max is allowed).

Example: 4
rules[].​params.​perItemboolean or null

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
rules[].​params.​thresholdinteger or null(int32)>= 1

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
rules[].​params.​leadTimeDaysinteger or null(int32)>= 0

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
rules[].​params.​amountnumber or null(double)>= 0.01

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
rules[].​params.​perstring or null

require_deposit only. What amount is measured against:

  • person (the default): the required deposit is amount x party size.
  • booking: the required deposit is amount, flat, whatever the party size.
Enum"person""booking"
Example: "person"
rules[].​params.​actionstring or null

booking_window only. The operation the window limits:

  • book: POST /venues/{compositeId}/booking, and an amend that changes the date or time (checked against the new start).
  • amend: PUT / PATCH /venues/bookings/{bookingId} on a confirmed booking (checked against the stored start).
  • cancel: DELETE /venues/bookings/{bookingId} on a confirmed booking.
Enum"book""amend""cancel"
Example: "cancel"
rules[].​params.​minHoursBeforeinteger or null(int32)>= 1

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
rules[].​params.​maxHoursBeforeinteger or null(int32)>= 1

booking_window with action book only. A booking is refused when the reservation starts more than this many hours away.

Example: 2400
rules[].​messagestring or null

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."
rules[].​summarystringrequired

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."
rules[].​whenAppliedArray of strings(PartnerBookingRuleMoment)non-emptyuniquerequired

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).

Items Enum"booking_create""booking_amend""booking_payment""booking_cancel"
Example: ["booking_create","booking_amend"]
rules[].​consequencestring(PartnerBookingRuleConsequence)required

What the platform does when the rule is violated or triggers, and so what you should do:

  • 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.
  • 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.
  • 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.
  • 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.
  • 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"
rules[].​errorCodestring or null^(BKRUL-N-(00[1-8]|100)|AVAIL-N-00[78])$

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).

Example: "BKRUL-N-001"
rules[].​hookstring or null

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"
rules[].​priorityinteger or null(int32)

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
rules[].​whenobject or null

Expression rules only (omitted for fixed effects): the condition, one boolean expression tree in JsonLogic format, exactly as the platform evaluates it. The rule applies when it evaluates to true. Read facts with {"var": "<path>"}; build the fact document from hooks[].facts[].howToObtain and evaluate with the operator reference in the API description (comparison, logic, arithmetic, in, list predicates filter / map / some / all / none where var is the element, aggregates count / sum / distinct / pluck; limits depth 12 and 96 nodes). A fact that is missing or of the wrong type for an operator makes the rule NOT apply (skipped), it never fails the booking. Example: {"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]}]}
rules[].​thenArray of objects or null<= 8 items

Expression rules only (omitted for fixed effects): the actions applied when when is true, in order. Each action is an object with exactly one key: {"set": {"path": "booking.type", "value": "request"}} (writes a literal or an expression to a path that is writable at the hook; several sets on a path: the last wins), {"reject": {"message": "..."}} (refuses the booking with BKRUL-N-100; the message is optional; ignored at the payment hook) or {"flag": {"name": "vip"}} (a named annotation with no behaviour). outcomes gives the same information already parsed.

Example: [{"reject":{"message":"Parties of more than 8 on the Terrace must include a pre-order."}}]
rules[].​outcomesobject or null(PartnerBookingRuleOutcomes)

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.

hooksArray of objects(PartnerBookingHook)required

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.

hooks[].​namestringrequired

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"
hooks[].​descriptionstringrequired

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."
hooks[].​triggeredByArray of stringsrequired

The endpoint calls that run this hook. For payment it lists the calls that compute the amount payable of an existing booking.

Example: ["POST /venues/{compositeId}/booking"]
hooks[].​writablePathsArray of objects(PartnerBookingHookWritablePath)required

The only paths a set action may write at this hook. Empty = the hook can only reject or flag.

hooks[].​writablePaths[].​pathstringrequired

Dotted path that a set action may write (also readable as a fact once written).

Example: "booking.type"
hooks[].​writablePaths[].​typestringrequired

Type of the value written:

  • number: a JSON number.
  • string: a JSON string.
  • bool: true or false.
  • list: a JSON array.
  • object: a JSON object.
Enum"number""string""bool""list""object"
Example: "string"
hooks[].​writablePaths[].​allowedValuesArray of stringsrequired

The values the platform accepts at this path; any other value is ignored.

Example: ["book","request"]
hooks[].​writablePaths[].​meaningstringrequired

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."
hooks[].​factsArray of objects(PartnerBookingHookFact)required

Every fact an expression can read at this hook, with how to compute it from your request.

hooks[].​facts[].​pathstringrequired

Dotted path for {"var": ...}. Absolute (booking.partySize) at the top level of facts; relative to the element (slug) inside elementFields.

Example: "booking.partySize"
hooks[].​facts[].​typestringrequired

Type of the value:

  • number: a decimal number.
  • string: a JSON string.
  • bool: true or false (never a number or string).
  • list: an array; see isList, elementType and elementFields for its elements.
  • object: a nested object (not used by the facts of today's hooks).
Enum"number""string""bool""list""object"
Example: "number"
hooks[].​facts[].​isListbooleanrequired

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
hooks[].​facts[].​nullablebooleanrequired

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
hooks[].​facts[].​descriptionstringrequired

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)"
hooks[].​facts[].​howToObtainstringrequired

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."
hooks[].​facts[].​elementTypestring or null

For a list of scalars, the type of its elements; omitted otherwise.

  • number: every element is a number (for example partner.categoryIds).
  • string: every element is a string.
  • bool: every element is true or false.
Enum"number""string""bool"
Example: "number"
hooks[].​facts[].​elementFieldsArray of objects or null(PartnerBookingHookFact)

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.

Response
application/json
{ "generatedAt": "2026-10-01T09:30:12Z", "caller": { "partnerId": 482, "partnerCategoryIds": [ … ], "categoryIdsByOperator": { … } }, "rules": [ { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … }, { … } ], "hooks": [ { … }, { … }, { … } ] }