Skip to main content

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โ€‹

FieldTypeRequiredDescription
namestringYesExperience name (max 255 characters)
unique_codestringNoUnique code (auto-generated if not provided)
brief_descriptionstringNoShort description
long_descriptionstringNoFull description
category_idintegerNoPrimary category ID. Use category_ids for multiple categories.
category_idsinteger[]NoPut the experience in multiple categories (primary first). The first id becomes the primary (also returned as category). Takes precedence over category_id.
statusstringNoactive, inactive (default: active)
duration_valuenumberYesDuration value (minimum: 0.1; decimals allowed, e.g. 3.5 hours)
duration_unitstringYesMinutes, Hours, or Days
price_appliesstringNoyearRound or seasonal (default: yearRound)
price_structurestringNoparticipant or group (default: participant)
default_capacityintegerNoDefault capacity per session (for private experiences this is the maximum group size)
is_privatebooleanNoPrivate 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_bookingintegerNoThe 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_experiencebooleanNoDefault 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_onlybooleanNoDefault 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_modestringNoinstant (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_idintegerNoThe 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_idinteger or nullNoAnother 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_basisstringNoHow 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_perintegerNoUnits consumed per booking, guest, or session. Default 1.
equipmentarrayNoMulti-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_pricebooleanNotrue 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_guidebooleanNoGuide 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_neededintegerNoDefault 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.
locationstringNoWhere the experience takes place
min_booking_noticebooleanNoRequire advance booking (default: false)
min_booking_notice_valueintegerNoNotice period value
min_booking_notice_unitstringNohours, days
same_day_cutoff_timestring|nullNoSame-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_typestring|nullNoThe 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_valuenumber|nullNoThe 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_requiredboolean|nullNoRequire participant details. true = always, false = never, null or omitted = follow the shop-wide default (participant-priced experiences default to always)
unit_namestringNoWhat each group is, for example canoe. Up to 40 characters. Group pricing only. See Priced per unit
unit_name_pluralstringNoThe plural, when the worked-out one is wrong. Up to 40 characters
default_groups_capacityintegerNoGroup 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_bookingintegerNoFewest 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_feenumberNoCharged 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_labelstringNoCustomer-facing name for that fee (max 255 characters). Defaults to a localized "Small-group fee".
volume_discount_from_groupintegerNoMulti-group discount threshold โ€” groups from this one onward (minimum 2) each get volume_discount_percent off.
volume_discount_percentnumberNoPercent (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_pricebooleanNoDisplay 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_fromintegerNoGroup 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_percentnumberNoPercent (0-100) off every per-person line once a booking reaches people_discount_from people. Send it with people_discount_from.
required_certificationsstring[]NoGuide 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_languagesstring[]NoGuide 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_booking either pays below_min_groups_fee once, or is rejected with a validation error when no fee is configured.
  • Multi-group discount โ€” every group from volume_discount_from_group onward gets volume_discount_percent off that group's base price.
  • Groups per slot โ€” default_groups_capacity caps how many groups a departure can hold, independent of participant seats. Override it for a single departure with groups_capacity on 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โ€‹

StatusCodeDescription
401authentication_requiredNo API key provided
401invalid_api_keyInvalid API key
403insufficient_scopeAPI key lacks experiences:write scope
422validation_errorInvalid request data
422equipment_invalidA 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 7 holds product 41 and product 43; price 8 holds 42 and 43.
  • 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_id must 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 returns 422.

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 422 equipment_invalid, for example "Paddleboard has no sizes yet. Add its sizes first."
  • A row without ask_size keeps whatever it was. ask_size: false stops 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_size and the full list of questions in asks_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.