PUBLIC
Returns a product with its calendar day records for the given date range.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
Cache —Cache-Control: public, max-age=300(5 minutes)
Part of the catalog — The product catalog explains how merchants, products, calendars and timeslots fit together.
Why this endpoint exists
Dated products are priced per day. A Tuesday in November and a Saturday in July are different prices with different discounts and different remaining stock. This endpoint is the calendar behind the date picker: for each day in a range it returns what the customer pays, what they save, and whether any capacity is left.
The five-minute cache is the shortest in the catalog, and deliberately so — stock moves. Everything else about a product can be an hour stale without harm; availability cannot.
Skip this endpoint entirely when the product reports no_date: true. There are no day records to fetch.
How to use it
curl "https://api.acc.funtrips.io/v2/merchants/$MERCHANT/products/$PRODUCT/calendar?from=2026-09-04&to=2026-09-18" \
-H "x-campaign-id: $CAMPAIGN_ID"{
"data": {
"product": {
"id": "d4b8f2a1-6c3e-4957-b18d-2f5a9c7e3b04",
"product_type": "TICKET",
"physical_product": false,
"unit_price": { "value": "24.00", "currency": "EUR" },
"list_price": { "value": "24.00", "currency": "EUR" },
"booking_costs": { "value": "0.99", "currency": "EUR" },
"visit_guarantee_cost": { "value": "1.50", "currency": "EUR" }
},
"days": [
{
"date": "2026-09-04",
"effective_price": { "value": "18.50", "currency": "EUR" },
"list_price": { "value": "24.00", "currency": "EUR" },
"discount_percentage": 22.9,
"discount_amount": { "value": "5.50", "currency": "EUR" },
"available_stock": 142
},
{
"date": "2026-09-05",
"effective_price": { "value": "24.00", "currency": "EUR" },
"list_price": { "value": "24.00", "currency": "EUR" },
"discount_percentage": 0,
"discount_amount": { "value": "0.00", "currency": "EUR" }
}
]
}
}Query parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
from | date YYYY-MM-DD | today | Inclusive start |
to | date YYYY-MM-DD | from + 7 days | Inclusive end |
Both are optional. Omit both and you get today through today + 7 days. Both bounds are inclusive, so from=2026-09-04&to=2026-09-04 returns exactly one day.
Request the range your UI actually shows. A month view asks for a month; do not fetch a year to render a fortnight.
Reading the days array
A missing date is not a bookable date. days contains only days with a calendar record — gaps mean closed or not on sale, and your date picker should disable any date absent from the response. Never interpolate.
| Field | Meaning |
|---|---|
date | The calendar day, YYYY-MM-DD |
effective_price | What the customer pays per unit that day |
list_price | Pre-discount reference — the strikethrough |
discount_percentage | Saving as a percentage |
discount_amount | Saving as money |
available_stock | Units left, nullable |
available_stock being null means unlimited, not zero
available_stock being null means unlimited, not zeronull is the "no capacity limit configured for this day" signal. Treating it as 0 hides every unconstrained day in your picker — a bug that looks like a sold-out product.
const bookable = day.available_stock === null || day.available_stock > 0;Days with available_stock: 0 are genuinely sold out. Show them disabled rather than hiding them, so the customer can see they picked a popular date.
The prices here are per-day, and not the whole bill
effective_price is one unit on one day, before per-ticket surcharges and before customer-specific discounts from wallet, vouchers or membership. It is the right number for the date picker and the wrong number for the order summary. The order summary comes from create checkout.
The product wrapper is deliberately slim
data.product carries only what the calendar view owns: ids, type, the physical flag, and the base prices. title, conditions and age_group are not here. They belong to the products view — fetch them from get product or the product list.
The practical pattern for a product page is one metadata call plus one calendar call, the metadata cached for two hours and the calendar re-fetched as the customer pages through months.
Timeslots are a separate call
This endpoint answers "which days, at what price". When the product reports requires_timeslot: true, the customer must also pick a time of day — that comes from POST /fulfillment/timeslots for the chosen date. A day present here with stock available can still return zero timeslots, and that combination means the day is not bookable after all.
Errors
| Status | type | Cause |
|---|---|---|
400 | — | Malformed date (must be YYYY-MM-DD), non-UUID path parameter, or missing x-campaign-id |
404 | urn:qup:error:product-not-found | No such product under that merchant in this campaign |
500 | urn:qup:error:product-internal-error | Unexpected fault |
A 200 with an empty days array is not an error: the product exists but has no bookable days in the range you asked for. Widen the range, or check sold_out and first_available_date on the product.
