Get Bundle
Retrieve a single bundle with all its items.
A bundle may be a simple group of rental products or a combo that mixes rentals and an experience with "pick one" choice groups, a duration-tiered discount on the rental, and a single flat security hold. See Mixed bundles (combos) for the additional fields.
Endpointโ
GET /v1/bundles/{id}
Authenticationโ
Requires a live or test API key with products:read scope.
Path Parametersโ
| Parameter | Type | Description |
|---|---|---|
id | integer | Bundle ID |
Example Requestโ
curl -X GET "https://app.equipdash.com/api/v1/bundles/1" \
-H "Authorization: Bearer YOUR_API_KEY"
Responseโ
Success (200 OK)โ
{
"data": {
"id": 1,
"name": "Beach Day Package",
"description": "Everything you need for a perfect day at the beach including umbrella, chairs, and cooler.",
"excerpt": "Complete beach package",
"pricing": {
"type": "calculated",
"flat_price": null,
"interval_unit": null
},
"security_deposit_type": "inherit",
"security_deposit_amount": null,
"has_experience_items": false,
"items": [
{
"id": 1,
"item_type": "product",
"experience_id": null,
"choice_group": null,
"choice_label": null,
"variation_id": null,
"rate_category_id": null,
"display_label": "Beach Umbrella",
"discount_tiers": null,
"allowed_durations": null,
"product": {
"id": 10,
"name": "Beach Umbrella"
},
"variation": null,
"quantity": 1,
"discount": null,
"sort_order": 1
},
{
"id": 2,
"item_type": "product",
"experience_id": null,
"choice_group": null,
"choice_label": null,
"variation_id": null,
"rate_category_id": null,
"display_label": "Beach Chair",
"discount_tiers": null,
"allowed_durations": null,
"product": {
"id": 11,
"name": "Beach Chair"
},
"variation": {
"id": 5,
"name": "Blue"
},
"quantity": 2,
"discount": {
"type": "percentage",
"value": 10
},
"sort_order": 2
},
{
"id": 3,
"item_type": "product",
"experience_id": null,
"choice_group": null,
"choice_label": null,
"variation_id": null,
"rate_category_id": null,
"display_label": "Cooler",
"discount_tiers": null,
"allowed_durations": null,
"product": {
"id": 15,
"name": "Cooler"
},
"variation": null,
"quantity": 1,
"discount": {
"type": "flat",
"value": 5
},
"sort_order": 3
}
],
"choice_groups": [],
"always_included": [
{ "bundle_item_id": 1, "item_type": "product", "label": "Beach Umbrella" },
{ "bundle_item_id": 2, "item_type": "product", "label": "Beach Chair" },
{ "bundle_item_id": 3, "item_type": "product", "label": "Cooler" }
],
"total_items": 4,
"is_bookable": true,
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
},
"meta": {
"request_id": "req_abc123"
}
}
Response Fieldsโ
| Field | Type | Description |
|---|---|---|
id | integer | Bundle ID |
name | string | Bundle name |
description | string | Full description |
excerpt | string | Short description |
pricing.type | string | flat or calculated |
pricing.flat_price | float|null | Fixed price (if flat pricing) |
pricing.interval_unit | string|null | hour, day, or week (if flat pricing) |
security_deposit_type | string | inherit (sum the per-item product deposits) or flat (a single fixed hold for the whole combo). See Mixed bundles. |
security_deposit_amount | float|null | The flat hold amount, in the supplier's currency. Only set (and only meaningful) when security_deposit_type is flat; null otherwise. |
cancellation_policy_id | integer|null | Optional per-combo cancellation policy override. null means "apply the most rigid policy across the combo's items" (the default). |
cancellation_policy | object|null | The overriding policy (id, name) when cancellation_policy_id is set; null otherwise. |
has_experience_items | boolean | true when at least one item is an experience โ i.e. this is a combo. Booking it produces a mixed booking. |
items | array | Bundle items |
items[].item_type | string | product (rental), experience, or store_product (a shop add-on, always full price). Exactly one of product / experience / store_product is populated to match. |
items[].experience_id | integer|null | The experience ID on an experience item; null otherwise. |
items[].extra_participant_price | number|null | Experiences only. The per-participant charge (rate-category matched) applied to each participant beyond those included, e.g. 39.00 โ surface it as "charged per participant ($X)". Omitted on non-experience items; null when the experience has no positive per-head price. |
items[].choice_group | string|null | Items sharing the same non-null choice_group are "pick one" alternatives (e.g. sxs, watercraft). null = an always-included fixed item. |
items[].choice_label | string|null | The customer-facing label for this option (e.g. "Large (2 seats)"). |
items[].variation_id | integer|null | Rentals only. The one size or version this item is pinned to. null lets the customer pick. |
items[].rate_category_id | integer|null | Experiences only. The one price this item books at. null uses the experience's first price. |
items[].display_label | string | Resolved label to show the customer โ choice_label when set, otherwise the product/experience name. |
items[].discount_tiers | object|null | Rentals only. A { hours: percent_off } map, e.g. { "2": 20, "4": 15 } โ 2h gets 20% off, 4h gets 15% off. Supersedes discount when present. Always null on experience items (experiences are never discounted in a combo). |
items[].allowed_durations | array|null | Rentals only. The durations in hours this rental may take inside the bundle, e.g. [2, 4]. Fractional values mean minutes (0.5 = 30 min, 2.5 = 2h 30m). Enforces a minimum/whitelist that can be narrower than the product's standalone durations. null = any. |
items[].product | object|null | Product info (id, name, โฆ) on a rental item. |
items[].store_product | object|null | Store-product info (id, name, price, default_image) on a store_product item; null otherwise. Store products are always full price. |
items[].variation | object|null | Variation info if applicable |
items[].content_options | array | Parts inside a rental kit that need an option choice. Each has part_id, label, quantity, and options (id, name, available). Without booking dates, available does not check stock for a window. Send picks in the booking's content_variations map. Empty for items without these choices. |
items[].quantity | integer | Item quantity |
items[].discount | object|null | Flat per-item discount (type, value). For combos, prefer the duration-tiered discount_tiers when present. |
items[].sort_order | integer | Display order |
choice_groups | array | Additive convenience view of the pick-one structure โ one entry per non-empty choice_group, so you don't have to group the flat items array yourself. Empty ([]) for a simple bundle with no choice groups. The flat items array above is unchanged. |
choice_groups[].choice_group | string | The group name (e.g. "watercraft"). The customer picks one option from each group. |
choice_groups[].options | array | The options in this group โ each { bundle_item_id, item_type, label }. Send the chosen bundle_item_id as choices.{group} when booking. |
always_included | array | The fixed (non-choice) items that ship with every combo โ each { bundle_item_id, item_type, label }. These have no choice_group and are always priced in. |
total_items | integer | Sum of all item quantities |
is_bookable | boolean | Whether bundle can be booked |
Mixed bundles (combos)โ
A mixed bundle (combo) packages one or more rentals with an experience in a single bookable product. The customer picks one of each choice group, picks a rental duration, sees a combo total and dollar savings, and pays once. Booking a combo creates a single mixed booking.
How the extra fields fit together:
has_experience_items: trueflags the bundle as a combo.- Choice groups โ items sharing a
choice_group(e.g.sxs,watercraft) are alternatives; the customer picks exactly one from each group. Usechoice_label/display_labelfor the option labels. - Duration-tiered discount โ a rental item's
discount_tiers({ hours: percent }) andallowed_durations([2, 4]) drive the deal on the rental half. Durations are in hours, and fractional values are minutes (0.5= 30 min,2.5= 2h 30m) โ both thediscount_tierskeys and theallowed_durationsvalues. The experience is always full price โ itsdiscount_tiersis alwaysnull. - Store-product add-ons โ an item with
item_type: store_productis a shop add-on (e.g. a dry bag or a wetsuit sock). It carries astore_productblock (id,name,price,default_image) and, like an experience, is always full price (nodiscount_tiers). - Cancellation policy โ set
cancellation_policy_idto override the combo's cancellation policy; the matching policyid+nameare echoed undercancellation_policy. Leave itnullto apply the most rigid policy across the combo's items. - Flat security hold โ when
security_deposit_typeisflat, the combo carries a singlesecurity_deposit_amounthold covering both activities, instead of summing the per-item product deposits. - The percentage is internal. Customers should see the combo total and dollar savings computed at booking time (see the response of Create booking) โ
discount_tiersexposes the underlying percentages for your own logic, not for display.
Example โ Biff Rentals "Discovery Tour" comboโ
A guided side-by-side excursion (an experience, always $499) paired with a personal watercraft rental (the rental half gets the deal: 2h = 20% off, 4h = 15% off), under a single flat $4,000 security hold. The customer picks one side-by-side, one watercraft, and a 2h or 4h watercraft window.
{
"data": {
"id": 88,
"name": "Discovery Tour",
"description": "A guided side-by-side excursion plus a personal watercraft rental โ one combo price, one hold.",
"excerpt": "Side-by-side excursion + watercraft combo",
"pricing": {
"type": "calculated",
"flat_price": null,
"interval_unit": null
},
"security_deposit_type": "flat",
"security_deposit_amount": 4000,
"has_experience_items": true,
"items": [
{
"id": 501,
"item_type": "experience",
"experience_id": 1257,
"choice_group": "sxs",
"choice_label": "Can-Am Maverick X3 (2 seats)",
"display_label": "Can-Am Maverick X3 (2 seats)",
"discount_tiers": null,
"allowed_durations": null,
"experience": {
"id": 1257,
"name": "Discovery Tour Excursion โ X3"
},
"extra_participant_price": 39.00,
"product": null,
"variation": null,
"quantity": 1,
"discount": null,
"sort_order": 1
},
{
"id": 502,
"item_type": "experience",
"experience_id": 1260,
"choice_group": "sxs",
"choice_label": "Can-Am Maverick X3 MAX (4 seats)",
"display_label": "Can-Am Maverick X3 MAX (4 seats)",
"discount_tiers": null,
"allowed_durations": null,
"experience": {
"id": 1260,
"name": "Discovery Tour Excursion โ X3 MAX"
},
"extra_participant_price": 39.00,
"product": null,
"variation": null,
"quantity": 1,
"discount": null,
"sort_order": 2
},
{
"id": 503,
"item_type": "product",
"experience_id": null,
"choice_group": "watercraft",
"choice_label": "Sea-Doo GTI 130",
"display_label": "Sea-Doo GTI 130",
"discount_tiers": { "2": 20, "4": 15 },
"allowed_durations": [2, 4],
"product": {
"id": 163,
"name": "Sea-Doo GTI 130"
},
"variation": null,
"quantity": 1,
"discount": null,
"sort_order": 3
},
{
"id": 504,
"item_type": "product",
"experience_id": null,
"choice_group": "watercraft",
"choice_label": "Sea-Doo GTR-X 300",
"display_label": "Sea-Doo GTR-X 300",
"discount_tiers": { "2": 20, "4": 15 },
"allowed_durations": [2, 4],
"product": {
"id": 164,
"name": "Sea-Doo GTR-X 300"
},
"variation": null,
"quantity": 1,
"discount": null,
"sort_order": 4
}
],
"choice_groups": [
{
"choice_group": "sxs",
"options": [
{ "bundle_item_id": 501, "item_type": "experience", "label": "Can-Am Maverick X3 (2 seats)" },
{ "bundle_item_id": 502, "item_type": "experience", "label": "Can-Am Maverick X3 MAX (4 seats)" }
]
},
{
"choice_group": "watercraft",
"options": [
{ "bundle_item_id": 503, "item_type": "product", "label": "Sea-Doo GTI 130" },
{ "bundle_item_id": 504, "item_type": "product", "label": "Sea-Doo GTR-X 300" }
]
}
],
"always_included": [],
"total_items": 4,
"is_bookable": true,
"created_at": "2026-06-20T10:00:00Z",
"updated_at": "2026-06-20T10:00:00Z"
},
"meta": {
"request_id": "req_abc123"
}
}
choice_groups is the pick-one structure at a glance โ the customer picks one option from each group (here one side-by-side and one watercraft). It is purely additive: the same items are still in the flat items array. always_included lists any fixed items with no choice_group.
To book this combo, send the selection keys (choices, durations, participants, schedules, variations) in the bookings request โ see Create booking โ Mixed-bundle combo. To compute the live total before booking, call Bundle price preview.
Errorsโ
| Status | Code | Description |
|---|---|---|
| 401 | authentication_required | No API key provided |
| 403 | insufficient_scope | API key lacks products:read scope |
| 403 | forbidden | Bundle belongs to different supplier |
| 404 | resource_not_found | Bundle does not exist |
Prices by rate typeโ
When pricing.type is rates, pricing.rate_prices lists the offered rate category IDs, labels, prices, and seat_weight. pricing.rate_price_basis is once, hour, or day. Bundles with experiences charge once; rental-only bundles can charge by duration.
Send rate_counts as a map of rate category ID to count, for example {"1": 2, "2": 1}. These counts apply to every selected experience and to gear configured for each person. Choose departures through the existing schedules map and rental lengths through durations. Use each bundle's offered IDs rather than assuming labels or IDs.
A combo rate counts as one sale unit and reserves the number of people in seat_weight. Gear uses quantity_basis (person or booking) and an optional rate_category_id; a null category applies to everyone. Unavailable items or incompatible experience rates reject the whole booking.