Create Experience
Create a new experience (tour, activity, class).
Endpointโ
POST /v1/experiences
Authenticationโ
Requires an API key with the experiences:write scope.
Request Bodyโ
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Experience name (max 255 characters) |
unique_code | string | No | Unique code (auto-generated if not provided) |
brief_description | string | No | Short description |
long_description | string | No | Full description |
category_id | integer | No | Primary category ID. Use category_ids for multiple categories. |
category_ids | integer[] | No | Put the experience in multiple categories (primary first). The first id becomes the primary (also returned as category). Takes precedence over category_id. |
status | string | No | active, inactive (default: active) |
duration_value | number | Yes | Duration value (minimum: 0.1; decimals allowed, e.g. 3.5 hours) |
duration_unit | string | Yes | Minutes, Hours, or Days |
price_applies | string | No | yearRound or seasonal (default: yearRound) |
price_structure | string | No | participant or group (default: participant) |
default_capacity | integer | No | Default capacity per session (for private experiences this is the maximum group size) |
is_private | boolean | No | Private or shared? Private = the first booking takes the whole departure. Default true. Set false for shared experiences where different groups fill seats individually until capacity. On per-person pricing it is saved on the experience's booking option (exclusive), and capacity stays seats. |
min_people_per_booking | integer | No | The smallest party the booking site takes (e.g. 10 for a party). Works on per-person pricing, and on a private experience priced per group with no unit name, where it counts people in that one party. A shared experience priced per group is refused: use min_groups_per_booking. Staff can still book fewer in POS. |
list_as_experience | boolean | No | Default true. Set false to sell the experience only on connected marketplaces: it stays active and fully manageable, but is hidden from the booking widget, POS and the shop's own website. |
staff_only | boolean | No | Default false. Set true so only the shop's team can book it: it stays in POS, the app, this API and Dash, and is hidden from the booking widget, embeds and the shop's own website. Only applies while list_as_experience is true. |
booking_mode | string | No | instant (default), request, or flexible_request. request uses published departures. flexible_request lets the customer propose a date and start time. See Booking Requests |
equipment_product_id | integer | No | The rental product (fleet) this experience runs on. Overlapping sessions and rentals share that product's inventory and cannot overbook it. Must belong to your account. |
shares_places_with_id | integer or null | No | Another of your experiences whose places this one uses. A booking on either uses places on both when their sessions overlap, so a party of 10 at 10am leaves 5 of a 15-place session at 10am. null stops sharing. The linked experience is still held to its own limit, and one sold as private groups cannot be picked. |
equipment_units_basis | string | No | How equipment is consumed: booking (one set per customer booking), participant (one set per guest), session (one set for the whole departure), or unit (one set per unit sold, independently of guest count). Default booking. |
equipment_units_per | integer | No | Units consumed per booking, guest, or session. Default 1. |
equipment | array | No | Multi-equipment: the full gear list, superseding the single-product fields. Each row reserves its product from the shared pool for every session. Rows: {product_id (required), units_basis (booking|participant|session|unit), units_per (default 1), rate_category_id (optional), ask_size (optional)}. A row with a rate_category_id is held only by bookings of that price - see Gear per price. ask_size: true asks each person their size of the item - see Asking each person their size |
gear_per_price | boolean | No | true lets each price hold its own gear. Sending any equipment row with a rate_category_id turns it on. false clears the gear set per price and keeps the gear every price uses |
requires_guide | boolean | No | Guide availability link: when true, a time slot only sells while enough of its assigned guides are free โ a guide locked onto an overlapping booked departure is not free. Guides are assigned per schedule (see Schedules โ Assigning guides). Default false. |
guides_needed | integer | No | Default guides each departure needs before it can take bookings. Default 1. Overridable per schedule via guides_needed on the schedule endpoints. Only meaningful when requires_guide is true. |
location | string | No | Where the experience takes place |
min_booking_notice | boolean | No | Require advance booking (default: false) |
min_booking_notice_value | integer | No | Notice period value |
min_booking_notice_unit | string | No | hours, days |
same_day_cutoff_time | string|null | No | Same-day cut-off in the shop's time zone, 24-hour "HH:MM" such as "08:30". Online bookings for that day close at this time; later days are not affected. A session that starts earlier closes at its start. Works alongside the notice length, and the stricter one wins. Sending it turns min_booking_notice on. |
deposit_type | string|null | No | The deposit a customer pays to book this experience: percentage of the total, a fixed amount for the booking, or per_person, an amount for each person booked. Omit or null to use the shop's default deposit |
deposit_value | number|null | No | The percent (up to 100), or the fixed or per-person amount (up to 99999999.99), that goes with deposit_type. A per_person deposit of 50 charges 150 for a party of 3 |
participants_required | boolean|null | No | Require participant details. true = always, false = never, null or omitted = follow the shop-wide default (participant-priced experiences default to always) |
unit_name | string | No | What each group is, for example canoe. Up to 40 characters. Group pricing only. See Priced per unit |
unit_name_plural | string | No | The plural, when the worked-out one is wrong. Up to 40 characters |
default_groups_capacity | integer | No | Group booking rules โ the most groups one time slot can take (e.g. 4 if you own four of the item). Omit or null for unlimited. Ignored unless the experience is group-priced, shared (is_private: false) and not a package. |
min_groups_per_booking | integer | No | Fewest groups a single booking may contain. Pair it with below_min_groups_fee to charge a fee below the minimum, or omit the fee to block smaller bookings outright. |
below_min_groups_fee | number | No | Charged once per experience line when the line has fewer groups than min_groups_per_booking. Omit or null = bookings under the minimum are rejected instead. |
below_min_groups_fee_label | string | No | Customer-facing name for that fee (max 255 characters). Defaults to a localized "Small-group fee". |
volume_discount_from_group | integer | No | Multi-group discount threshold โ groups from this one onward (minimum 2) each get volume_discount_percent off. |
volume_discount_percent | number | No | Percent (0โ100) off each group from volume_discount_from_group onward. Applies to the group base price only โ never to per-participant charges, extras or fees. |
show_per_person_price | boolean | No | Display toggle: also show the approximate per-person price (group price รท people per group) on the booking widget. Display only โ billing stays per group. Default false. |
people_discount_from | integer | No | Group discount on per-person prices - the fewest people on this experience in one booking (minimum 2, every person type counted together) that earns people_discount_percent off. Omit or null for no discount. Does nothing on a price per group. |
people_discount_percent | number | No | Percent (0-100) off every per-person line once a booking reaches people_discount_from people. Send it with people_discount_from. |
required_certifications | string[] | No | Guide requirements - what a guide should hold to run this experience. Flags, never blocks: a guide without one is flagged when you assign them to a departure, and the departure still sells. See Guide requirements for the values. |
required_languages | string[] | No | Guide requirements - what a guide should speak to run this experience, as two-letter codes. Same warn-only behavior. See Guide requirements for the values. |
Group booking rulesโ
Group-priced experiences that share their slots (price_structure: "group", is_private: false, not a
package) can carry booking rules. The rules are enforced everywhere a booking is created โ this API,
the booking widget, the point of sale and the assistant โ so a quote and a charge always agree:
- Minimum groups โ a booking below
min_groups_per_bookingeither paysbelow_min_groups_feeonce, or is rejected with a validation error when no fee is configured. - Multi-group discount โ every group from
volume_discount_from_grouponward getsvolume_discount_percentoff that group's base price. - Groups per slot โ
default_groups_capacitycaps how many groups a departure can hold, independent of participant seats. Override it for a single departure withgroups_capacityon the schedule endpoints.
Both amounts are folded into the experience line's price, and are also returned separately
(below_min_fee_amount, below_min_fee_label, volume_discount_amount) so you can itemize them.
Sending any rule field for an ineligible experience stores null rather than returning an error.
Group discount on per-person pricesโ
An experience priced per person (price_structure: "participant", not a package) can take a
percentage off once a booking brings enough people: people_discount_from: 3 with
people_discount_percent: 10 is 10% off for 3 or more people. Everyone on the experience in the
booking counts, Adults and Children together, and a combo counts every person in it. Every
per-person line of the experience then takes the percentage off its own price.
It applies everywhere a booking is priced: this API's price quote, the booking
widget, the point of sale and the assistant. The amount is folded into each line's price and returned
as that line's volume_discount_amount. A promo code comes off what is left after it. Lines held at a
fixed total (a program price, an OTA payout) never take it.
Priced per unit (a boat, a canoe, a kart)โ
A shared group-priced experience can say what each group is. Set unit_name to canoe and the
experience is sold by the canoe: the group price is the price per canoe, the group's
max_participants on the pricing rule is the people one canoe holds, and default_groups_capacity
is the canoes that go out on each departure. The booking page, the point of sale, Dash and this API
then count in canoes, and a booking that sends only the number of people gets its canoes worked out
(see Create booking).
unit_name_plural is only needed when the worked-out plural is wrong. Send null for unit_name to
go back to plain groups; that clears the plural too. A booking option can carry its own word, and a
blank one follows the experience. Both fields are read back under pricing.
Guide requirementsโ
Two lists say what a guide should bring to run the experience: required_certifications and
required_languages. They are the same lists the portal sets under Guide requirements on the
experience.
Both are warn-only. Assigning a guide who is missing one flags it on the departure so the shop
can see it; it never stops the assignment, and it never stops a customer booking. Sending either
field replaces that list outright, so send the whole set. Send [] to clear one.
required_certifications takes any of:
first_aid (First Aid), cpr (CPR), wilderness_first_aid (Wilderness First Aid),
wilderness_first_responder (Wilderness First Responder), lifeguard (Lifeguard),
swiftwater_rescue (Swiftwater Rescue), avalanche_safety (Avalanche Safety),
dive_certification (Dive Certification), drivers_license (Driver's License),
boat_license (Boat License), food_handling (Food Handling), uscg_master (USCG Master),
uscg_oupv (USCG OUPV), uscg_ab (USCG AB), uscg_offshore (USCG Offshore Endorsement),
uscg_towing (USCG Towing Endorsement), specialty (Specialty Endorsement), other (Other).
required_languages takes any two-letter code from:
ar (Arabic), cs (Czech), da (Danish), nl (Dutch), en (English), tl (Filipino),
fi (Finnish), fr (French), de (German), el (Greek), he (Hebrew), hi (Hindi),
hu (Hungarian), id (Indonesian), it (Italian), ja (Japanese), ko (Korean), ms (Malay),
zh (Mandarin Chinese), no (Norwegian), pl (Polish), pt (Portuguese), ro (Romanian),
ru (Russian), es (Spanish), sv (Swedish), th (Thai), tr (Turkish), uk (Ukrainian),
vi (Vietnamese).
Anything outside those lists is rejected with a 422 validation_error naming what can be sent.
Both come back on every experience response, alongside required_certification_names and
required_language_names, which carry the same entries in the words the shop reads on screen - so
you never have to print first_aid to a person.
{
"required_certifications": ["first_aid", "boat_license"],
"required_languages": ["es"]
}
Example Requestโ
curl -X POST "https://app.equipdash.com/api/v1/experiences" \
-H "Authorization: Bearer eqd_live_abc123xyz789..." \
-H "Content-Type: application/json" \
-d '{
"name": "Beginner Surf Lesson",
"brief_description": "Learn to surf with professional instructors",
"category_id": 2,
"duration_value": 2,
"duration_unit": "Hours",
"price_applies": "yearRound",
"price_structure": "participant",
"default_capacity": 8,
"location": "Main Beach",
"min_booking_notice": true,
"min_booking_notice_value": 12,
"min_booking_notice_unit": "Hours",
"participants_required": true,
"required_certifications": ["first_aid", "lifeguard"],
"required_languages": ["es"]
}'
Responseโ
Success (201 Created)โ
{
"data": {
"id": 15,
"name": "Beginner Surf Lesson",
"unique_code": "BSL-001",
"status": "active",
"brief_description": "Learn to surf with professional instructors",
"long_description": null,
"category": {
"id": 2,
"name": "Water Activities"
},
"duration": {
"value": 2,
"unit": "Hours"
},
"pricing": {
"applies": "yearRound",
"structure": "participant"
},
"capacity": 8,
"is_private": true,
"location": "Main Beach",
"booking_notice": {
"required": true,
"value": 12,
"unit": "Hours",
"minutes": 720,
"same_day_cutoff_time": null,
"inherited": false
},
"participants_required": true,
"required_certifications": ["first_aid", "lifeguard"],
"required_certification_names": ["First Aid", "Lifeguard"],
"required_languages": ["es"],
"required_language_names": ["Spanish"],
"availability": [],
"default_image": null,
"created_at": "2026-01-15T12:00:00+00:00",
"updated_at": "2026-01-15T12:00:00+00:00"
},
"meta": {
"request_id": "req_def456",
"timestamp": "2026-01-15T12:00:00+00:00"
}
}
Errorsโ
| Status | Code | Description |
|---|---|---|
| 401 | authentication_required | No API key provided |
| 401 | invalid_api_key | Invalid API key |
| 403 | insufficient_scope | API key lacks experiences:write scope |
| 422 | validation_error | Invalid request data |
| 422 | equipment_invalid | A gear row is wrong: an item that is not yours, a price not on this experience, or ask_size on an item with no sizes |
Gear per priceโ
When the customer's pick decides the gear (one price takes a paddleboard, another a kayak), give each
gear row the price it belongs to. Rows without a rate_category_id are used by every price.
{
"equipment": [
{ "product_id": 41, "units_basis": "participant", "units_per": 1, "rate_category_id": 7 },
{ "product_id": 42, "units_basis": "participant", "units_per": 1, "rate_category_id": 8 },
{ "product_id": 43, "units_basis": "participant", "units_per": 1 }
]
}
- A booking line of price
7holds product41and product43; price8holds42and43. - Each price is limited by its own gear, so one price can sell out while another still has spaces. Check experience availability returns the spaces per price.
- A
rate_category_idmust be one of the prices on this experience, and one product is either every price's or a price's own, never both. Either mistake returns422.
Asking each person their sizeโ
Set ask_size: true on a gear row and checkout asks every person on the booking, one by one, which
size of that item they need. The choices are the item's own sizes, the same ones its rentals use.
{
"equipment": [
{ "product_id": 41, "units_basis": "participant", "units_per": 1, "ask_size": true },
{ "product_id": 43, "units_basis": "session", "units_per": 1 }
]
}
- The item needs sizes first. A row asking for a size of an item with none returns
422equipment_invalid, for example "Paddleboard has no sizes yet. Add its sizes first." - A row without
ask_sizekeeps whatever it was.ask_size: falsestops asking; sizes people already gave are kept. - An item taken out of the gear list stops being asked.
- The response lists each row's
ask_sizeand the full list of questions inasks_each_person. To ask anything else of each person (a certificate level, a weight), use the Questions endpoints.
Each person's sizes come back on the booking as gear_sizes; see
Get Booking.