PUBLIC
Returns the list of products belonging to the specified merchant.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
Cache —Cache-Control: public, max-age=7200(2 hours)
Part of the catalog — The product catalog explains how merchants, products, calendars and timeslots fit together.
Why this endpoint exists
A product is a specific purchasable thing at a merchant: an adult day ticket, a child ticket, a family bundle, a spa arrangement. This is the list you render on the merchant page as the customer's actual choice, and each item carries everything you need to decide how it must be sold.
Note the response has no meta.pagination — a merchant's product list is small and comes back whole. Pair it with the merchant detail read to build a merchant page.
How to use it
curl "https://api.acc.funtrips.io/v2/merchants/8a2f4c61-9d3e-4b57-a0c8-1e5f7b2d9a30/products" \
-H "x-campaign-id: 3c1e5a90-7b2d-4f61-9a83-1d4e7f2b8c05" \
-H "Accept-Language: nl-NL"{
"data": [
{
"id": "d4b8f2a1-6c3e-4957-b18d-2f5a9c7e3b04",
"title": "Dagticket volwassene",
"product_type": "TICKET",
"physical_product": false,
"sku": "ZOO-ADULT-DAY",
"age_group": "adult",
"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" },
"no_date": false,
"requires_timeslot": true,
"best_effective_price": { "value": "18.50", "currency": "EUR" },
"best_list_price": { "value": "24.00", "currency": "EUR" },
"best_discount_amount": { "value": "5.50", "currency": "EUR" },
"best_discount_percentage": 22.9,
"first_available_date": "2026-09-04",
"last_available_date": "2026-12-31",
"sold_out": false,
"conditions": "Geldig op de gekozen datum."
}
]
}The four flags that decide your UI
Most fields are content or money. These four are behavioural, and reading them wrong produces checkouts that fail at the last step.
no_date — show a date picker?
no_date — show a date picker?false (default) means the product sells on specific calendar days. Fetch the calendar and make the customer choose one.
true means there are no per-day entries. Hide the date picker and submit today's date at checkout — that is accepted and expected. Trying to render a calendar for a no_date product gets you an empty calendar and a dead end.
requires_timeslot — show a time picker?
requires_timeslot — show a time picker?Independent of no_date. A product can be calendar-dated yet valid all day.
true means the customer must also pick an entry window, from POST /fulfillment/timeslots.
Whenrequires_timeslotistrueand the timeslot list for the chosen date comes back empty, that date has no availability. Block it and prompt for another date.Do not fall back to a midnight visit time. Timeslotted providers reject it, and because midnight UTC is 02:00 in Amsterdam in summer, the failure surfaces as a booking on the wrong day. When
requires_timeslotisfalse, an empty timeslot list simply means "all day" and the date alone is enough.
physical_product — collect an address?
physical_product — collect an address?true means something gets shipped, so the checkout needs a full shipping address. You do not have to derive this yourself: create checkout returns requires_shipping_address, resolved authoritatively server-side. Use this flag for previews ("shipped to your home"), and the checkout's flag as the rule.
sold_out — can it still be bought?
sold_out — can it still be bought?true means this product sells on dates and no purchasable date remains — everything left is sold out or past. Show it as unavailable and do not offer a date picker.
When sold_out is true, the best_* price fields and the availability dates are omitted. Do not treat their absence as €0.
A product without a calendar is always purchasable and reports false.
Prices: which one to show
| Field | Meaning |
|---|---|
unit_price | Current price for one unit |
list_price | Undiscounted reference price — the strikethrough |
booking_costs | Surcharge per ticket |
visit_guarantee_cost | Surcharge per ticket for the flexible-date option |
best_effective_price | Cheapest actual price across available days |
best_list_price | That day's pre-discount price |
best_discount_amount / best_discount_percentage | The deepest discount available |
The best_* family answers "from €X" and "up to Y% off" across the product's remaining days, computed stock-aware — a sold-out cheap day does not set the headline. For a specific day's price, read the calendar.
Never total these yourself for display at checkout. Surcharges are per-ticket, discounts depend on the customer's wallet, vouchers and memberships, and the authoritative arithmetic comes back in the checkout totals.
Other fields worth knowing
sku— the upstream catalog identifier. It travels with the checkout line and is what commission settlement keys on. Log it; it makes reconciliation conversations concrete.age_group— populated only whenproduct_typeisTICKET(adult,child, …). Use it to group ticket types.ticket_validity_type/ticket_validity_date— whenticket_validity_typeisVALIDITY_PERIOD, the ticket is valid throughticket_validity_dateinclusive. This is how evergreen products print a real "valid until" date instead of the purchase day. Surface it, or customers will assume same-day-only.conditions— merchant terms to display before purchase.
Errors
| Status | type | Cause |
|---|---|---|
400 | — | merchant_id is not a UUID, or x-campaign-id is missing |
500 | urn:qup:error:product-internal-error | Unexpected fault |
A merchant with no products in this campaign returns 200 with an empty array.
