Skip to main content

Create Booking

Create a new booking with products and/or experiences. Prices are automatically calculated.

POST /v1/bookings

Authenticationโ€‹

Requires a live API key with bookings:write scope. Test keys cannot create bookings.

Request Bodyโ€‹

FieldTypeRequiredDescription
customer_idintegerConditionalID of an existing customer. Required unless the booking holds only store_products: a sale of store products with no customer_id is saved against the account's Walk-in customer (customer.is_walk_in is true in the response). A Walk-in sale cannot hold rentals, experiences, bundles, services, passes or gift cards.
booking_typestringYesrental, experience or store. Send store for a sale of store_products only, which needs no dates. If you send both products and experiences, the booking is created as a combined mixed booking regardless of this value โ€” see Mixed bookings.
start_datestringYesStart date/time (ISO 8601)
end_datestringConditionalEnd date/time (ISO 8601). Optional when duration is supplied (rentals) โ€” it is then computed for you.
durationobjectNoRental only. { "value": number, "unit": "hour"|"day"|"week" }. When provided, end_date is computed from start_date + this duration (parity with the booking widget / POS).
productsarrayConditionalRequired for rental bookings (unless bundles provided)
bundlesarrayNoProduct bundles to add (rental bookings only)
experiencesarrayConditionalRequired for experience bookings
statusstringNoconfirmed (default) or draft. A draft sends nothing to the customer or the shop and fires no booking.created webhook until it is confirmed.
holds_datesbooleanNoDrafts only. true holds the items and seats so nobody else can book them while it is a draft. Default false.
hold_untilstringNoDrafts only, with holds_dates. Last day to hold them (YYYY-MM-DD, the supplier's calendar). Leave out to hold until the draft is confirmed or canceled.
sourcestringNoCustom source label (default: API)
price_channelstringNoonline (default) or counter. counter charges the shop's counter prices. Returned on the booking, and later changes to the booking keep it.
discountbooleanNoEnable booking-level discount
discount_typestringNopercentage or flat
discount_valuenumberNoDiscount amount
security_depositbooleanNoEnable security deposit
security_deposit_amountnumberNoDeposit amount
pickup_location_idintegerNoLocation ID for pickup (see Locations)
custom_chargesarrayNoLines with no product behind them, such as an amount typed on a till keypad. Up to 100 lines, each { "description": string (max 255), "quantity": integer 1-9999 (default 1), "unit_price": number, 0 or more }. They are taxed and discounted like the rest of the booking. A booking that holds only custom charges (and store products) is a Walk-in sale and needs no customer_id.
deliveryobjectNoDeliver to the customer instead. Address only - the charge is derived from the supplier's settings and a fee sent here is ignored. See Quote a Delivery.
return_location_idintegerNoLocation ID for return (see Locations)
participantsarrayNoThe people on the booking, each with their answers to the shop's per-person questions. See Participants Array.
custom_field_valuesobjectNoAnswers to the shop's booking questions, keyed by question ID: { "41": "Hotel pickup" }.
participant_custom_field_valuesobjectNoPer-person answers by position: { "0": { "88": "yes" } }. Position 0 is the customer when they are not in participants, so the positions shift. Send the answers on each person in participants instead.
attributionobjectNoMarketing attribution captured at the source of the booking (Google Ads / UTM). See Attribution Object.
Defined rental durations

Some suppliers restrict rentals to a fixed set of durations (e.g. 2 hours, full day). Check booking_defaults.rental_duration_mode from Get settings โ€” when it is defined, you must either send a duration matching one of booking_defaults.defined_rental_durations, or an end_date that lines up exactly with one of those durations. Otherwise the request returns 422 with error code duration_not_allowed. When the mode is free, send any end_date (or a duration) as normal.

Mixed bookingsโ€‹

Send products and experiences in the same request to create one combined ("mixed") booking โ€” a single reservation, a single payment, one booking.created webhook. The response type is "mixed" and line_types is ["rental", "experience"]. Each line carries its own fulfillment_status and is fulfilled independently (pick up the rental, check in the experience); see Bookings โ†’ Per-Line Fulfillment Status.

A combo bundle (a bundle whose has_experience_items is true) also produces a mixed booking โ€” the experience and rental halves come from inside the bundle rather than top-level products / experiences. See Mixed-bundle combo.

Availability is checked for both halves: the request is rejected if the rental items aren't available for the date range or the experience slot is full โ€” even when the other half is fine.

{
"customer_id": 123,
"booking_type": "rental",
"start_date": "2026-02-15T09:00:00Z",
"end_date": "2026-02-15T17:00:00Z",
"products": [{ "product_id": 10, "quantity": 1 }],
"experiences": [{ "experience_id": 5, "schedule_id": 88, "quantity": 2 }],
"status": "confirmed"
}

Products Arrayโ€‹

FieldTypeRequiredDescription
product_idintegerYesProduct ID
variation_idintegerNoVariation ID
quantityintegerNoQuantity (default: 1)
extrasarrayNoExtras to sell on this line. See Extras Array.
content_variationsobjectNoFor a product whose contents link other products with variations (customer-picked options): map of contents template_item_id โ†’ chosen variation_id, e.g. { "1234": 56 }. The chosen option's stock is set aside for the booking window. Omitted picks hold stock at product level (any option).
pass_codestringNoA customer's pass code. One unit on this line comes to zero and a visit comes off the pass. See Use a pass on a booking.

Bundles Arrayโ€‹

FieldTypeRequiredDescription
bundle_idintegerYesProduct bundle ID
quantityintegerNoBundle quantity (default: 1)
choicesobjectConditionalCombos only. Which option the customer picked per choice group: { "<choice_group>": <bundle_item_id> }, e.g. { "sxs": 502, "watercraft": 503 }. Required when the bundle has choice groups (item.choice_group is set on its items).
durationsobjectConditionalCombos only. Rental window per chosen item, in hours: { "<bundle_item_id>": <hours> }, e.g. { "503": 2 }. Fractional values mean minutes (0.5 = 30 min, 2.5 = 2h 30m). The value must be one of that item's allowed_durations; the matching discount_tiers percentage is applied server-side.
participantsobjectNoCombos only. Headcount per chosen experience item: { "<bundle_item_id>": <headcount> }. Extra heads are charged at the experience's own per-participant rate (never discounted).
schedulesobjectConditionalCombos only. The experience slot per chosen experience item: { "<bundle_item_id>": <experience_schedule_id> }. Required for the experience half (a seat is reserved against that slot's capacity).
variationsobjectNoCombos only. Variation per chosen rental item, when the product has variations: { "<bundle_item_id>": <variation_id> }.
content_variationsobjectNoOptions for parts inside rental kits: { "<bundle_item_id>": { "<template_item_id>": <variation_id> } }. Get the part and option IDs from each bundle item's content_options. The selected option reserves stock for the booked quantity and window. Omitted picks reserve stock at product level.
extrasarrayNoExtras to sell on this bundle. See Extras Array. Any extra offered on an item inside the bundle is offered on the bundle โ€” the server attaches each extra to the first chosen item it applies to.
note

Bundles are only available when the request includes a rental (booking_type: "rental"), or when the bundle is a combo that carries an experience โ€” in which case the booking is created as a mixed booking. At least one product or bundle is required for a rental booking.

Where the selection keys come from

Fetch the bundle first with Get bundle. The choices / durations / participants / schedules / variations keys are all keyed by the bundle item IDs (and choice_group names) returned there. The customer sees a combo total and dollar savings โ€” the underlying discount_tiers percentage is never charged to them directly; it is applied server-side to compute the savings.

Experiences Arrayโ€‹

FieldTypeRequiredDescription
experience_idintegerYesExperience ID
schedule_idintegerYesSchedule slot ID
rate_category_idintegerNoRate category ID. Omit for group-priced experiences.
booking_option_idintegerNoThe way this line is sold, when the shop sells the experience more than one way. See Booking options. Omit it and the experience's first offered option is used. The option decides which price rows apply, so the figure on the line matches what the booking page and the point of sale would charge
quantityintegerNoParticipants for participant pricing, or number of groups for group pricing (default: 1)
start_timestringNoOpen-window experiences only: when this line starts on its day, local HH:MM (for example 09:30). It must fall between the window's opening and the last start that still ends by closing. Each day of a multi-day booking can have its own. Omit it and the line starts when the window opens. Ignored on fixed-time sessions
booked_participantsintegerNoSeats to reserve against the schedule's capacity. For group pricing send the full group block (groups x max participants). Defaults to quantity.
actual_participantsintegerNoGroup pricing only โ€” the actual headcount in the group(s). Drives the optional per-participant charge (price_per_participant on the pricing rule) and the manifest. Defaults to booked_participants when the experience has a per-participant charge. Must not exceed quantity x the group's max_participants.
extrasarrayNoExtras to sell on this line. See Extras Array.
pass_codestringNoA customer's pass code. One person on this line comes to zero and a visit comes off the pass. Per-person prices only. See Use a pass on a booking.
Group pricing with a per-participant charge

When a group-priced experience has price_per_participant set, the line total is quantity x group price + actual_participants x price_per_participant. Always send actual_participants with the real headcount so the customer is charged correctly.

Activities sold per unit (a boat, a canoe, a kart)โ€‹

When a group-priced experience or its booking option has a unit_name (see Get experience), each group is one of those units. On that line quantity is the number of units and booked_participants is the number of people.

Send the people and leave quantity out, and the units are worked out for you: the people divided by the people each unit holds, rounded up, and never below the experience's minimum. Four people in canoes that hold two book 2 canoes.

{
"experiences": [
{ "experience_id": 5, "schedule_id": 88, "booked_participants": 4 }
]
}

Send both and the pair is checked. Too few units for the people, or more units than people, is refused with 422 and nothing is booked. Each refused line's reason is in error.details[], keyed by the line:

{
"error": {
"code": "booking_creation_failed",
"message": "Some items could not be added to the booking",
"details": [
{ "field": "experiences.0", "message": "Canoe trip: 1 canoe fits 2 people." }
]
},
"meta": { "request_id": "req_abc123" }
}

A line with no unit_name works exactly as described above.

Participants Arrayโ€‹

The people on the booking. The customer is always added as one of them; leave them out of the list and they are added first. A person is an existing customer (customer_id) or a name and email, and a name and email that match one of your customers use that customer.

FieldTypeRequiredDescription
customer_idintegerNoOne of your customers
first_namestringNoFirst name, for someone who is not a customer yet
last_namestringNoLast name
emailstringNoEmail address
notesstringNoComments about this person, up to 5000 characters
custom_field_valuesobjectNoThis person's answers, keyed by the question's field_id: { "88": "yes", "89": "34" }. List the questions with Experience questions

Each answer must be for one of your per-person questions that is asked for an item on this booking. Any other ID (a booking question, another account's question, or a question set for a different item) is refused with 422 and nothing is booked:

{
"error": {
"code": "invalid_participant_answers",
"message": "Some answers are for questions this booking does not ask each person.",
"details": [
{ "field": "participants.1.custom_field_values.99", "message": "Question 99 is not asked of each person on this booking." }
]
},
"meta": { "request_id": "req_abc123" }
}

When the same question is answered here and in participant_custom_field_values, the answer here is kept.

{
"participants": [
{ "customer_id": 42, "custom_field_values": { "88": "yes" } },
{ "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "custom_field_values": { "88": "no" } }
]
}

Passes Arrayโ€‹

Passes bought on this booking. Each is issued once the booking is confirmed or paid, and its code is emailed to the customer. A booking of passes alone needs no dates. See Customer passes.

FieldTypeRequiredDescription
pass_idintegerYesThe pass (see Manage passes)
holder_namestringNoName on the pass. Defaults to the customer's name

Gift Cards Arrayโ€‹

Gift cards bought on this booking. They are added to the total after tax: never taxed, never discounted by a promo code, and always paid in full when the rest of the booking takes a deposit. The voucher is created, and its code emailed, once the booking's payments cover the gift cards. A booking of gift cards alone needs no dates.

FieldTypeRequiredDescription
amountnumberYesThe gift card's value, at least 1, in the shop's currency
recipient_namestringNoWho it is for. Required when recipient_email is sent
recipient_emailstringNoWhere the code is sent. Defaults to the customer's email
messagestringNoUp to 500 characters, printed on the gift card email
{
"gift_cards": [
{ "amount": 50, "recipient_name": "Sam Rivera", "recipient_email": "sam.rivera@example.test", "message": "Enjoy" }
]
}

Returns 422 when an amount is below 1. A booking with gift cards is refused while the shop has gift card sales turned off. The booking's gift_cards_sold shows each one with its status.

Extras Arrayโ€‹

Extras are supplier-defined add-ons (helmets, insurance, photo packages) from the Extras catalog. They attach to a specific product or experience line โ€” or ride a bundle, where the server attaches each extra to the first chosen item inside the bundle it applies to โ€” and are always priced server-side from the catalog. You only send the id and quantity.

FieldTypeRequiredDescription
extra_idintegerYesExtra ID (see List Extras)
quantityintegerNoQuantity (default: 1). An extra charged once can only be added once, so send 1 or leave it out

The request returns 422 when an extra is inactive, not offered on that product/experience, belongs to another supplier, or exceeds its remaining stock for the booking window. Each booked extra is stored as a snapshot (name + unit price at booking time), so later catalog edits never change existing bookings. Per-time extras (charge_per = per_time) are priced server-side against the rental window โ€” the booking-line response includes billed_units (periods billed) and time_unit.

{
"products": [
{
"product_id": 1,
"quantity": 1,
"extras": [
{ "extra_id": 3, "quantity": 2 }
]
}
]
}

An extra charged once (a cleaning fee, a photo package) goes on a line once. Asking for more is refused with a field-named 422 under {key}.{i}.extras.{j}.quantity, where {key} is products, experiences or bundles. An extra the customer can take several of is charged per_item, and its max_quantity is null.

{
"error": {
"code": "validation_error",
"message": "The given data was invalid.",
"details": [
{
"field": "products.0.extras.0.quantity",
"message": "\"Photo package\" is charged once per booking, so it can only be added once."
}
]
},
"meta": { "request_id": "req_abc123" }
}

Attribution Objectโ€‹

Optional marketing attribution stored against the booking. The booking widget captures these automatically from the host page; send them here when you create bookings server-side and want the same reporting (e.g. Google Ads offline-conversion uploads). All fields are optional strings; unknown keys are ignored.

FieldTypeDescription
gclidstringGoogle Ads click ID
gbraidstringGoogle Ads click ID (iOS app โ†’ web)
wbraidstringGoogle Ads click ID (web โ†’ app)
fbclidstringMeta (Facebook) click ID
utm_sourcestringCampaign source, e.g. google
utm_mediumstringCampaign medium, e.g. cpc
utm_campaignstringCampaign name
utm_termstringCampaign keyword / term
utm_contentstringCampaign content / ad variant
landing_urlstringLanding-page URL (max 500 chars)
refstringThe shop's own link tag, max 64 chars. Shown as Link tag on the booking. The booking page keeps it automatically when a link or embed carries ?ref=, for example ?ref=summer-flyer. Stored lowercase, with anything other than letters, digits, ., - and _ turned into a dash.
{
"attribution": {
"gclid": "EAIaIQobChMI...",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "summer-rentals",
"landing_url": "https://example.com/book?gclid=EAIaIQobChMI...",
"ref": "summer-flyer"
}
}

Rental Booking Exampleโ€‹

curl -X POST "https://app.equipdash.com/api/v1/bookings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 456,
"booking_type": "rental",
"start_date": "2026-02-15T09:00:00Z",
"end_date": "2026-02-17T17:00:00Z",
"products": [
{
"product_id": 1,
"quantity": 2
},
{
"product_id": 5,
"variation_id": 12,
"quantity": 1
}
],
"source": "Website Integration",
"pickup_location_id": 1,
"return_location_id": 2
}'

Experience Booking Exampleโ€‹

curl -X POST "https://app.equipdash.com/api/v1/bookings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 456,
"booking_type": "experience",
"start_date": "2026-02-15T17:00:00Z",
"end_date": "2026-02-15T19:00:00Z",
"experiences": [
{
"experience_id": 3,
"schedule_id": 78,
"rate_category_id": 12,
"quantity": 2
},
{
"experience_id": 3,
"schedule_id": 78,
"rate_category_id": 13,
"quantity": 1
}
]
}'

Rental Booking with Bundlesโ€‹

Product bundles include multiple related items at a bundled price:

curl -X POST "https://app.equipdash.com/api/v1/bookings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 456,
"booking_type": "rental",
"start_date": "2026-02-15T09:00:00Z",
"end_date": "2026-02-17T17:00:00Z",
"bundles": [
{
"bundle_id": 5,
"quantity": 1
}
],
"products": [
{
"product_id": 10,
"quantity": 1
}
]
}'

Mixed-bundle comboโ€‹

A combo bundle (one whose has_experience_items is true) pairs an experience with a rental. Pass the customer's picks in the bundle's selection keys โ€” choices, durations, participants, schedules, variations. Because the combo carries an experience, the booking is created as a mixed booking even though you send booking_type: "rental".

This example books the Biff Rentals "Discovery Tour" combo (bundle 88): the side-by-side excursion option 502 (the X3 MAX experience, schedule slot 3300), paired with the Sea-Doo GTI 130 watercraft (item 503) for a 2h window. The excursion is full price ($499); the watercraft gets the 2h tier discount (20% off $300 โ†’ $240), so the combo total is $739 (a $60 saving). One flat $4,000 security hold covers both.

curl -X POST "https://app.equipdash.com/api/v1/bookings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 456,
"booking_type": "rental",
"start_date": "2026-07-15T09:00:00Z",
"bundles": [
{
"bundle_id": 88,
"quantity": 1,
"choices": { "sxs": 502, "watercraft": 503 },
"durations": { "503": 2 },
"participants": { "502": 2 },
"schedules": { "502": 3300 }
}
]
}'
KeyValueMeaning
choices.sxs502Pick the X3 MAX side-by-side (excursion experience).
choices.watercraft503Pick the Sea-Doo GTI 130 watercraft (rental).
durations.5032Rent the watercraft for 2 hours โ†’ the discount_tiers["2"] = 20 % tier applies to the rental half.
participants.50222 people on the side-by-side; heads beyond the included one are charged the experience's per-head rate (never discounted).
schedules.5023300The experience slot the excursion is reserved against.

The customer sees the combo total and dollar savings: $499 (excursion, full price) + $240 (watercraft, 20% off $300) = $739, saving $60 off the standalone $799. The 20% itself is internal. The bundles[] block in the response carries the resolved original_price, bundle_discount, final_price, and the snapshotted security_deposit_amount.

{
"data": {
"id": 901,
"type": "mixed",
"line_types": ["rental", "experience"],
"status": "confirmed",
"payment_status": "unpaid",
"start_date": "2026-07-15T09:00:00+00:00",
"end_date": "2026-07-15T13:00:00+00:00",
"currency": "CAD",
"pricing": {
"subtotal": 739.00,
"discount": { "enabled": false, "type": null, "value": null, "amount": 0 },
"security_deposit": 4000.00,
"total": 739.00
},
"bundles": [
{
"id": 77,
"bundle_id": 88,
"bundle_name": "Discovery Tour",
"quantity": 1,
"original_price": 799.00,
"bundle_discount": 60.00,
"final_price": 739.00,
"security_deposit_amount": 4000.00
}
],
"products": [
{
"line_id": 610,
"product": { "id": 163, "name": "Sea-Doo GTI 130" },
"variation": null,
"quantity": 1,
"unit_price": 240.00,
"line_total": 240.00
}
],
"experiences": [
{
"line_id": 611,
"experience": { "id": 1260, "name": "Discovery Tour Excursion โ€” X3 MAX" },
"schedule_id": 3300,
"quantity": 2,
"line_total": 499.00
}
],
"created_at": "2026-06-20T10:00:00+00:00"
},
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-06-20T10:00:00+00:00"
}
}
note

durations is in hours (fractional values mean minutes โ€” 0.5 = 30 min, 2.5 = 2h 30m) and must match one of the chosen rental item's allowed_durations (from Get bundle). If you omit end_date, the combo's window is derived from the schedule slot and the chosen rental duration. Sending a duration outside allowed_durations, or omitting a required choices / schedules entry, returns 422.

Combo selection errors (422)โ€‹

When a combo selection is structurally invalid, the booking is not created and the API returns a 422 whose error.details[] name the exact field and the valid options โ€” so you can fix the request without guessing. Errors are namespaced by the array index, e.g. bundles.0.choices.watercraft. (On Update booking the same checks apply under add_bundles.0.โ€ฆ.)

WhenfieldExample message
A required "pick one" group has no chosen optionbundles.{i}.choices.{group}Please choose an option for 'watercraft'. Valid options: 503 (Sea-Doo GTI 130), 504 (Sea-Doo GTR-X 300).
The chosen option isn't part of that groupbundles.{i}.choices.{group}The chosen option (999) is not part of the 'watercraft' group. Valid options: 503 (Sea-Doo GTI 130), 504 (Sea-Doo GTR-X 300).
A rental duration isn't in allowed_durationsbundles.{i}.durations.{bundle_item_id}Duration 3h is not allowed for 'Sea-Doo GTI 130'. Valid durations: 2h, 4h.
An experience option has no chosen time slotbundles.{i}.schedules.{bundle_item_id}Please choose a time for 'X3 MAX (4 seats)'.
Two chosen experience slots overlap in time (one party can't be on two activities at once)bundles.{i}.schedules.{bundle_item_id} of the later item'Snorkel trip' at 9:00 AM overlaps 'Discover dive' at 8:00 AM. Pick a departure that doesn't clash.
{
"error": {
"code": "validation_error",
"message": "The given data was invalid.",
"details": [
{
"field": "bundles.0.durations.503",
"message": "Duration 3h is not allowed for 'Sea-Doo GTI 130'. Valid durations: 2h, 4h."
}
]
},
"meta": { "request_id": "req_abc123" }
}
tip

Use Bundle price preview to validate a selection and see the live combo total before you create the booking โ€” it runs the same structural checks and returns the same field-named 422s, without taking payment or reserving inventory.

Responseโ€‹

Returns 201 Created with the complete booking:

{
"data": {
"id": 123,
"type": "rental",
"line_types": ["rental"],
"status": "confirmed",
"payment_status": "unpaid",
"source": "Website Integration",
"start_date": "2026-02-15T09:00:00+00:00",
"end_date": "2026-02-17T17:00:00+00:00",
"currency": "USD",
"pricing": {
"subtotal": 205.00,
"discount": {
"enabled": false,
"type": null,
"value": null,
"amount": 0
},
"taxes": [
{
"name": "Sales Tax",
"rate": 10,
"amount": 20.50,
"included_in_price": false
}
],
"security_deposit": 0,
"total": 225.50
},
"pickup_location": {
"id": 1,
"name": "Main Store",
"address": "123 Beach Road, Bondi NSW 2026, AU"
},
"return_location": {
"id": 2,
"name": "Manly Branch",
"address": "45 Ocean Street, Shop 2, Manly NSW 2095, AU"
},
"customer": {
"id": 456,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com"
},
"products": [
{
"line_id": 45,
"product": {
"id": 1,
"name": "Mountain Bike"
},
"variation": null,
"quantity": 2,
"unit_price": 60.00,
"line_total": 120.00
},
{
"line_id": 46,
"product": {
"id": 5,
"name": "Kayak"
},
"variation": {
"id": 12,
"name": "Double"
},
"quantity": 1,
"unit_price": 85.00,
"line_total": 85.00
}
],
"created_at": "2026-02-01T10:00:00+00:00"
},
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-02-01T10:00:00+00:00"
}
}

With Discountโ€‹

curl -X POST "https://app.equipdash.com/api/v1/bookings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_id": 456,
"booking_type": "rental",
"start_date": "2026-02-15T09:00:00Z",
"end_date": "2026-02-17T17:00:00Z",
"products": [
{"product_id": 1, "quantity": 1}
],
"discount": true,
"discount_type": "percentage",
"discount_value": 10
}'

What Happens During Creationโ€‹

  1. Customer validation: Customer must belong to your account
  2. Product validation: All products and bundles must exist and be active
  3. Availability check: Sufficient inventory for the date range โ€” or, for a product set to keep selling when sold out, enough of its shortage allowance left
  4. Price calculation: Uses product/bundle pricing rules (flat, tiered, seasonal)
  5. Tax calculation: Applies your configured tax rules
  6. Discount application: Applies any booking-level discount
  7. Participant creation: The customer is added as a participant, then everyone in participants, with their answers

Error Responsesโ€‹

Missing Products/Bundles for Rentalโ€‹

{
"error": {
"code": "validation_error",
"message": "The given data was invalid.",
"details": [
{
"field": "products",
"message": "At least one product or bundle is required for a rental booking."
}
]
}
}

Product Not Availableโ€‹

{
"error": {
"code": "validation_error",
"message": "The given data was invalid.",
"details": [
{
"field": "products.0",
"message": "Insufficient availability for product Mountain Bike. Requested: 5, Available: 3"
}
]
}
}
Products that keep selling when sold out

A product can be set to keep taking bookings past its on-hand stock, up to a small allowance โ€” see Keep selling when sold out. Requests within that allowance are accepted, not rejected, and the resulting rental line reports how many units it sold past stock in shortage_quantity (see Get Booking). Check /v1/availability/products first if you want to know in advance: bookable_quantity is what will be accepted, available_quantity is what is physically on hand.

Invalid Customerโ€‹

{
"error": {
"code": "validation_error",
"message": "The given data was invalid.",
"details": [
{
"field": "customer_id",
"message": "The selected customer does not belong to your account."
}
]
}
}
  1. Check availability first: POST /v1/availability/products/bulk
  2. Get pricing (optional): POST /v1/pricing/calculate
  3. Create booking: POST /v1/bookings
  4. Add payment (optional): POST /v1/bookings/{id}/payments

Notesโ€‹

  • All items are validated before the booking is created (atomic operation)
  • If any item fails validation, the entire booking fails
  • Prices include variation adjustments automatically
  • Bundle prices use the bundle's configured discount rules
  • Taxes are calculated based on your supplier settings
  • Bundles are only available for rental bookings

Bundle rate countsโ€‹

For a bundle using prices by rate type, include rate_counts on each bundles[] entry. This maps offered rate category IDs to counts and replaces per-experience participant counts for that bundle. Keep the existing departure, choice, duration, and variation maps. The booking saves its rate labels, counts, and sold prices so later catalog changes do not reprice it.