Booking Options
A booking option is one way an experience can be sold. A shop running a class might sell "Group class", "Semi-private" and "Private session" from the same experience, each with its own prices, its own party size and sometimes its own length.
Every experience has at least one booking option. A shop that sells its experience one way has exactly one, and nothing about its bookings or its booking page changes.
Endpointsโ
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/experiences/{id}/booking-options | List booking options |
| GET | /v1/experiences/{id}/booking-options/{optionId} | Get booking option |
| POST | /v1/experiences/{id}/booking-options | Create booking option |
| PATCH | /v1/experiences/{id}/booking-options/{optionId} | Update booking option |
| PATCH | /v1/experiences/{id}/booking-options/{optionId}/archived | Archive or restore |
| POST | /v1/experiences/{id}/booking-options/reorder | Reorder booking options |
Authenticationโ
Booking options ride on the experience scopes you already have. experiences:read reads them, experiences:write creates, edits, reorders and archives them, the same way those scopes cover an experience's sessions and its pricing. A key that can manage experiences can manage how they are sold, with nothing to grant.
The Booking Option Objectโ
| Field | Type | Description |
|---|---|---|
id | integer | Unique booking option identifier |
experience_id | integer | The experience this option belongs to |
name | string | The shop's own words for this way of booking, for example Private session |
price_structure | string | participant (a price per person) or group (one price for the whole party) |
exclusive | boolean | true when booking this takes the whole session, so nobody else can join it |
unit_name | string|null | What each group is under this option ("canoe"). null follows the experience's pricing.unit_name |
unit_name_plural | string|null | Its plural ("canoes"). null when unit_name is null |
asks_headcount | boolean | false when booking this way needs only the number of units (no people limit per unit, no per-person charge or details) |
min_people | integer|null | Smallest party this option takes. null = no minimum of its own |
max_people | integer|null | Largest party this option takes. null = no limit of its own |
default_groups_capacity | integer|null | How many groups one session sells under this option. null = use the experience's own number. Ignored while exclusive is true, because booking out the session always means one group |
min_groups_per_booking | integer|null | Response only. Minimum group quantity for this option; null means no minimum |
below_min_groups_fee | number|null | Response only. Fee for booking fewer than the minimum groups. A minimum with a null fee blocks smaller bookings |
below_min_groups_fee_label | string|null | Response only. Name of the small-booking fee; null uses the standard label |
volume_discount_from_group | integer|null | Response only. First group receiving the multi-group discount; null means no discount |
volume_discount_percent | number|null | Response only. Percentage discount on each group from volume_discount_from_group onward |
show_per_person_price | boolean | Response only. Whether to display an approximate price per person; charges remain per group |
duration | object | { "value": number|null, "unit": string|null }. This option's own length. Both null means it runs for as long as the experience says |
sort_order | integer | Display position. Lists already come back in this order |
archived | boolean | true when the shop has stopped offering this option |
archived_at | string|null | ISO 8601 datetime the option was archived |
created_at | string | ISO 8601 datetime |
updated_at | string | ISO 8601 datetime |
Group pricing rulesโ
Read these response fields from the selected option when building quantity pickers or showing fees. Do not fall back to the experience's group rules when an option returns null. Options that book out the session return no minimum-group fee or multi-group discount.
The existing experience group-rule settings update the default option. New secondary options do not inherit its minimum-group fee or discount. The response-only fields above are not accepted by the booking-option create or update endpoints.
min_people and max_people limit people in a party; min_groups_per_booking counts group units. A small-booking fee applies to the group-unit minimum and does not waive the party-size minimum. Participant-priced bookings combine all rate-category lines for the same option and session when checking party size.
price_structure and exclusive are separate questionsโ
They answer different things and neither one implies the other. A semi-private session is priced per person and still books out the whole session.
To ask "can anyone else join this?", read exclusive. To ask "how is this priced?", read price_structure.
Where options show up elsewhereโ
Three responses carry the option so an app never has to guess which one a booking or a session belongs to.
| Response | Field | Meaning |
|---|---|---|
| Get experience | booking_options | The ways this experience is sold, archived ones included, so an existing booking's option can still be named. Filter on archived before offering a choice. Only the single-experience endpoint carries it, not List experiences |
| Get booking experience line | booking_option_id, booking_option_label | The option the line was sold as, plus the label as it read at the time |
| Schedules | booking_option_id | The option this session runs for. null = it runs for every option |
| List pricing | booking_option_id | The option a price row belongs to. null = it prices the experience's first option |
Reading the label on a bookingโ
booking_option_id is always there once a line has an option. booking_option_label is not: it comes
back null unless the shop actually sells that experience more than one way.
Every experience carries one option row, including in a shop that has never opened the screen. If the label came back for those too, an app would print "Standard" on every booking the shop has ever taken, as though the customer had chosen it. So the field answers the question a person is really asking - which way was this sold? - and stays quiet when there was nothing to choose.
One exception is worth knowing. A shop that retires an option is back to selling one way, and the bookings it already took on the retired option still come back with their label, so an old booking can still be named.
Use booking_option_id when you are reconciling rows, and booking_option_label when you are showing
something to a person.
The label on a booking line is a snapshot taken when the booking was taken. Renaming an option, or archiving it, never rewrites what last month's booking says.
Selling on an optionโ
Name the option on the experience line and the booking is priced from that option's own price rows:
curl -X POST "https://app.equipdash.com/api/v1/bookings" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"booking_type": "experience",
"customer_id": 1234567,
"experiences": [
{
"experience_id": 1234567,
"schedule_id": 7654321,
"booking_option_id": 4321,
"quantity": 2
}
]
}'
Leave booking_option_id out and the experience's first offered option is used, which is what every booking did before options existed. The same field is accepted on add_experiences[] when you update a booking.
When the option you name has no price of its own, the request is refused with 422 booking_creation_failed and a message naming the option, rather than charging another option's price.
Pricing an optionโ
A new option has no price until you give it one. Name it on Add pricing and the row belongs to that option alone:
curl -X POST "https://app.equipdash.com/api/v1/experiences/1234567/pricing" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pricing_type": "group",
"price": 180.00,
"booking_option_id": 4321
}'
Leave booking_option_id out and the row prices the experience's first option, which is what every price row written before options existed does. List pricing takes the same field as a query parameter, so you can read back what one way of booking costs.
An id belonging to a different experience comes back 404 resource_not_found rather than being priced against the wrong option.
Reserving a session for one optionโ
A session can be set aside for a single way of booking. Send booking_option_id when you add a schedule or update one:
curl -X POST "https://app.equipdash.com/api/v1/experiences/1234567/schedules" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-03-14",
"start_time": "09:00",
"booking_option_id": 4321
}'
Send null, or leave it out, and the session runs for every option. The id has to belong to the same experience, or the request comes back 422 validation_error.
Notesโ
- A new option lands at the bottom of the list. Use reorder to move it.
- Adding a second live option is what tells the shop's booking page to offer a choice. While an experience has one option, no chooser appears anywhere.
- Options are archived, never deleted through the API, so bookings sold on them keep their names and their price rows.