Quote
A quote is the price of one choice: an option, a start, who is coming, and any extras or pickup. Ask for one each time the traveller changes a choice, and show it as it comes.
POST /v1/public/quotes with:
| Field | What it is |
|---|---|
option_id |
The option chosen. |
start |
The start, in the trip’s own time: 2026-11-14T15:00, or a date (2026-11-14) for an option open all day. |
units |
Each unit chosen, with qty, and ages when the seller prices children by age. |
extras |
Each extra chosen, with qty. Optional. |
pickup |
Where to collect the travellers: a place_id, or lat and lng. Optional. |
currency |
One of the site’s currencies. Optional: the site’s display currency by default. |
curl -X POST "http://tourpingo-production-api:4000/v1/public/quotes" \ -H "Tp-Publishable-Key: $TP_KEY" \ -H "Content-Type: application/json" \ -d '{ "option_id": "9e4c2a71-3b5d-4e8f-a1c6-7d20f5b93e48", "start": "2026-11-14T15:00", "units": [ { "unit_id": "1c7e9b42-6a3f-4d85-b0e2-58f4a1c9d736", "qty": 2 }, { "unit_id": "2d8fac53-7b40-4e96-81f3-69a5b2dae847", "qty": 1, "ages": [ 8 ] } ], "currency": "USD"}'const res = await fetch("http://tourpingo-production-api:4000/v1/public/quotes", { method: "POST", headers: { "Tp-Publishable-Key": process.env.TP_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ "option_id": "9e4c2a71-3b5d-4e8f-a1c6-7d20f5b93e48", "start": "2026-11-14T15:00", "units": [ { "unit_id": "1c7e9b42-6a3f-4d85-b0e2-58f4a1c9d736", "qty": 2 }, { "unit_id": "2d8fac53-7b40-4e96-81f3-69a5b2dae847", "qty": 1, "ages": [ 8 ] } ], "currency": "USD" }),})const answer = await res.json()Answer: 200 OK
{ "option_id": "9e4c2a71-3b5d-4e8f-a1c6-7d20f5b93e48", "lines": [ { "kind": "unit", "ref": "1c7e9b42-6a3f-4d85-b0e2-58f4a1c9d736", "label": "Adult", "qty": 2, "unit_price": { "amount": 6500, "currency": "USD" }, "original_unit_price": null, "total": { "amount": 13000, "currency": "USD" }, "pay_at": "upfront", "included": false, "vehicle": null }, { "kind": "unit", "ref": "2d8fac53-7b40-4e96-81f3-69a5b2dae847", "label": "Child (3-11)", "qty": 1, "unit_price": { "amount": 5000, "currency": "USD" }, "original_unit_price": null, "total": { "amount": 5000, "currency": "USD" }, "pay_at": "upfront", "included": false, "vehicle": null } ], "totals": { "due_now": { "amount": 18000, "currency": "USD" }, "pay_locally": [], "grand_total": { "amount": 18000, "currency": "USD" } }, "confirmation": "instant", "pending_expires_at": null, "remaining": 14, "pax": 3, "pairing": false, "vehicles": [], "fx": { "base": "USD", "quote": "USD", "rate": "1", "rate_id": "0f6d2b8e-4c1a-4e5f-9b3d-7a8c6e2f1d90", "as_of": "2026-10-01T00:00:00.000Z" }, "policy": { "name": "Flexible", "tiers": [ { "from_days": 1, "to_days": null, "refund_pct": 100 }, { "from_days": 0, "to_days": 0, "refund_pct": 0 } ], "preset": "flexible" }, "pickup": null}- Each line: its
label,qtyandtotal. Whenoriginal_unit_priceis set, show it struck through: it is a special’s price before its discount. totals.due_now: what is paid when booking.totals.pay_locally: what is paid on the day, in its own currency. Show it apart, so nobody thinks it is charged now.confirmation:instantbooks at once;manualwaits for the seller, so say so before checkout.policy: the cancellation policy this booking would have.
Show amounts with the currency’s own decimals (money). Don’t round, add or convert them yourself.
When it is refused
Section titled “When it is refused”A selection that can’t be booked gets a 422 whose code says why, with
the facts in details:
curl -X POST "http://tourpingo-production-api:4000/v1/public/quotes" \ -H "Tp-Publishable-Key: $TP_KEY" \ -H "Content-Type: application/json" \ -d '{ "option_id": "9e4c2a71-3b5d-4e8f-a1c6-7d20f5b93e48", "start": "2026-11-14T15:00", "units": [ { "unit_id": "1c7e9b42-6a3f-4d85-b0e2-58f4a1c9d736", "qty": 4 } ], "currency": "USD"}'const res = await fetch("http://tourpingo-production-api:4000/v1/public/quotes", { method: "POST", headers: { "Tp-Publishable-Key": process.env.TP_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ "option_id": "9e4c2a71-3b5d-4e8f-a1c6-7d20f5b93e48", "start": "2026-11-14T15:00", "units": [ { "unit_id": "1c7e9b42-6a3f-4d85-b0e2-58f4a1c9d736", "qty": 4 } ], "currency": "USD" }),})const answer = await res.json()Answer: 422 Unprocessable Content
{ "type": "about:blank", "title": "Unprocessable Content", "status": 422, "code": "NotEnoughCapacity", "detail": "Not enough places are left then.", "details": { "remaining": 2 }}Turn each code into a message of your own, using details: “Only 2 places
are left at 15:00” reads better than an error. The
errors guide lists them
all. A request that doesn’t fit the contract (a missing field, a wrong type)
is 422 InvalidInput instead.
A price for each group size
Section titled “A price for each group size”Many sellers price private trips by group size, so a smaller group pays more
each. To show that as a table, read every size at once with
GET /v1/public/options/{option_id}/group-prices, instead of quoting each
size:
| Parameter | What it is |
|---|---|
start |
The start to price, as a quote’s start. |
month |
Or a month, e.g. 2026-11: its first open start is priced. This month by default. |
currency |
One of the site’s currencies. Optional. |
up_to |
The largest group to price, 1 to 50. By default the option’s own limit, or 10 when it sets none. |
Each of groups is a quote for that many travellers on start: its
total, each (the total shared out, rounded up) and unit_price, with
the group’s confirmation. A size the seller can’t take on that start is
left out. units prices each other age band, such as a child, travelling
with the smallest group, and policy is the cancellation policy. The
prices are the quotes’ own, so a traveller who books that size on that
start, with nothing added, pays what the table showed.
When the traveller goes on to book, hold the places with the same selection.