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โ
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | integer | Conditional | ID 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_type | string | Yes | rental, 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_date | string | Yes | Start date/time (ISO 8601) |
end_date | string | Conditional | End date/time (ISO 8601). Optional when duration is supplied (rentals) โ it is then computed for you. |
duration | object | No | Rental only. { "value": number, "unit": "hour"|"day"|"week" }. When provided, end_date is computed from start_date + this duration (parity with the booking widget / POS). |
products | array | Conditional | Required for rental bookings (unless bundles provided) |
bundles | array | No | Product bundles to add (rental bookings only) |
experiences | array | Conditional | Required for experience bookings |
status | string | No | confirmed (default) or draft. A draft sends nothing to the customer or the shop and fires no booking.created webhook until it is confirmed. |
holds_dates | boolean | No | Drafts only. true holds the items and seats so nobody else can book them while it is a draft. Default false. |
hold_until | string | No | Drafts 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. |
source | string | No | Custom source label (default: API) |
price_channel | string | No | online (default) or counter. counter charges the shop's counter prices. Returned on the booking, and later changes to the booking keep it. |
discount | boolean | No | Enable booking-level discount |
discount_type | string | No | percentage or flat |
discount_value | number | No | Discount amount |
security_deposit | boolean | No | Enable security deposit |
security_deposit_amount | number | No | Deposit amount |
pickup_location_id | integer | No | Location ID for pickup (see Locations) |
custom_charges | array | No | Lines 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. |
delivery | object | No | Deliver 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_id | integer | No | Location ID for return (see Locations) |
participants | array | No | The people on the booking, each with their answers to the shop's per-person questions. See Participants Array. |
custom_field_values | object | No | Answers to the shop's booking questions, keyed by question ID: { "41": "Hotel pickup" }. |
participant_custom_field_values | object | No | Per-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. |
attribution | object | No | Marketing attribution captured at the source of the booking (Google Ads / UTM). See Attribution Object. |
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โ
| Field | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | Product ID |
variation_id | integer | No | Variation ID |
quantity | integer | No | Quantity (default: 1) |
extras | array | No | Extras to sell on this line. See Extras Array. |
content_variations | object | No | For 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_code | string | No | A 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โ
| Field | Type | Required | Description |
|---|---|---|---|
bundle_id | integer | Yes | Product bundle ID |
quantity | integer | No | Bundle quantity (default: 1) |
choices | object | Conditional | Combos 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). |
durations | object | Conditional | Combos 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. |
participants | object | No | Combos only. Headcount per chosen experience item: { "<bundle_item_id>": <headcount> }. Extra heads are charged at the experience's own per-participant rate (never discounted). |
schedules | object | Conditional | Combos 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). |
variations | object | No | Combos only. Variation per chosen rental item, when the product has variations: { "<bundle_item_id>": <variation_id> }. |
content_variations | object | No | Options 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. |
extras | array | No | Extras 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. |
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.
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โ
| Field | Type | Required | Description |
|---|---|---|---|
experience_id | integer | Yes | Experience ID |
schedule_id | integer | Yes | Schedule slot ID |
rate_category_id | integer | No | Rate category ID. Omit for group-priced experiences. |
booking_option_id | integer | No | The 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 |
quantity | integer | No | Participants for participant pricing, or number of groups for group pricing (default: 1) |
start_time | string | No | Open-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_participants | integer | No | Seats to reserve against the schedule's capacity. For group pricing send the full group block (groups x max participants). Defaults to quantity. |
actual_participants | integer | No | Group 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. |
extras | array | No | Extras to sell on this line. See Extras Array. |
pass_code | string | No | A 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. |
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.
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | integer | No | One of your customers |
first_name | string | No | First name, for someone who is not a customer yet |
last_name | string | No | Last name |
email | string | No | Email address |
notes | string | No | Comments about this person, up to 5000 characters |
custom_field_values | object | No | This 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.
| Field | Type | Required | Description |
|---|---|---|---|
pass_id | integer | Yes | The pass (see Manage passes) |
holder_name | string | No | Name 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.
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | The gift card's value, at least 1, in the shop's currency |
recipient_name | string | No | Who it is for. Required when recipient_email is sent |
recipient_email | string | No | Where the code is sent. Defaults to the customer's email |
message | string | No | Up 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.
| Field | Type | Required | Description |
|---|---|---|---|
extra_id | integer | Yes | Extra ID (see List Extras) |
quantity | integer | No | Quantity (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.
| Field | Type | Description |
|---|---|---|
gclid | string | Google Ads click ID |
gbraid | string | Google Ads click ID (iOS app โ web) |
wbraid | string | Google Ads click ID (web โ app) |
fbclid | string | Meta (Facebook) click ID |
utm_source | string | Campaign source, e.g. google |
utm_medium | string | Campaign medium, e.g. cpc |
utm_campaign | string | Campaign name |
utm_term | string | Campaign keyword / term |
utm_content | string | Campaign content / ad variant |
landing_url | string | Landing-page URL (max 500 chars) |
ref | string | The 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 }
}
]
}'
| Key | Value | Meaning |
|---|---|---|
choices.sxs | 502 | Pick the X3 MAX side-by-side (excursion experience). |
choices.watercraft | 503 | Pick the Sea-Doo GTI 130 watercraft (rental). |
durations.503 | 2 | Rent the watercraft for 2 hours โ the discount_tiers["2"] = 20 % tier applies to the rental half. |
participants.502 | 2 | 2 people on the side-by-side; heads beyond the included one are charged the experience's per-head rate (never discounted). |
schedules.502 | 3300 | The 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"
}
}
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.โฆ.)
| When | field | Example message |
|---|---|---|
| A required "pick one" group has no chosen option | bundles.{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 group | bundles.{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_durations | bundles.{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 slot | bundles.{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" }
}
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โ
- Customer validation: Customer must belong to your account
- Product validation: All products and bundles must exist and be active
- 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
- Price calculation: Uses product/bundle pricing rules (flat, tiered, seasonal)
- Tax calculation: Applies your configured tax rules
- Discount application: Applies any booking-level discount
- 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"
}
]
}
}
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."
}
]
}
}
Recommended Workflowโ
- Check availability first:
POST /v1/availability/products/bulk - Get pricing (optional):
POST /v1/pricing/calculate - Create booking:
POST /v1/bookings - 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.