Calculate Pricing
Calculate prices for multiple products, experiences, or bundles in a single request.
Endpointโ
POST /v1/pricing/calculate
Request Bodyโ
| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Items to price (max 50) |
items[].type | string | Yes | product, experience, or bundle |
items[].product_id | integer | If type=product | Product ID |
items[].experience_id | integer | If type=experience | Experience ID |
items[].bundle_id | integer | If type=bundle | Bundle ID |
items[].variation_id | integer | No | Product variation ID. When the product has variation pricing enabled and this variation carries its own pricing, the quote uses the variation's pricing; otherwise the product's base pricing applies. |
items[].location_id | integer | No | Pickup location ID. When the product has location pricing enabled and this location carries its own pricing, the quote uses it. Most specific scope wins: variation-at-location โ location โ variation โ base. |
items[].quantity | integer | No | Quantity (default: 1) |
items[].rate_category_id | integer | No | Experience rate category |
items[].schedule_id | integer | No | Experience schedule slot |
items[].booking_option_id | integer | No | Selected booking option. Prices, minimum group quantities, fees and discounts come from this option; omitted uses the default option |
items[].participants | integer | No | Group-priced options only. Actual headcount used for the optional per-participant charge. When omitted, the calculation uses quantity as the headcount. |
items[].package_choices | array | No | Tour-package experiences only โ the chosen option per choice group. Each option's per-traveler price is folded into the experience line_total. Travelers = participants if sent, else quantity. |
items[].package_choices[].group_id | integer | If package_choices present | Choice group ID |
items[].package_choices[].option_id | integer | If package_choices present | Selected option ID within the group |
items[].extras | array | No | Add-on extras for this product or experience line. Each extra's total is folded into the line line_total. |
items[].extras[].extra_id | integer | If extras present | Extra ID |
items[].extras[].quantity | integer | No | Extra quantity (default: 1) |
start_date | string | Yes | Start date/time (ISO 8601) |
end_date | string | Yes | End date/time (ISO 8601) |
customer_id | integer | No | Quote for this customer, and the endpoint reads their address off the record. The simplest way to resolve a deposit rule that looks at where the customer is. |
customer_country | string | No | The customer's country, ISO 3166-1 alpha-2. Use these three when you have the location but not a customer record. |
customer_subdivision | string | No | The customer's state, province or region. Accepts a code (TX), a name (Texas) or a qualified reference (US-TX). |
customer_postcode | string | No | The customer's postcode, for a deposit rule that matches on postcode prefix. |
price_channel | string | No | online (default) or counter. counter quotes the shop's counter prices. The response returns price_channel and counter_prices.available (whether the shop prices the counter differently). |
Security depositโ
The response's security_deposit object is the hold this booking would need:
| Field | Type | Description |
|---|---|---|
enabled | boolean | Whether a hold applies at all |
amount | number | The hold, in the shop's currency |
basis | string or null | Which deposit rule set the figure, e.g. 28 days or longer. Null when the shop's flat deposit applied. |
A shop can set deposit rules that change the hold for a long booking, or for a
customer a long way from them. Pass customer_id (or the three customer_*
location fields) and the endpoint resolves the same figure the booking page and
the counter would; omit them and any rule that reads location is skipped. Where
more than one rule matches, the highest amount wins.
You do not have to send the resolved amount back when you create the booking:
POST /bookings applies the same rules itself whenever you leave
security_deposit_amount off, so an integration gets the right hold without
quoting first. Send an amount only to override the shop's rules on purpose.
The rules themselves are managed in the shop's settings. A shop's rules match on places inside its own country (states, provinces or regions), on postcode starts, and on how long the booking runs.
Example Requestโ
curl -X POST "https://app.equipdash.com/api/v1/pricing/calculate" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"type": "product",
"product_id": 1,
"quantity": 2
},
{
"type": "product",
"product_id": 5,
"variation_id": 12,
"quantity": 1
},
{
"type": "experience",
"experience_id": 3,
"schedule_id": 45,
"rate_category_id": 8,
"quantity": 2,
"extras": [
{ "extra_id": 7, "quantity": 2 }
]
},
{
"type": "experience",
"experience_id": 12,
"schedule_id": 88,
"rate_category_id": 14,
"quantity": 2,
"participants": 2,
"package_choices": [
{ "group_id": 4, "option_id": 9 },
{ "group_id": 5, "option_id": 11 }
]
}
],
"start_date": "2026-02-15T09:00:00Z",
"end_date": "2026-02-17T17:00:00Z"
}'
For a group-priced booking option with booking rules, the quote itemizes what the booking will charge:
a below-minimum fee appears as below_min_fee ({ "amount": 200.00, "label": "Small-group fee" }) and a
multi-group discount as volume_discount ({ "amount": 27.90 }). Both are already reflected in
line_total. Render them as sub-lines without adding them on top. A quantity below the selected option's minimum
with no fee configured returns an error for that item, exactly as creating the booking would.
For an experience priced per person with a group discount (people_discount_from and
people_discount_percent), send every person type as its own item in the same request: the quote counts
everyone on the experience across the items, and each item that earns the discount carries it as
volume_discount ({ "amount": 20.00 }), already taken off its line_total.
For a tour package the experience base rate is typically 0 โ the entire price comes from the selected package_choices, priced per traveler. Send package_choices (and participants for the traveler count) and the choice total is folded into line_total.
Responseโ
{
"data": {
"period": {
"start_date": "2026-02-15T09:00:00+00:00",
"end_date": "2026-02-17T17:00:00+00:00"
},
"currency": "USD",
"items": [
{
"index": 0,
"type": "product",
"product_id": 1,
"product_name": "Mountain Bike",
"variation_id": null,
"quantity": 2,
"unit_price": 60.00,
"line_total": 120.00,
"duration": "2 days",
"pricing_breakdown": [
{"label": "Day 1", "amount": 30.00},
{"label": "Day 2", "amount": 30.00}
]
},
{
"index": 1,
"type": "product",
"product_id": 5,
"product_name": "Kayak",
"variation_id": 12,
"quantity": 1,
"unit_price": 85.00,
"line_total": 85.00,
"duration": "2 days"
},
{
"index": 2,
"type": "experience",
"experience_id": 3,
"experience_name": "Sunset Kayak Tour",
"schedule_id": 45,
"rate_category_id": 8,
"rate_category_name": "Adult",
"quantity": 2,
"unit_price": 45.00,
"line_total": 90.00
}
],
"summary": {
"subtotal": 295.00,
"item_count": 3
}
},
"meta": {
"request_id": "req_abc123",
"timestamp": "2026-02-01T10:00:00+00:00"
}
}
Response Fieldsโ
Item (Product)โ
| Field | Type | Description |
|---|---|---|
index | integer | Position in request array |
type | string | product |
product_id | integer | Product ID |
product_name | string | Product name |
variation_id | integer|null | Variation ID |
quantity | integer | Quantity |
unit_price | number | Price per unit |
line_total | number | Total (unit_price ร quantity) |
duration | string | Human-readable duration |
pricing_breakdown | array | Line-by-line pricing details |
Item (Experience)โ
| Field | Type | Description |
|---|---|---|
type | string | experience |
experience_id | integer | Experience ID |
experience_name | string | Experience name |
schedule_id | integer | Schedule slot ID |
price_per_participant | number | Present for group pricing with a per-head charge โ the per-participant rate |
participants | integer | Present for group pricing with a per-head charge โ the headcount used |
participants_total | number | Present for group pricing with a per-head charge โ participants x price_per_participant, included in line_total |
rate_category_id | integer|null | Rate category ID |
rate_category_name | string|null | Rate category name |
quantity | integer | Number of participants |
unit_price | number | Price per person |
package_choices_total | number | Present for tour packages โ sum of the chosen options' per-traveler price ร travelers, included in line_total |
package_choice_lines | array | Present for tour packages โ itemized chosen options (group_name, option_label, price, quantity) |
extras_total | number | Present when extras were sent โ sum of the line's extras, included in line_total |
extra_lines | array | Present when extras were sent โ itemized extras (name, quantity, unit_price, price, charge_per) |
line_total | number | Total price (base + per-participant + package choices + extras) |
Products also accept
extrasand returnextras_total/extra_linesin the same shape, folded into the productline_total.
Item (Error)โ
If an item can't be priced:
{
"index": 0,
"type": "product",
"product_id": 999,
"error": "Product not found"
}
Pricing Breakdownโ
For products with schedule/tiered pricing, pricing_breakdown shows how the price was calculated:
{
"pricing_breakdown": [
{"label": "First 2 hours", "amount": 25.00},
{"label": "Additional hours (3)", "amount": 15.00}
]
}
Notesโ
- Prices don't include taxes (applied at booking time)
- Variation price adjustments are included in
unit_price - Tour-package
package_choicesand lineextrasare priced with the same logic used when the booking is created, so the preview matches the final charge - A required choice group left unanswered, an invalid option, or an unavailable extra returns that item with an
error - If an item has an error, it won't contribute to
summary.subtotal - Maximum 50 items per request