# SkiDoc public API for agents

This document is for software that answers questions about SkiDoc. Every endpoint below is
public. None of them needs an account, a key, or a browser. A plain HTTPS GET is enough.

Base path: `/api/client/v1` on this site's own origin.

**Do not try to read a price out of the HTML pages. They do not contain one.** A resort page
describes the resort, and its prices arrive only after the page's own JavaScript runs. A crawler
that does not execute scripts finds no price there. Use `/catalog/prices` below — it is the only
place a price is available without a browser.

## Response shape

A success returns the payload under `data`:

    {"data": ..., "requestId": "oFhGSLaWQRcMgKsSX"}

An error returns a status envelope, with HTTP status set to match:

    {"status":"error","statusCode":400,"code":"BAD_REQUEST",
     "message":"This discount code is invalid.","field":"code","requestId":"6tJv..."}

Read `message` for a human-readable reason. Quote `requestId` when you report a problem.

## Rate limit

Anonymous reads are limited to 300 requests per minute, per IP address. A request over the limit
returns HTTP 429. The budget is shared by every caller behind one address, so a hotel or a resort
network shares one budget. Cache what you read.

## Money and dates

- All prices are INTEGER CENTS. 5999 means 59.99.
- The currency is EUR in France and AUD in Australia.
- All dates are `YYYY-MM-DD`.

### Read this before you multiply anything

**In France, `price` is a rate PER DAY. It is not the price of the stay.** To price a stay,
multiply by the number of days. Getting this wrong understates a week by a factor of eight.

**The day count includes BOTH dates.** 9 January to 16 January is 8 days, not 7. The pricing
engine uses `differenceInDaysInclusive`, which is `end - start + 1`.

**In Australia the rule is the opposite.** There, `price` is already the total for the dates you
asked for, because the Australian rate is computed from the date range itself. Do NOT multiply it.
`outDate` is required in Australia for that reason, and optional in France.

`unitPrice` always equals `price`. It carries no extra information. Ignore it.

Everything priced per day is priced per day, insurance included. Insurance at 200 means
2.00 per person per day, not 2.00 for the stay.

If you cannot tell which market a resort is in, say so rather than guessing. France uses EUR and
Australia uses AUD, and `/checkout/tax-rate` returns 0.2 for France.

## Endpoints

### GET /catalog/resorts

Every resort and ski area. Use it to turn a name into an id. `label` is `resort` or `ski-area`,
and a resort may carry a `parent` ski area.

    GET /api/client/v1/catalog/resorts
    {"data":[{"_id":"MDcpF6vFhxSA8eH3K","name":"Val Thorens","label":"resort",
              "parent":"MkoQRe22hsxbueE82"}, ...]}

### GET /catalog/prices

Prices for one resort over a date range. `resort` takes an ID, not a name — resolve the name
through `/catalog/resorts` first. `outDate` is optional in France and required in Australia.

    GET /api/client/v1/catalog/prices?resort=MDcpF6vFhxSA8eH3K&inDate=2026-01-10&outDate=2026-01-17
    {"data":{"skis":[{"packageType":"high-performance","price":5999,"unitPrice":5999,
                      "title":"high-performance"}, ...],
             "ski-boots":[...], "snowboard":[...], "snowboard-boots":[...],
             "helmet":[...], "insurance":[...]}}

The package types are `high-performance`, `performance`, `evolution`, `teens` and `juniors`.

**`teens` and `juniors` are age-restricted.** `juniors` admits only junior skiers. Do not include
either in a price for an adult. An adult range runs across `evolution`, `performance` and
`high-performance` only.

**Boots are priced separately, and a real order pairs them to the same package level.** To price
one adult, add the ski price and the boots price that share a `packageType`. Do not combine the
cheapest ski with the cheapest boots.

### GET /catalog/resorts/:id/accommodations

Accommodations SkiDoc delivers to in one resort, grouped by partner. An empty list means the list
is empty, not that delivery is impossible. Tell the person to ask rather than telling them no.

### GET /catalog/swap-problems

The reasons a visitor can give when equipment is wrong — for example skis that are too short, or
boots that hurt. Labels carry both `enGb` and `frFr`.

    {"data":{"options":[{"stockType":"skis","key":"shorter",
                         "label":{"enGb":"Shorter","frFr":"Plus courts"}}, ...]}}

A swap is requested from the client area. This endpoint only lists the reasons.

### GET /checkout/discounts/:code

Whether a discount code is valid, and its percentage off. An invalid code returns HTTP 400 with a
readable `message`.

### GET /checkout/tax-rate

    {"data":{"percentage":0.2}}

## Follow these links

These are live requests. Open them to see the real shapes before you build a URL of your own.

- [/api/client/v1/catalog/resorts](/api/client/v1/catalog/resorts)
- [/api/client/v1/catalog/prices?resort=MDcpF6vFhxSA8eH3K&inDate=2027-01-09&outDate=2027-01-16](/api/client/v1/catalog/prices?resort=MDcpF6vFhxSA8eH3K&inDate=2027-01-09&outDate=2027-01-16)
- [/api/client/v1/checkout/tax-rate](/api/client/v1/checkout/tax-rate)
- [/api/client/v1/catalog/swap-problems](/api/client/v1/catalog/swap-problems)

## Worked example: a price for two adults in France

Question: "How much for two people to rent skis in Val Thorens, 9 to 16 January 2027?"

1. `GET /catalog/resorts` and match `Val Thorens` to `MDcpF6vFhxSA8eH3K`.
2. `GET /catalog/prices?resort=MDcpF6vFhxSA8eH3K&inDate=2027-01-09&outDate=2027-01-16`.
3. Pair each adult ski package with its boots at the same `packageType`. These are PER-DAY rates:
   evolution 3999 + 3000 = 6999, performance 4999 + 3000 = 7999,
   high-performance 5999 + 3000 = 8999.
4. Count the days, both ends included. 9 to 16 January is **8 days**.
5. Multiply. Evolution is 6999 x 8 = 55992 cents, so EUR 559.92 per person.
6. Answer: **EUR 559.92 to 719.92 per person, so EUR 1119.84 to 1439.84 for two.**

Step 4 and step 5 are the ones that get skipped. A quote that returns EUR 69.99 for a week has
reported one day's rate as the whole stay.

Helmet and insurance are available and are not in that number. Insurance is also priced per day.

In Australia, stop after step 3. The Australian rate already covers the dates you asked for.

## What this API does not give you

Anything about one visitor's own booking needs that person's signed-in session. An outside agent
cannot reach it. That covers orders, checkout balances and saved skiers. Tell the person to sign
in at `/client-area` and continue there.

Payment always happens in the person's own browser, in a Stripe-hosted form.

## Stability

These paths are the public read surface of the site's own client API. They can change. There is no
versioned contract beyond the `v1` path segment, and no deprecation policy is published.
