Direct naar de inhoud
Voor skischolen

API voor skischolen

Koppel de software waarmee je skischool al werkt: beschikbaarheid erin, boekingen eruit via een ondertekende webhook, en je antwoorden terug zonder het dashboard te openen.

Met een skischoolaccount op Alpine Pal heb je je team van skileraren, hun agenda's en hun boekingsaanvragen in één dashboard. De API is datzelfde dashboard voor een machine: een channelmanager, een CRM of een eigen script kan de beschikbaarheid van je skileraren lezen en schrijven, van elke betaalde boeking horen op het moment dat die binnenkomt, en die accepteren, afwijzen of er een ander tijdstip voor voorstellen.

De API is bewust beperkt gehouden. Klanten betalen op Alpine Pal, dus de API maakt geen boekingen aan; lessen die elders zijn verkocht, legt hij vast als blokken die de agenda kloppend houden. Wat op de piste geld verschuift – een no-show registreren, een klacht afsluiten – blijft een menselijke handeling in het dashboard.

Een sleutel aanmaken

Sleutels horen bij de skischool, niet bij een persoon, en alleen de eigenaar van de skischool kan ze aanmaken of intrekken, via het tabblad Koppelingen in het dashboard van de skischool. Een sleutel wordt één keer getoond, bij het aanmaken; wij bewaren een hash en kunnen hem niet opnieuw tonen. Een skischool kan tot 5 live sleutels hebben, zodat een nieuwe koppeling een eigen sleutel krijgt en een oude kan worden ingetrokken zonder aan de rest te komen. Live sleutels beginnen met ap_live_; de sleutels van de sandbox beginnen met ap_test_ en tellen apart.

Een sleutel werkt niet meer vanaf het moment dat hij wordt ingetrokken, en alle sleutels krijgen een 403 school_not_active zodra de skischool is gesloten of niet is goedgekeurd. Een skischool die alleen is geschorst (verborgen op de site, zonder eigen pagina) houdt haar sleutels: haar boekingen lopen door, en de API ook. Sleutels kun je niet via de API zelf beheren. Om een sleutel te vervangen: maak de nieuwe sleutel aan, zet je systeem erop over en trek daarna de oude in – in de tussentijd zijn ze allebei geldig. Het dashboard toont wanneer elke sleutel voor het laatst is gebruikt, tot op 5 minuten nauwkeurig. Sleutels zijn voor servers: zet er nooit een in een browser of een mobiele app.

Authenticatie, headers en body

Elk verzoek stuurt de sleutel mee als bearer-token. De basis-URL is https://alpinepal.com/api/v1; antwoorden zijn JSON, worden nooit gecachet en hebben elk een X-Request-Id die je ons kunt doorgeven. Tijdstippen op de dag volgen de klok van het skigebied, als HH:mm; datums zijn YYYY-MM-DD, en tijdstempels zijn ISO 8601 in UTC met milliseconden.

De body van een verzoek moet JSON zijn, met een header Content-Type: application/json (anders 415), kleiner dan 256 KB (413), en wordt streng gecontroleerd: een onbekend veld geeft een 400 validation_error met de paden die niet kloppen. Een lege body geldt als een leeg object. Een methode die het pad niet ondersteunt, geeft een 405 met een Allow-header; een schuine streep aan het eind geeft een 404.

HeaderWaar
Authorization: Bearer <sleutel>Elk verzoek
Content-Type: application/jsonElk verzoek met een body
X-Request-IdElk antwoord, ook bij fouten; geef hem door als je ons schrijft
Cache-Control: no-store, Vary: AuthorizationElk antwoord
WWW-Authenticate: Bearer realm="alpinepal-api"401-antwoorden
Allow405-antwoorden
Retry-After (seconden)429-antwoorden; de body herhaalt het als details.retryAfterSeconds
curl
curl https://alpinepal.com/api/v1/me \
  -H "Authorization: Bearer ap_live_…"

Er is geen CORS: de API werkt van server tot server, en een verzoek vanuit een browser mislukt. Dat is met opzet, want een sleutel in een browser is een sleutel die iedereen kan lezen.

Verzoeklimieten

Per sleutel, in vaste vensters van een minuut: 600 leesverzoeken (GET) en 120 schrijfverzoeken (al het andere, DELETE inbegrepen) per minuut. Daarboven is het antwoord 429 rate_limited met een Retry-After-header, in seconden. Een testsleutel heeft een eigen tegoed. Mislukte authenticatie is per IP-adres beperkt, 20 in 10 minuten; daarna krijgt elke volgende poging een 429, nog voordat de sleutel zelfs maar wordt opgezocht. Webhooktests en replays delen een apart tegoed van 10 in 10 minuten per sleutel, want elk daarvan is een verzoek aan de server van iemand anders.

Fouten

Elke fout heeft dezelfde vorm: een HTTP-status, een vaste code om op te vertakken, een bericht voor mensen en de request-id. Vijf codes hebben details: validation_error noemt de foute velden als [{path, message}] (maximaal 10); booking_state zegt in welke status de boeking nu is ({bookingStatus}); booking_conflict noemt de uren die in de weg zitten en, als die boeking betaald is, de id en referentie ervan; rate_limited herhaalt de wachttijd als {retryAfterSeconds}; inside_notice_window geeft de minimale boekingstermijn van de skileraar in uren ({hours}).

StatusCodesWanneer
400validation_error, invalid_cursor, invalid_jsonHet verzoek zelf klopt niet
401unauthenticated, invalid_api_keyGeen sleutel, of een sleutel die ongeldig, onbekend of ingetrokken is of bij de verkeerde omgeving hoort
403forbidden, school_not_active, managed_by_school, not_managed_by_school, not_sandboxDe sleutel is in orde, maar deze actie is niet aan jou; not_sandbox is een live sleutel op een sandbox-endpoint
404not_foundBestaat niet, of is niet een van je skileraren. We bevestigen nooit dat iets buiten je skischool bestaat
405method_not_allowedVerkeerde methode voor het pad; Allow noemt de juiste
409booking_state, pack_pending, slot_conflict, booking_conflict, external_ref_exists, concurrent_update, not_unassigned, not_on_sale, cancel_window_closed, lesson_started, lesson_ended, pack_day_fixed, format_fixed_time, format_price_differs, group_lesson_fixed_time, group_lesson_not_assignable, group_lesson_full, group_lessons_disabledEen echt statusconflict; lees de huidige status en beslis opnieuw
413, 415payload_too_large, unsupported_media_typeBody's moeten JSON zijn en kleiner dan 256 KB
422outside_availability, in_the_past, too_far_ahead, outside_ski_day, invalid_availability, api_blocks_read_only, limit_reached, invalid_webhook_url, unknown_event, range_too_long, unknown_resort, resort_required, price_below_minimum, invalid_price, no_resorts, invalid_max_students, currency_mismatch, invalid_duration, too_many_students, instructor_not_verified, wrong_discipline, invalid_duration_prices, invalid_season_prices, invalid_included_students, invalid_lesson_length, invalid_notice, invalid_disciplines, invalid_lesson_formats, formats_not_sold, format_required, unknown_format, format_not_on_date, format_wrong_time, invalid_group_lesson, group_lesson_wrong_date, inside_notice_windowCorrect opgebouwd, maar in strijd met een regel van de agenda, de les of de skischool
429rate_limitedZie Retry-After
500internalOnze fout; met de request-id kunnen we hem vinden
409 Conflict
{
  "error": {
    "code": "booking_state",
    "message": "Invalid booking state",
    "requestId": "req_929e0418a4b7",
    "details": { "bookingStatus": "confirmed" }
  }
}

De gegevens lezen

Id's. Skileraren hebben de id van hun account (user_…), of unclaimed_… zolang een profiel dat je skischool zonder account heeft aangemaakt nog niet is overgenomen; de plek van een skischool voor lessen zonder toegewezen skileraar is pool_<schoolId>; de skileraren van de sandbox zijn sandbox_ia_… en sandbox_ib_…. Boekingen zijn b_…, events evt_…, bezorgingen whd_…, endpoints wh_…, verzoeken req_…. Referenties zijn AP-<jaar>-<5 cijfers> voor echte boekingen en SB<6 hex> in de sandbox.

Tijden. date, startTime en endTime zijn de lokale tijd van resortTimeZone, nooit UTC; de tijdstempels (createdAt, updatedAt, paidAt, instructorResponseDeadline…) zijn UTC. Valt een les in de nacht waarin de klok wordt verzet op een uur dat twee keer voorkomt, dan geldt het eerste; een uur dat die nacht niet bestaat, schuift naar voren. endTime mag 24:00 zijn. Of een tijdstip in het verleden ligt, wordt beoordeeld op de klok van het skigebied.

Geld. subtotal = (hourlyPrice + extraStudentPrice × max(0, numberOfStudents − includedStudents)) × durationHours, afgerond op de valuta; dat is wat de klant betaalt. hourlyPrice is die van de lesdatum: die van een seizoen (seasonPrices) als de datum erin valt, anders die van de skileraar of de plek zelf; includedStudents is hoeveel leerlingen die prijs dekt, 1 tenzij de skischool meer instelt. platformFee is onze commissie: 10%, of de hogere die je skischool schriftelijk met ons heeft afgesproken; elke boeking houdt het percentage waartegen ze is verkocht. netAmount is de rest, subtotal − platformFee, die op het saldo van je skischool wordt bijgeschreven als de les is afgerond, wie hem ook gaf en wie die skileraar ook beheert – en die vervalt als de boeking wordt terugbetaald. Elke skileraar in je team rekent af in de valuta van je skischool; uurprijzen hebben een minimum per valuta (10 EUR, 11 USD, 10 CHF) en extraStudentPrice is 0 of meer. Materiaal dat de klant met de les bij je skischool huurt, telt ook mee in subtotal (equipment.amount) en wordt behandeld als de les.

VeldWaarden
bookingStatusZie de tabel onder Boekingen
paymentStatuspending, processing, succeeded, failed, authentication_required, refund_pending, refunded – alleen succeeded en de twee terugbetalingsstatussen bereiken ooit de API
acceptedViainstructor, school, auto, customer, of null zolang niet geaccepteerd
studentLevelsEén item per deelnemer: { level: beginner | intermediate | advanced, age: adult | child }; leeg in de sandbox
disciplineski of snowboard – die van de les; bij de disciplines van een skileraar kan ook both staan
language / customer.languageDe taal van de les als code van twee letters, en de taal waarin de klant de site gebruikt (en, es, it, fr, de, pl, nl, ru)
meetingPointstatus not_agreed, proposed of confirmed, plus title zodra er een is
verificationStatus (skileraar)draft, pending_review, verified, rejected

Je skischool, skigebieden en skileraren

EndpointGeeft terug
GET /De descriptor: naam, versie, links naar deze pagina en naar de specificatie (geen sleutel nodig)
GET /meJe skischool: id, slug, name, currency, country, freeCancellationHours (haar annuleringsbeleid, in uren) en sandbox (true achter een testsleutel)
GET /resortsElk skigebied waar een van je skileraren of je plek voor lessen zonder toegewezen skileraar lesgeeft, met tijdzone en valuta
GET /instructorsJe team, actief en inactief; skileraren die de skischool hebben verlaten, staan er niet meer in, en de plek zonder toegewezen skileraar staat er nooit in
GET /instructors/{id}Eén skileraar: tarieven, lesduren, buffer, minimale boekingstermijn, disciplines, talen, verificationStatus, managedBy, autoAcceptBookings
PATCH /instructors/{id}De tarieven – hourlyPrice, extraStudentPrice, extraStudentPer, durationPrices, seasonPrices, includedStudents, groupClassesEnabled en groupPricePerStudent – voor iedereen in je team (de uurprijs en de prijs per plaats hebben een minimum per valuta, een negatieve extraStudentPrice geeft een 422 invalid_price, en groepslessen aanzetten vereist een prijs per plaats); een nieuwe groupPricePerStudent past ook de prijs van de actieve groepslessen aan. autoAcceptBookings alleen voor een skileraar die door de skischool wordt beheerd. Alles wordt eerst gecontroleerd en samen weggeschreven: een body die op één veld wordt geweigerd, verandert niets

managedBy is de regel die bepaalt wie antwoordt: staat er instructor, dan zijn de agenda en de aanvragen van de skileraar en kan de API lezen maar niet schrijven; staat er school, dan schrijft de skischool – en dus de API – de agenda en beantwoordt zij de aanvragen, en kijkt de skileraar mee. Het wordt ingesteld vanuit het team in het dashboard, door een persoon, en kan niet via de API worden gewijzigd. Geef je een agenda terug aan de skileraar, dan worden de externe blokken verwijderd die je systeem erin heeft gezet.

Als een skileraar de skischool verlaat, verdwijnen de boekingen van die skileraar uit GET /bookings en stoppen de events, terwijl wat al is verdiend op je saldo blijft. Daarvoor en voor een wijziging van managedBy komt er geen event: vergelijk GET /instructors bij elke synchronisatie.

unclaimed markeert een skileraar die de skischool zonder account in het dashboard heeft toegevoegd: niemand logt in als die skileraar, dus het profiel staat altijd op managedBy school en de skischool handelt alles af, ook de chat. Als de persoon het profiel overneemt met een eigen account, wordt unclaimed false en verandert de id van de skileraar in een definitieve: alle paden en het filter instructorId blijven de oude id accepteren, de skileraar draagt die als previousId, en events noemen vanaf dan de nieuwe. Geen enkel event meldt dit: vergelijk GET /instructors bij elke synchronisatie en koppel op previousId.

Beschikbaarheid

De beschikbaarheid van een skileraar is een lijst met blokken. Een blok is available, blocked of vacation, hoort bij een skigebied waar de skileraar lesgeeft, loopt van startTime tot endTime (endTime mag 24:00 zijn) en is gekoppeld aan een dag van de week (dayOfWeek 0–6, zondag eerst, eventueel met since en until als eerste en laatste datum waarop het geldt) of aan één datum – precies een van de twee. Nieuwe available-uren moeten tussen 07:00 en 21:00 uur vallen, lokale tijd van het skigebied (anders een 422 outside_ski_day); blocked- en vacation-blokken mogen elk tijdstip beslaan. Klanten kunnen alleen boeken binnen available-blokken die door niets anders worden overlapt.

PUT /instructors/{id}/availability vervangt het hele rooster – elk skigebied, elk blok, tot 400 – door wat je stuurt, precies zoals het opslaan van de agenda in het dashboard. Id's zijn optioneel: de server geeft een id aan een blok zonder id, behoudt de id's die je meestuurt en weigert duplicaten of het voorvoegsel xb_ van externe blokken. Externe blokken (zie hieronder) worden door PUT nooit aangeraakt en mogen er niet in voorkomen. Een ongeldig blok, een onbekend skigebied, beide ankers tegelijk of een since na de until van het blok geeft een 422 invalid_availability. Blokken die niet meer kunnen gelden (een datum die voorbij is, een weekblok na zijn until), vallen weg: PUT slaat ze niet op en GET geeft ze niet terug. Een datum is voorbij als hij overal voorbij is, om 12:00 UTC de volgende dag.

GET /instructors/{id}/availability
{
  "blocks": [
    { "id": "ab_1", "resortId": "r_baqueira", "type": "available",
      "dayOfWeek": 6, "startTime": "09:00", "endTime": "13:00",
      "since": "2026-12-19", "until": "2027-04-15" },
    { "id": "ab_2", "resortId": "r_baqueira", "type": "vacation",
      "date": "2026-12-25", "startTime": "07:00", "endTime": "21:00" },
    { "id": "xb_7c1d", "resortId": "r_baqueira", "type": "blocked",
      "date": "2027-01-10", "startTime": "10:00", "endTime": "12:00",
      "source": "api", "externalRef": "lueira:LS-4471" }
  ]
}

GET …/availability/expanded doet het rekenwerk voor je: voor één skigebied en tot 62 dagen geeft het per dag de vrije periodes die een klant zou kunnen boeken en de bezette, elke bezette periode gelabeld als booking (een bevestigde les), pending (een betaalde aanvraag die op antwoord wacht) of external (een blok uit de API). Het houdt rekening met de buffer tussen lessen en de minimale boekingstermijn die de skileraar heeft ingesteld; gaten die korter zijn dan de minimale lesduur laat het niet weg, dus een gat van dertig minuten staat als vrij.

De uitgebreide weergave

GET /instructors/{id}/availability/expanded?resortId=r_baqueira&from=2027-01-10&to=2027-01-11
{
  "instructorId": "user_2Zk9qX1aR7",
  "resortId": "r_baqueira",
  "timeZone": "Europe/Madrid",
  "days": [
    { "date": "2027-01-10",
      "free": [{ "start": "09:00", "end": "10:00" }, { "start": "12:00", "end": "13:00" }],
      "busy": [{ "start": "10:00", "end": "12:00", "kind": "external" }] },
    { "date": "2027-01-11",
      "free": [{ "start": "09:00", "end": "13:00" }],
      "busy": [] }
  ]
}

Externe blokken

Een les die je buiten Alpine Pal hebt verkocht, houdt de skileraar bezet. Leg hem vast als extern blok en het tijdslot verdwijnt uit de zoekresultaten, in elk skigebied waar de skileraar lesgeeft – iemand kan niet in twee dalen tegelijk zijn. Voor blokken moet de skischool de skileraar beheren (managedBy = school).

Elk blok heeft je eigen externalRef. Dezelfde referentie nog eens sturen met dezelfde datum en uren is idempotent en geeft 200 met het opgeslagen blok, welke resortId je ook meegeeft; dezelfde referentie met andere uren geeft een 409 external_ref_exists – verwijder het blok en maak het opnieuw aan. Een blok dat overlapt met een actieve boeking geeft een 409 booking_conflict met de uren die in de weg zitten en, als die boeking betaald is, de id en referentie ervan: die les is bij ons al verkocht.

Een skileraar heeft vanaf vandaag maximaal 400 externe blokken; nog één erbij geeft een 422 limit_reached. Een blok waarvan de datum voorbij is (om 12:00 UTC de volgende dag), verdwijnt vanzelf: GET /blocks toont het niet meer, een DELETE erop geeft een 404 en het telt niet meer mee voor de 400.

EndpointOpmerkingen
GET /instructors/{id}/blocksOptionele filters from, to, externalRef
POST /instructors/{id}/blocksdate, startTime, endTime, externalRef (1–120 tekens), optioneel resortId (een skigebied waar de skileraar lesgeeft; standaard het eerste); 07:00–21:00, niet in het verleden op de klok van dat skigebied, maximaal 30 dagen voorbij de boekingshorizon van 182 dagen
DELETE /instructors/{id}/blocks/{blockId}204; een blok uit het rooster (niet uit de API) geeft hier een 404
POST /instructors/{id}/blocks
curl -X POST https://alpinepal.com/api/v1/instructors/user_2Zk9qX1aR7/blocks \
  -H "Authorization: Bearer ap_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "date": "2027-01-10", "startTime": "10:00", "endTime": "12:00",
        "externalRef": "lueira:LS-4471" }'

Bij het accepteren van een boekingsaanvraag wordt het tijdslot opnieuw gecontroleerd: een extern blok op die uren geeft, net als een actieve boeking of een groepsles, een 409 slot_conflict, en dan moet de skischool de aanvraag afwijzen of een ander tijdstip voorstellen. Op de plek zonder toegewezen skileraar wordt bij het accepteren niets opnieuw gecontroleerd: de plaatsen ervan zijn geteld toen de klant betaalde. Zodra je blok met 201 is beantwoord, is het uur van jou: een betaling die er al voor bezig was, wordt terugbetaald zodra die betaling binnenkomt, en bereikt je als booking.expired.

Lessen zonder toegewezen skileraar

Een skischool kan uren verkopen zonder te zeggen wie ze gaat geven: de klant boekt en betaalt, en de skischool wijst een skileraar toe wanneer ze wil – via de API, vanuit het dashboard, of op het ontmoetingspunt met wie er vrij is. In de API is dit de eigen plek van de skischool: die heeft een eigen agenda, een eigen prijs en een eigen schakelaar voor directe bevestiging, staat niet bij /instructors, en de boekingen ervan komen binnen met unassigned: true en instructorId gelijk aan de plek (pool_…). De plek verkoopt zoveel lessen tegelijk als de agenda plaatsen heeft: een available-blok kan een capacity hebben (tot 50; 1 als die ontbreekt), geteld in het skigebied van het blok, minuut voor minuut – de eigen blokken van een dag vervangen de wekelijkse voor de minuten die ze beslaan, en binnen elk van beide tellen overlappende blokken op. Een les heeft een plaats nodig voor de hele duur. De groepslessen van de plek nemen geen plaatsen in: ze lopen naast de privélessen en naast elkaar.

GET /unassigned toont de instellingen; PATCH /unassigned stelt een of meer van hourlyPrice, extraStudentPrice (0 of meer; een negatieve geeft een 422 invalid_price), extraStudentPer, durationPrices, seasonPrices, includedStudents, autoAcceptBookings en maximumStudents in. De eerste PATCH maakt de plek aan; tot dan geven de agenda- en blok-endpoints ervan een 404. Er is geen schakelaar: active zegt of de plek te koop is, en dat is ze zodra de uurprijs boven het minimum ligt (of, als ze formules verkoopt, zodra de formules en seizoenen compleet zijn); ze verkoopt in de uren die de agenda openzet, dus een skischool die deze lessen niet wil, zet er geen open. Een active in een PATCH wordt genegeerd. Verder werkt de agenda van de plek zoals die van een skileraar, in de skigebieden van de skischool.

Vaste formules. In plaats van per uur kan een skischool de privélessen van haar plek verkopen in vaste formules, die je in het dashboard instelt: maximaal 6 (ochtend, middag, hele dag of anders, elk met een begin en een einde) en seizoenen, periodes met een prijs per formule, per les voor de hele groep. saleMode in GET /unassigned zegt welke van de twee geldt, en lessonFormats geeft de formules en seizoenen (alleen-lezen in de API). Zolang de plek formules verkoopt, is elke privéles ervan een van die formules: een formule zonder prijs in het seizoen van een datum wordt die dag niet verkocht, en op een datum buiten elk seizoen wordt niets verkocht. Een formule heeft de hele tijd een vrije plaats nodig, zoals elke les van de plek, en heeft niet de limiet van 6 uur van lessen per uur. Zo'n plek is te koop zodra de formules en seizoenen compleet zijn, wat de uurprijs ook is. De boekingen ervan hebben format (id en soort), subtotal is de prijs van die formule op die dag, en propose-time verzet zo'n les alleen naar dezelfde tijden op een dag die de formule tegen de betaalde prijs verkoopt (anders 409 format_fixed_time of format_price_differs). In de sandbox accepteert POST /sandbox/bookings op de plek formatId en gebruikt het de formules van je skischool.

EndpointHetzelfde als
GET, PUT /unassigned/availabilityGET, PUT /instructors/{id}/availability
GET /unassigned/availability/expandedDe uitgebreide weergave, voor een van de skigebieden van de skischool; elke vrije periode zegt ook hoeveel plaatsen er nog over zijn (places)
GET, POST /unassigned/blocks · DELETE /unassigned/blocks/{blockId}Externe blokken op de plek: elk neemt één plaats in in zijn skigebied (bij meer dan één skigebied is resortId verplicht: 422 resort_required), en wordt alleen geweigerd (409 booking_conflict) waar er anders meer lessen dan plaatsen zouden zijn, met een van de onze ertussen
GET /bookings?instructorId=pool_<schoolId>Alleen de lessen zonder toegewezen skileraar
POST /bookings/{id}/assigninstructorId in de body: verplaatst een les zonder toegewezen skileraar naar iemand in je team – zie Een aanvraag beantwoorden
GET /unassigned
{
  "instructorId": "pool_sch_2a71",
  "configured": true,
  "active": true,
  "hourlyPrice": 55,
  "extraStudentPrice": 15,
  "autoAcceptBookings": false,
  "maximumStudents": 6,
  "currency": "EUR",
  "resorts": ["r_baqueira", "r_grandvalira"],
  "saleMode": "formats",
  "lessonFormats": {
    "formats": [
      { "id": "fmt_3b9c1e7a20", "kind": "morning", "startTime": "09:00", "endTime": "13:00", "durationHours": 4 },
      { "id": "fmt_8f02d6c4e1", "kind": "full_day", "startTime": "09:00", "endTime": "17:00", "durationHours": 8 }
    ],
    "seasons": [
      {
        "id": "sea_8d41f0c2b9",
        "name": "Low season",
        "ranges": [{ "from": "2026-11-29", "to": "2026-12-19" }, { "from": "2027-01-03", "to": "2027-02-06" }],
        "prices": [{ "formatId": "fmt_3b9c1e7a20", "price": 450 }, { "formatId": "fmt_8f02d6c4e1", "price": 700 }]
      }
    ]
  }
}

Tot de les is toegewezen, neemt de skischool de rol van de skileraar op zich: ze beantwoordt de aanvraag, chat met de klant en spreekt het ontmoetingspunt af. Eenmaal toegewezen is de boeking van de skileraar zoals elke andere, en wat de skischool er nog mee mag doen, volgt uit managedBy.

Groepslessen

Een groepsles is een les met plaatsen die losse klanten elk apart kopen: eenmalig (date) of als wekelijkse reeks (dayOfWeek, eventueel met since en until), met een capaciteit, een prijs per plaats voor de hele les, een niveau, een doelgroep (audience: iedereen, kinderen of volwassenen – een kinderles kan een minAge en maxAge noemen, en elke deelnemer die een plaats boekt, moet daarbinnen vallen), een vast ontmoetingspunt en een minimumaantal deelnemers om door te gaan. De groepslessen van een skileraar nemen hun prijs per plaats over uit de eigen instellingen van die skileraar (groupClassesEnabled en groupPricePerStudent, te schrijven via PATCH /instructors/{id}); alleen de plek zonder toegewezen skileraar noemt de prijs bij het aanmaken, en een prijswijziging past de actieve groepslessen aan. Groepslessen worden vanuit het dashboard gepubliceerd, aan een skileraar gegeven en geannuleerd – in de API zijn ze in v1 alleen-lezen – en horen bij een skileraar uit je team of bij de plek zonder toegewezen skileraar.

GET /instructors/{id}/classes (en GET /unassigned/classes) tonen de actieve groepslessen. GET …/classes/expanded?from&to zet ze om in concrete datums met hun bezette plaatsen, inclusief betalingen die nog binnen hun reserveringstijd van 10 minuten lopen – maximaal 62 dagen per aanroep. In de agenda van een skileraar staan de uren van een groepsles in de uitgebreide weergave van de beschikbaarheid ook als busy, en een extern blok erop wordt geweigerd met een 409 booking_conflict: die uren kun je niet elders verkopen. Achter de plek zonder toegewezen skileraar staat een heel team: de groepslessen ervan nemen niets af van de privéplaatsen, lopen ernaast en naast elkaar, en een extern blok botst er nooit mee.

GET /instructors/{id}/classes
{
  "data": [
    {
      "id": "gc_8c4d21f0a9b3",
      "instructorId": "user_2NcT4jQ0uW",
      "unassigned": false,
      "resortId": "r_grandvalira",
      "discipline": "ski",
      "language": "en",
      "dayOfWeek": 3,
      "since": "2026-12-23",
      "until": "2027-03-31",
      "startTime": "10:00",
      "endTime": "12:00",
      "capacity": 8,
      "minStudents": 3,
      "pricePerStudent": 35,
      "currency": "EUR",
      "level": "beginner",
      "meetingPoint": { "title": "Top of the gondola" },
      "cancelledDates": ["2027-01-20"],
      "createdAt": "2026-09-22T09:00:00.000Z",
      "updatedAt": "2026-09-22T09:00:00.000Z"
    }
  ]
}
GET /instructors/{id}/classes/expanded?from=2027-01-11&to=2027-01-17
{
  "data": [
    {
      "classId": "gc_8c4d21f0a9b3",
      "date": "2027-01-13",
      "startTime": "10:00",
      "endTime": "12:00",
      "capacity": 8,
      "seatsTaken": 5
    }
  ]
}

Elke verkochte plaats komt via /bookings binnen als een gewone boeking met een groupClassId, bevestigd op het moment dat ze betaald is (acceptedVia auto). propose-time en assign weigeren een plaats – het tijdstip hoort bij de groepsles en bij alle andere deelnemers, en een groepsles gaat in zijn geheel naar een skileraar, vanuit het dashboard. Een groepsles annuleren (of één datum van een reeks) betaalt elke plaats volledig terug en stuurt één booking.cancelled per plaats; een groepsles die 24 uur voor de start nog onder zijn minStudents zit, wordt op dezelfde manier afgelast, met cancelledBy platform.

Boekingen

Alleen betaalde boekingen bereiken de API: een boeking bestaat voor jou vanaf het moment dat de betaling binnenkomt (paidAt), en een terugbetaalde boeking blijft in de lijst staan met haar paymentStatus. Een klant kiest een skileraar en een tijdslot, betaalt, en de aanvraag komt terecht bij de skileraar of bij de skischool. Vanaf dan wordt ze beantwoord, of ze verloopt zonder antwoord en de klant krijgt zijn geld terug.

De klok: de antwoordtermijn is 48 uur vanaf de betaling, nooit later dan het begin van de les; een voorstel of het afwijzen ervan laat hem opnieuw ingaan (nooit voorbij de vroegste van de oorspronkelijke en de voorgestelde starttijd); een controleronde die elk uur draait, laat verlopen wat over tijd is, dus het verlopen kan tot een uur later vallen. Een bevestigde les wordt upcoming 24 uur voor de start en in_progress terwijl hij bezig is. Hij wordt afgerond als de klant hem bevestigt, of vanzelf 48 uur na het einde; met de afronding komt je deel vrij. De klant kan kosteloos annuleren tot freeCancellationHours uur voor de start: de termijn van de boeking zelf, die het annuleringsbeleid van je skischool had toen ze werd gemaakt (standaard 48), en die de boeking houdt, hoe het beleid later ook verandert.

bookingStatusBetekenis
pending_instructor_approvalBetaald, wacht op antwoord vóór instructorResponseDeadline
change_proposedEr is een ander tijdstip voorgesteld; de klant accepteert het, wijst het af of laat het verlopen. Het oorspronkelijke tijdslot blijft vastgehouden; het voorgestelde niet, en bij acceptatie wordt het opnieuw gecontroleerd
confirmedGeaccepteerd (acceptedVia zegt door wie: instructor, school, auto, customer)
upcoming, in_progressBevestigd en vlak voor of tijdens de les
completedAfgerond; je deel van het geld is vrijgegeven
rejected_by_instructor, expiredVolledig terugbetaald
cancelled_by_customer, cancelled_by_instructorGeannuleerd; terugbetaling volgens het annuleringsbeleid
no_show_customer, no_show_instructor, disputed, refundedIngesteld vanuit het dashboard of door ons
A booking
{
  "id": "b_3e8c1f",
  "reference": "AP-2027-51234",
  "instructorId": "user_2Zk9qX1aR7",
  "unassigned": false,
  "resortId": "r_baqueira",
  "resortTimeZone": "Europe/Madrid",
  "date": "2027-01-12",
  "startTime": "10:00",
  "endTime": "12:00",
  "durationHours": 2,
  "discipline": "ski",
  "language": "en",
  "numberOfStudents": 2,
  "studentLevels": [{ "level": "beginner", "age": "adult" }, { "level": "beginner", "age": "adult" }],
  "groupClassId": null,
  "format": null,
  "pack": null,
  "equipment": null,
  "customerMessage": "Two adults, first time on skis.",
  "customer": { "name": "Santi P.", "language": "es" },
  "meetingPoint": { "status": "not_agreed" },
  "subtotal": 130,
  "platformFee": 13,
  "netAmount": 117,
  "currency": "EUR",
  "bookingStatus": "pending_instructor_approval",
  "paymentStatus": "succeeded",
  "acceptedVia": null,
  "instructorResponseDeadline": "2027-01-06T10:00:00.000Z",
  "freeCancellationHours": 48,
  "proposed": null,
  "createdAt": "2027-01-04T09:12:41.000Z",
  "updatedAt": "2027-01-04T09:13:02.000Z",
  "paidAt": "2027-01-04T09:13:02.000Z",
  "completedAt": null,
  "refundedAt": null,
  "url": "https://alpinepal.com/en/booking/b_3e8c1f"
}

Over de klant krijg je een korte naam (voornaam en initiaal), de voorkeurstaal en het bericht – genoeg om de les te geven, en wat onze privacyverklaring de klant belooft. E-mailadres, telefoonnummer en accountkenmerken worden nooit verstuurd; het gesprek met de klant blijft in de chat op Alpine Pal, waar de skileraar zit.

Opvragen en gesynchroniseerd blijven

GET /bookings toont de betaalde boekingen van je skileraren, oudste wijziging eerst, met filters voor status (een lijst met komma's), instructorId, from en to (lesdatums) en updatedSince (elk ISO 8601-tijdstip; inclusief). Pagina's werken met een keyset op (updatedAt, id): volg nextCursor tot die null is. De cursor is ondoorzichtig en verloopt nooit, maar hoort bij de filters waarmee hij is gemaakt – verander je die, begin dan opnieuw; een verzonnen cursor geeft een 400.

Voor een synchronisatie elke nacht of elk uur is het patroon hetzelfde: onthoud de nieuwste updatedAt die je hebt gezien en vraag alles op sinds dat moment, met de waarde precies zoals je die hebt ontvangen. Webhooks laten je weten op het moment dat er iets verandert; met dit endpoint haal je de achterstand in na een storing. updatedAt verandert ook bij wijzigingen die geen event sturen – de les wordt upcoming of in_progress, er is een herinnering verstuurd – dus een regel kan in een synchronisatie opduiken zonder dat er zichtbaar iets anders is.

Everything that changed since your last sync
curl "https://alpinepal.com/api/v1/bookings?updatedSince=2027-01-04T09:00:00Z&limit=100" \
  -H "Authorization: Bearer ap_live_…"
# → { "data": [ … ], "nextCursor": "eyJ1IjoiMjAyNy0…" }
# then, while nextCursor is not null:
curl "https://alpinepal.com/api/v1/bookings?updatedSince=2027-01-04T09:00:00Z&limit=100&cursor=eyJ1IjoiMjAyNy0…" \
  -H "Authorization: Bearer ap_live_…"

Een aanvraag beantwoorden

Acties staan open voor de skischool bij skileraren die ze beheert en bij haar eigen plek voor lessen zonder toegewezen skileraar; bij een skileraar die zichzelf beheert, is het antwoord 403 not_managed_by_school. Elke actie is een voorwaardelijke update: is de boeking niet meer in de status die de actie verwacht, dan is het antwoord 409 booking_state met de huidige status in details, en verandert er niets. Een actie herhalen is dus veilig, en de manier om na te gaan wat er is gebeurd, is GET /bookings/{id}.

EndpointRegel
POST /bookings/{id}/acceptVanuit pending; de les mag nog niet begonnen zijn. Het tijdslot wordt opnieuw gecontroleerd: een actieve boeking of groepsles die het overlapt (inclusief de buffer van de skileraar, ook betaalde aanvragen), of een extern blok op die uren, geeft een 409 slot_conflict. Op de plek zonder toegewezen skileraar wordt niets opnieuw gecontroleerd: de plaatsen ervan zijn bij de betaling geteld
POST /bookings/{id}/rejectVanuit pending of change_proposed; betaalt de klant volledig terug
POST /bookings/{id}/propose-timedate, startTime, optioneel message; alleen vanuit pending, één voorstel tegelijk. De klant accepteert het, wijst het af of laat het verlopen; het oorspronkelijke tijdslot blijft vastgehouden en het voorgestelde niet
POST /bookings/{id}/cancelEen bevestigde, aanstaande of lopende les die nog niet is afgelopen; betaalt de klant volledig terug
POST /bookings/{id}/assigninstructorId in de body; alleen een les met unassigned: true, vanuit pending, confirmed of upcoming. De skileraar moet in je team staan, actief en geverifieerd zijn, die discipline in dat skigebied geven, in die valuta afrekenen, zoveel deelnemers en die lesduur toestaan en op dat moment vrij zijn; de gepubliceerde uren worden niet gecontroleerd – dat is jouw beslissing. De termijn verschuift niet; vanaf hier antwoordt wie de skileraar beheert. De klant en de skileraar krijgen bericht, en er volgt een booking.updated met reason instructor_assigned
POST /bookings/{id}/propose-time
curl -X POST https://alpinepal.com/api/v1/bookings/b_3e8c1f/propose-time \
  -H "Authorization: Bearer ap_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "date": "2027-01-12", "startTime": "14:00",
        "message": "The morning is taken; would 14:00 work?" }'

Twee dingen ontbreken hier met opzet: een no-show registreren en een les afronden. Allebei verschuiven ze geld op basis van wat er op de piste is gebeurd, en allebei blijven ze een menselijke handeling in het dashboard.

Webhooks

Registreer een https-URL en we sturen elk boekingsevent daarheen met een POST – dezelfde JSON die de boekingsendpoints teruggeven, verpakt in een event. Tot 10 endpoints per skischool, elk geabonneerd op bepaalde events of met "*" op allemaal. Registreer ze vanuit het dashboard of via de API; het dashboard toont in beide gevallen het bezorglog. De URL moet https zijn, met een hostnaam die een punt bevat, zonder inloggegevens of fragment, en geen privéadres of een adres van ons.

EndpointDoet
GET /webhooksJe endpoints, inclusief secrets
POST /webhooksurl, events (1–20, of ["*"]), optioneel description; 201 met het secret
PATCH /webhooks/{id}Een of meer van url, events, description, active; weer aanzetten zet de foutteller op nul
DELETE /webhooks/{id}Verwijdert het endpoint en het bezorglog ervan
POST /webhooks/{id}/rotate-secretEen nieuw secret; het oude vervalt meteen
POST /webhooks/{id}/testEen ping, in de wachtrij gezet als gewone bezorging en meteen geprobeerd; meldt hoe die poging ging, en mislukt hij, dan wordt hij opnieuw geprobeerd zoals elke andere
GET /webhooks/{id}/deliveriesDe nieuwste eerst, zonder body: standaard 50, met ?limit= tot 100
POST /webhooks/{id}/deliveries/{deliveryId}/replayHetzelfde event (zelfde id, zelfde inhoud) nog een keer als eigen nieuwe bezorging, meteen geprobeerd en opnieuw geprobeerd zoals elke andere
POST /webhooks
curl -X POST https://alpinepal.com/api/v1/webhooks \
  -H "Authorization: Bearer ap_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://crm.example.com/alpinepal", "events": ["*"] }'
# → 201 { "id": "wh_…", "secret": "whsec_…", … }  — keep the secret
What a delivery carries
{
  "id": "evt_4b2c9e17a0d3",
  "type": "booking.confirmed",
  "apiVersion": "v1",
  "createdAt": "2027-01-04T10:02:17.000Z",
  "schoolId": "sch_2a71",
  "sandbox": false,
  "data": {
    "booking": { "id": "b_3e8c1f", "bookingStatus": "confirmed", "acceptedVia": "school", "…": "…" },
    "previousStatus": "pending_instructor_approval"
  }
}
Request headers on every delivery
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: AlpinePal-Webhooks/1.0
X-AlpinePal-Event: booking.confirmed
X-AlpinePal-Event-Id: evt_4b2c9e17a0d3
X-AlpinePal-Delivery: whd_00dd9d2f0cdd
X-AlpinePal-Signature: t=1799999999,v1=5f1c…e2a9

Events. booking.requested – er is een betaalde aanvraag binnengekomen. booking.confirmed – geaccepteerd door de skileraar of de skischool, automatisch bevestigd, of de klant heeft een voorgesteld tijdstip geaccepteerd: acceptedVia zegt welke van de drie, en previousStatus is change_proposed in het laatste geval. booking.time_proposed – de klant heeft een ander tijdstip voorgesteld gekregen. booking.rejected. booking.cancelled – cancelledBy is customer, instructor, school of platform (het account achter de boeking is verwijderd, of een groepsles is afgelast omdat het minimum niet werd gehaald). booking.expired – niemand heeft op tijd geantwoord. booking.completed – de les is afgelopen, no-shows inbegrepen. booking.updated – al het andere dat het melden waard is, met data.reason: proposal_declined, meeting_point_proposed, meeting_point_confirmed, dispute_opened, dispute_resolved, refund_settled (het geld is terug bij de klant), refund_issued (door ons) of instructor_assigned (previousInstructorId noemt de plek waar de les op stond). ping is het testevent, met een lege data; het is geen type waarop je je abonneert. Er gaat geen event uit voor upcoming, in_progress of wijzigingen in het team of in managedBy.

Wat je kunt verwachten. Bezorging gebeurt minstens één keer: een antwoord dat na onze time-out van 10 seconden binnenkomt, telt als mislukt en het event komt opnieuw, dus gebruik de event-id als sleutel en negeer herhalingen. De volgorde is niet gegarandeerd – nieuwe pogingen en de controleronde gooien events door elkaar – dus sorteer op data.booking.updatedAt. Niet elke boeking begint met booking.requested: een boeking bij een skileraar met directe bevestiging (autoAcceptBookings) komt meteen binnen als booking.confirmed, met previousStatus pending_instructor_approval. Maak de boeking in je systeem dus aan bij het eerste event dat je voor die id ziet, welk type het ook is. De payload wordt vastgelegd op het moment dat het event ontstaat; een nieuwe poging bevat wat toen gold, en een later event de nieuwere status. Een event kan verloren gaan als het ons niet lukt het vast te leggen; GET /bookings?updatedSince is het vangnet. Elk event bevat sandbox: true of false. Het log bewaart bezorgingen 30 dagen; met een replay stuur je een eerdere bezorging opnieuw.

Bezorgingen verifiëren

Elke bezorging is ondertekend met het secret van het endpoint: de header X-AlpinePal-Signature bevat een tijdstempel en een HMAC-SHA256 van de tijdstempel, een punt en de ruwe body. Verifieer die handtekening voordat je ergens op vertrouwt, vergelijk in constante tijd en weiger tijdstempels die meer dan 5 minuten van je klok afwijken. Het secret krijg je terug bij het aanmaken van het endpoint en via GET /webhooks, en je kunt het vervangen vanuit het dashboard of met POST /webhooks/{id}/rotate-secret; het oude vervalt meteen.

Verifying the signature (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody must be the exact bytes received, before any JSON parsing.
export function verifyAlpinePal(secret, rawBody, signatureHeader, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t)) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, "hex"));
}

Antwoord binnen 10 seconden met een willekeurige 2xx. Al het andere – ook een 3xx, want we volgen geen redirects – is mislukt: we proberen het opnieuw na 5 minuten, 15 minuten, 1 uur, 4 uur, 12 uur en 24 uur, in totaal ongeveer 42 uur, telkens met dezelfde event-id, zodat je dubbele bezorgingen kunt herkennen. Een endpoint wordt pas uitgezet, en de beheerders van de skischool krijgen pas bericht, als een bezorging bij alle pogingen is mislukt zonder dat er in de tussentijd ook maar één bezorging naar dat endpoint is aangekomen; een korte storing zet het niet uit, hoeveel events er ook onderweg zijn. Zolang het uit staat, worden er geen nieuwe events voor in de wachtrij gezet (met GET /bookings?updatedSince haal je ze in), en een openstaande bezorging waarvan de poging in die tijd aan de beurt is, wordt als mislukt gemarkeerd zonder te worden verstuurd; pogingen die pas na het weer aanzetten aan de beurt zijn, gaan gewoon de deur uit. Stuur de mislukte opnieuw met een replay. POST /webhooks/{id}/test zet een ping in de wachtrij als gewone bezorging, probeert hem meteen en meldt hoe die poging ging: een mislukte ping wordt op dezelfde manier opnieuw geprobeerd, telt als elke bezorging mee voor het uitzetten van het endpoint en wordt bij een endpoint dat uit staat als mislukt gemarkeerd zonder te worden verstuurd. GET /webhooks/{id}/deliveries toont de nieuwste bezorgingen eerst, standaard 50 en met ?limit= tot 100, met het aantal pogingen en zonder body: elk event heeft één bezorging per endpoint, plus één voor elke replay, met dezelfde event-id.

Sandbox

Elke skischool krijgt een testkopie zodra de eigenaar in het dashboard een testsleutel aanmaakt (Koppelingen → Sandbox): een kopie van de skischool met twee verzonnen skileraren – sandbox_ia_…, die aanvragen met de hand beantwoordt, en sandbox_ib_…, met directe bevestiging aan – en de plek voor lessen zonder toegewezen skileraar, elk met een agenda van 09:00–17:00 op alle dagen in de skigebieden van de skischool, 1 tot 6 uur per les, tot 6 deelnemers, zonder buffer en zonder minimale boekingstermijn. Testsleutels beginnen met ap_test_ en bereiken alleen de testkopie; live sleutels nooit. Al het andere is dezelfde API: de skileraren van de testkopie opvragen, hun agenda's schrijven, webhooks registreren, aanvragen beantwoorden, lessen toewijzen.

Wat een klant op de site doet, doet POST /sandbox/bookings voor jou: het maakt een betaalde boeking aan voor de skileraar die je noemt, met dezelfde controles als een echte – gepubliceerde beschikbaarheid, geen overlap – en zonder dat er geld aan te pas komt. Vanaf daar draait het echte systeem, en de acties hieronder nemen de plaats in van de klant en de klok, zodat je elk event binnen enkele minuten kunt zien. Er gaat geen e-mail de sandbox uit, en niets erin is ooit openbaar; elk event dat de sandbox verstuurt, bevat sandbox: true, en GET /me ook. POST /sandbox/reset maakt de sandbox leeg, zodat een testsuite schoon kan beginnen.

EndpointOpmerkingen
POST /sandbox/bookingsinstructorId (een skileraar van de testkopie, of de plek ervan), date, startTime, durationHours (binnen de grenzen van de skileraar); optioneel numberOfStudents, resortId, discipline, language, customerMessage. 201 met de boeking; 403 not_sandbox met een live sleutel
POST /sandbox/classesPubliceert een groepsles bij een skileraar van de sandbox of bij de plek, volgens de regels van productie; boek de plaatsen met POST /sandbox/bookings en groupClassId
POST /sandbox/bookings/{id}/accept-proposal · decline-proposalHet antwoord van de klant op een voorgesteld tijdstip
POST /sandbox/bookings/{id}/cancelDe klant annuleert, volgens dezelfde regels als op de site
POST /sandbox/bookings/{id}/completeWat de bevestiging van de klant, of de controleronde 48 uur na de les, doet: completed en je deel vrijgegeven
POST /sandbox/bookings/{id}/expireWat de controleronde na de termijn doet: expired en terugbetaald
POST /sandbox/resetVerwijdert de boekingen, chats, saldoboekingen en groepslessen van de testkopie; sleutels, webhooks, agenda's en het bezorglog blijven
POST /sandbox/bookings
curl -X POST https://alpinepal.com/api/v1/sandbox/bookings \
  -H "Authorization: Bearer ap_test_…" \
  -H "Content-Type: application/json" \
  -d '{ "instructorId": "sandbox_ia_3f9a", "date": "2027-01-10",
        "startTime": "10:00", "durationHours": 2, "numberOfStudents": 2 }'

Nog buiten bereik in de sandbox: klachten, no-shows, een terugbetaling door ons, een annulering met cancelledBy platform, en het ontmoetingspunt (de events daarvan komen van de site). Sandboxboekingen hebben een lege studentLevels en een klant die Sandbox C. heet en de site in het Engels leest.

OpenAPI

De machineleesbare beschrijving staat op https://alpinepal.com/api/v1/openapi.json – elk pad, elke methode, elke parameter en elk schema hierboven, in OpenAPI 3.1. Laat er een generator op los voor een getypeerde client, of importeer het bestand in je HTTP-tool om erdoorheen te bladeren. Het wordt zonder authenticatie geleverd en een uur lang gecachet.

Versies en wijzigingen

Dit is v1. Binnen v1 voegen we alleen dingen toe: nieuwe velden, nieuwe optionele parameters, nieuwe eventtypes, nieuwe endpoints. Je koppeling moet velden en events negeren die ze niet kent. Alles wat een bestaande client zou breken, komt in een v2 op een nieuwe basis-URL, en v1 blijft draaien terwijl je overstapt.

  • 2026-09 — v1: skischool, skigebieden, skileraren, beschikbaarheid en uitgebreide weergave, externe blokken, boekingen met keyset-paginering, accept / reject / propose-time / cancel, ondertekende webhooks met nieuwe pogingen.

  • 2026-09-18 — hourlyPrice en extraStudentPrice op PATCH /instructors/{id}; de plek voor lessen zonder toegewezen skileraar onder /unassigned en POST /bookings/{id}/assign; reason en previousInstructorId op booking.updated; de sandbox, ap_test_-sleutels en sandbox op elk event.

  • 2026-09-18 — herziening: booking_state bevat de huidige status; updatedSince accepteert elk ISO-tijdstip; de terugbetaling van een boeking zonder betaling stuurt ook refund_settled; één booking.completed per les; assign controleert de grenzen van de skileraar; meeting_point_proposed; POST /webhooks/{id}/deliveries/{deliveryId}/replay; de klant- en klokacties van de sandbox; deze pagina herschreven rond wat de code doet.

  • 2026-09-20 — PATCH /instructors/{id} schrijft zijn velden samen weg, of niets; een blok dat met 201 is beantwoord, wint nu van een betaling die op hetzelfde uur al bezig was (de klant krijgt zijn geld terug, jij ontvangt booking.expired).

  • 2026-09-22 — groepslessen: GroupClass en de datums ervan onder /instructors/{id}/classes en /unassigned/classes (alleen-lezen), groupClassId op elke boeking, cancelledBy platform als een groepsles het minimum niet haalt, POST /sandbox/classes en groupClassId op POST /sandbox/bookings.

  • 2026-09-23 — groepsprijzen bij de skileraar: groupClassesEnabled en groupPricePerStudent op Instructor en op PATCH /instructors/{id}; een nieuwe prijs per plaats past de actieve groepslessen aan; het minimum per plaats is 10 EUR / 10 USD.

  • 2026-09-24 — gedocumenteerd: een boeking bij een skileraar die direct bevestigt, komt alleen binnen als booking.confirmed, nooit als booking.requested; maak boekingen aan bij het eerste event voor hun id. Geen gedragswijziging.

  • 2026-09-27 — gedocumenteerd: een ontmoetingspunt voorstellen stuurt wel degelijk booking.updated, met reason meeting_point_proposed; de sectie over synchroniseren zei dat er geen event werd gestuurd. Geen gedragswijziging.

  • 2026-09-27 — blokken waarvan de datum voorbij is, vallen weg: PUT /instructors/{id}/availability slaat ze niet meer op en geeft ze niet meer terug, en een extern blok verdwijnt uit GET /blocks zodra de datum voorbij is en telt niet meer mee voor de limiet van externe blokken, die nu vanaf vandaag telt. Tot nu toe stapelden beide soorten zich op tot de skileraar de limiet bereikte.

  • 2026-10-05 — skileraren die de skischool zonder account toevoegt: unclaimed en previousId op Instructor. Als de persoon het profiel overneemt, verandert de id; alle paden en het filter instructorId accepteren de oude nog steeds.

  • 2026-10-06 — plaatsen op de plek zonder toegewezen skileraar: capacity op de available-blokken van de plek (meerdere lessen tegelijk, per skigebied geteld), places op elke vrije periode van de uitgebreide weergave, en de externe blokken van de plek nemen elk één plaats in in plaats van de uren te sluiten. De groepslessen van de plek botsen nu alleen nog in hun eigen skigebied. De agenda's van skileraren blijven ongewijzigd: capacity in de agenda van een skileraar geeft een 422 invalid_availability.

  • 2026-10-06 — de plek zonder toegewezen skileraar laat de groepslessen naast de privéplaatsen en naast elkaar lopen: een groepsles neemt de uren niet meer in (ze blijven in free, met hun plaatsen), en een extern blok botst er niet meer mee. GroupClass krijgt audience (all, children, adults) met optionele minAge en maxAge; een plaats in een kinderles wordt alleen verkocht aan deelnemers die als kind zijn geboekt, in een les voor volwassenen alleen aan volwassenen. POST /sandbox/classes accepteert dezelfde velden.

  • 2026-10-06 — weekblokken en reeksen groepslessen kunnen later beginnen: met since, de eerste datum (inclusief), kan een skischool een seizoen klaarzetten voordat het begint, zonder de uren nu al te verkopen. PUT /instructors/{id}/availability accepteert het op blokken met dayOfWeek (een since na de until van het blok geeft een 422 invalid_availability), GET geeft het terug, GroupClass bevat het en POST /sandbox/classes accepteert het. Zonder since geldt een reeks vanaf nu, zoals voorheen.

  • 2026-10-08 — een endpoint wordt pas uitgezet als een bezorging bij alle pogingen is mislukt zonder dat er in de tussentijd een bezorging naar dat endpoint is aangekomen. Vroeger ging het uit na tien mislukte pogingen op rij, geteld over alle bezorgingen heen, zodat een korte storing met veel events onderweg het kon uitzetten en hun nieuwe pogingen verloren gingen.

  • 2026-10-16 — de lesdag loopt van 07:00 tot 21:00 uur, lokale tijd van het skigebied (eerder van 06:00 tot 23:00): externe blokken, voorgestelde tijden en boekingen en groepslessen in de sandbox daarbuiten geven een 422 outside_ski_day, net als nieuwe available-uren daarbuiten bij PUT /instructors/{id}/availability en PUT /unassigned/availability. Al opgeslagen uren blijven staan, blocked- en vacation-blokken mogen elk tijdstip beslaan en een eerder gepubliceerde groepsles houdt zijn tijd.

  • 2026-10-16 — vaste formules op de plek zonder toegewezen skileraar (API 1.8.0): GET /unassigned krijgt saleMode en lessonFormats (formules en seizoenen met een prijs per formule, alleen-lezen), Booking krijgt format (null bij lessen die per uur zijn verkocht), POST /sandbox/bookings accepteert formatId, en propose-time op een les met een formule houdt de tijden en de prijs vast (409 format_fixed_time, format_price_differs). Voor een plek die per uur verkoopt, verandert er niets.

  • 2026-10-16 — geen schakelaar meer voor lessen zonder toegewezen skileraar (API 1.8.1): active in GET /unassigned zegt nu of de plek te koop is – dat is ze zodra de uurprijs boven het minimum ligt (of de formules en seizoenen compleet zijn) – en PATCH negeert het; een skischool die deze lessen niet wil, zet er geen uren voor open.

  • 2026-10-20 — prijzen voor meerdere uren achter elkaar (API 1.10.0): Instructor en UnassignedLessons krijgen `durationPrices`, optionele vaste prijzen voor een hele les van 2, 3 of 4 uur voor één leerling. De prijs voor 4 uur geldt ook voor langere lessen tegen zijn uurtarief, een duur zonder eigen prijs krijgt het tarief van de langste kortere duur die er een heeft, en extra leerlingen worden nog steeds per uur gerekend. Zonder deze prijzen blijft alles per uur. PATCH /instructors/{id} en PATCH /unassigned accepteren ze (null wist ze); prijzen die per uur duurder zijn dan een kortere les, of in totaal goedkoper dan een kortere, geven 422 `invalid_duration_prices`.

  • 2026-10-19 — meerdaagse pakketten (API 1.9.0): een klant kan met één betaling meerdere dagen van de cursus van een groepsles kopen (GroupClass.packs, met een prijs per persoon per leeftijdsgroep, en courseId) of meerdere dagen achter elkaar van een vaste formule (UnassignedLessons: packDays en per seizoen minDays en packPrices). Elke dag is een eigen boeking met zijn deel van de prijs en een `pack` dat zegt welk pakket en welke dag het is; studentLevels kan de `band` van elke leerling bevatten.

  • 2026-11-02 — voorwaarden per skigebied (API 1.22.0): UnassignedLessons krijgt `resortTerms`: per skigebied-id elk skigebied dat anders verkoopt, met alle prijzen en voorwaarden zoals ze daar gelden (dezelfde velden als op het hoogste niveau: `hourlyPrice`, `durationPrices`, `seasonPrices`, `saleMode`, `lessonFormats` en de rest). Bij één set voorwaarden is het `{}`, en een skigebied dat ontbreekt verkoopt met de waarden van het hoogste niveau. PATCH /unassigned accepteert `resortId` om de voorwaarden van dat skigebied te schrijven en de andere te laten zoals ze zijn; een skigebied waar de school niet werkt geeft 422 `unknown_resort`. Zonder `resortId` wordt het hoogste niveau geschreven, dat een skigebied uit `resortTerms` niet leest. `active` is true zolang er in minstens één skigebied wordt verkocht. Puur een toevoeging: zonder voorwaarden per skigebied wordt alles gelezen en verkocht als voorheen.

  • 2026-10-29 — uren waarin een skileraar bezet is in zijn eigen Google Agenda (API 1.21.1): een skileraar kan de uren die zijn Google Agenda als bezet toont op Alpine Pal laten sluiten. Ze horen niet bij het rooster: GET /instructors/{id}/availability toont ze niet en een PUT ziet noch wijzigt ze, terwijl /availability/expanded die uren buiten `free` laat, net als alles wat niet te koop is. Verder verandert er niets.

  • 2026-10-27 — prijzen per seizoen en inbegrepen leerlingen (API 1.21.0): Instructor en UnassignedLessons krijgen `seasonPrices` en `includedStudents`, allebei in te stellen met PATCH /instructors/{id} en PATCH /unassigned. `seasonPrices` bevat tot 4 seizoenen, elk met een `id`, een `name` voor het paneel (null als er geen is), 1 tot 10 datumbereiken (`from` en `to`, allebei inbegrepen) en een eigen `hourlyPrice` en `durationPrices`: een les op een datum binnen een seizoen kost de prijzen van dat seizoen, een datum buiten elk seizoen de eigen prijzen van de skileraar of de plek, zoals tot nu toe. Bereiken overlappen nooit, niet binnen een seizoen en niet tussen seizoenen, en de prijzen van elk seizoen volgen dezelfde regels als de eigen prijzen van de skileraar of de plek. Een PATCH stuurt de hele lijst (null of een lege lijst wist hem): houd de ids die een GET gaf en laat de id weg bij een nieuw seizoen. GET geeft null als er geen zijn. `includedStudents` (1 tot 20, standaard 1) is hoeveel leerlingen de prijs dekt: `extraStudentPrice` wordt alleen gerekend voor elke leerling daarboven. Seizoenen die een regel breken geven 422 `invalid_season_prices`, en een aantal buiten het bereik 422 `invalid_included_students`. Puur een toevoeging: zonder seizoenen en met één inbegrepen leerling blijft elke prijs zoals voorheen.

  • 2026-10-26 — een prijs van 0 of lager wordt geweigerd (API 1.20.4): in `durationPrices` geeft een duur die de skileraar of de plek verkoopt met een prijs van 0 of een negatieve prijs nu een 422 `invalid_duration_prices`. Tot nu toe werd die zonder melding weggelaten en antwoordde het verzoek 200, zodat die duur per uur ging. Wil je een duur geen eigen prijs geven, laat dan de sleutel weg (of stuur null voor het hele veld).

  • 2026-10-26 — correcties in de documentatie (API 1.20.3): deze pagina en de spec zeggen nu wat de API al deed. Een aanvraag accepteren controleert het tijdslot opnieuw (een actieve boeking, een groepsles of een extern blok erop geeft een 409 `slot_conflict`; op de plek zonder toegewezen skileraar wordt niets opnieuw gecontroleerd). Sleutels stoppen, met een 403 `school_not_active`, alleen als de skischool gesloten of niet goedgekeurd is; een geschorste skischool houdt haar API. Bij een endpoint dat uit staat, wordt een openstaande bezorging pas als mislukt gemarkeerd wanneer haar poging aan de beurt is; pogingen die na het weer aanzetten aan de beurt zijn, worden verstuurd. Een onbekende `extraStudentPer` geeft een 400 `validation_error`, niet de 422 `invalid_price` die het item van 1.13.0 noemde; `invalid_price` is een negatieve `extraStudentPrice`. `platformFee` is 10%, of de hogere commissie die een skischool schriftelijk heeft afgesproken, zoals de boeking werd verkocht. De voorbeeldboeking krijgt `unassigned`, `groupClassId`, `format`, `pack`, `equipment` en `freeCancellationHours`, en het ontmoetingspunt ervan zegt `not_agreed`. De foutentabel noemt alle codes, waaronder `managed_by_school`, die voor pakketten, formules en groepslessen en `inside_notice_window` (met `hours`). Bezorgingen worden standaard per 50 getoond en met `limit` tot 100, en een replay is een eigen bezorging met dezelfde event-id. Een ping is een gewone bezorging: opnieuw geprobeerd als hij mislukt, meegeteld voor het uitzetten van het endpoint en bij een endpoint dat uit staat als mislukt gemarkeerd zonder te worden verstuurd. Geen gedragswijziging.

  • 2026-10-26 — twee correcties (API 1.20.2): PATCH /instructors/{id} beoordeelt `durationPrices` nu op de lesduren van de skileraar zelf, zoals het paneel, en weigert de prijzen van iemand wiens lessen bij 2 uur beginnen dus niet meer als duurder dan een les van 1 uur die hij niet verkoopt. En met een testsleutel beschrijven GET en PATCH /unassigned de formules die de sandbox verkoopt (die van de echte skischool, die POST /sandbox/bookings al gebruikte): `saleMode` en `lessonFormats` tonen niet meer `hours` en leeg voor een plek die formules verkoopt.

  • 2026-10-26 — een dag van een pakket toewijzen (API 1.20.1): POST /bookings/{id}/assign antwoordt 409 `pack_pending` voor een dag van een meerdaags pakket dat nog op het antwoord van de skischool wacht, als de skileraar zijn boekingen zelf beheert (`managedBy: instructor`). Een pakket wordt in zijn geheel beantwoord, en die skileraar zou het deel van de skischool niet kunnen beantwoorden. Accepteer eerst het pakket, of wijs de dag toe aan een skileraar die de skischool beheert; al bevestigde dagen worden zoals voorheen een voor een toegewezen.

  • 2026-10-25 — één formule sluiten bij een skileraar (API 1.20.0): een blocked-blok met `formatId` wordt nu geaccepteerd in de agenda van een skileraar die in formules verkoopt, zoals op de plek zonder toegewezen skileraar sinds 1.15.0, en sluit alleen die formule op de datum of weekdagen ervan; de andere formules in die uren blijven te boeken. Een skileraar die de uren van de skischool volgt (`followsSchoolHours`) krijgt nu ook de sluitingen van één formule van de plek. Boekingen hielden al in elke agenda rekening met deze sluitingen.

  • 2026-10-25 — de open uren van de skischool voor haar skileraren (API 1.19.0): Instructor krijgt `followsSchoolHours`. Een skileraar die via de uitnodiging van de skischool binnenkomt, of die de skischool zonder account toevoegt, begint nu met `managedBy: school` en deze optie aan: de agenda is een kopie van de open uren van de plek zonder toegewezen skileraar, in de skigebieden waar hij lesgeeft en met één les tegelijk, en elke wijziging in het rooster van de plek komt erin terecht. De skischool zet het om in haar paneel (Privélessen). Een PUT /instructors/{id}/availability geeft vanaf dat moment eigen uren: `followsSchoolHours` wordt false. Skileraren die al in het team zitten, veranderen niet.

  • 2026-10-23 — skileraren in vaste formules (API 1.18.0): Instructor krijgt `saleMode`, `hours` of `formats`. De skischool kan nu elk van haar skileraren in vaste formules met een prijs per seizoen verkopen, zoals haar plek zonder toegewezen skileraar (in te stellen in haar paneel, onder Privélessen); hun privéboekingen hebben dan `format`, zoals die van de plek, en de uurtarieven verkopen niets. Booking.format is niet langer alleen van de plek. De 422 `format_required` luidt nu “Choose one of the lesson formats”. Puur een toevoeging: elke skileraar verkoopt per uur tot de skischool het verandert.

  • 2026-10-23 — Zwitserse frank (API 1.17.0): School.currency kan `CHF` zijn. Zwitserse skischolen en hun skileraren zetten hun prijzen in frank en worden in frank uitbetaald, dus al hun bedragen (prijzen, subtotal, uitbetalingen) zijn in CHF, met een eigen minimumprijs per uur.

  • 2026-10-23 — annuleringsbeleid per skischool (API 1.16.0): Booking krijgt `freeCancellationHours`, ook in data.booking van elk boekingsevent: tot hoeveel uur voor de start de klant kan annuleren met volledige terugbetaling, de termijn waaronder de boeking is verkocht – 48, 72, 168, 336, 672 of 1008. GET /me geeft de huidige `freeCancellationHours` van de skischool terug. De skischool kiest haar beleid in haar paneel (Profiel → Annuleringsbeleid); het geldt voor alles wat de skischool verkoopt (de privé- en groepslessen van haar skileraren en haar plek zonder toegewezen skileraar), en een boeking houdt de termijn waaronder ze is gemaakt, hoe het beleid ook verandert. Via de API alleen-lezen. Puur een toevoeging: eerdere boekingen hebben 48.

  • 2026-10-21 — één formule sluiten (API 1.15.0): een `blocked`-blok van de plek zonder toegewezen leraar mag `formatId` dragen en sluit dan alleen die formule op zijn datum of weekdagen. De plaatsen van die uren blijven voor de andere formules; de uren sluiten sloot elke formule die er een minuut mee deelde. De hele dag sluiten blijft zijn uren sluiten. GET geeft het veld terug; een PUT zonder het veld haalt de sluiting weg.

  • 2026-10-21 — formules voor Westendorf (API 1.14.0): begin en einde van een formule vallen nu op het kwartier (08:45, 13:15), niet alleen op het hele of halve uur. De `lessonFormats` van UnassignedLessons krijgt `extraStudentPrice`: wat elke leerling vanaf de tweede toevoegt aan een les met formules, één keer per les (per dag van een pakket), los van de uurtoeslag van de plek; 0 houdt de prijs voor de hele groep. In te stellen in het paneel.

  • 2026-10-20 — toeslag per leerling per les (API 1.13.0): Instructor en UnassignedLessons krijgen `extraStudentPer`, `hour` (zoals tot nu toe) of `lesson`: of `extraStudentPrice` voor elk uur van de les wordt gerekend of één keer voor de hele les. In te stellen met PATCH /instructors/{id} en PATCH /unassigned; een andere waarde geeft 422 `invalid_price`.

  • 2026-10-20 — voorwaarden van Courchevel en Westendorf (API 1.12.0): elke formule van een seizoen heeft een eigen minimum aantal dagen achter elkaar (`prices[].minDays`; de `minDays` van het seizoen is nu de kleinste daarvan en verouderd), zodat ochtenden vanaf vijf dagen en middagen los in dezelfde weken verkocht kunnen worden. UnassignedLessons krijgt `minBookingNoticeHours` (nu ook twee en drie dagen) en `disciplines`, beide in te stellen met PATCH /unassigned. Een pakket vaste formules op een plek zonder directe bevestiging wacht nu op de skischool, net als elke andere aanvraag; wie een van de dagen accepteert of afwijst, beantwoordt het hele pakket. De plek krijgt ook `minimumLessonDuration` en `maximumLessonDuration` (hele uren, PATCH /unassigned; 422 `invalid_lesson_length`): Snowsports Westendorf verkoopt twee en drie uur, nooit één. Gaat geen verkochte les per uur, dan mag `hourlyPrice` 0 zijn en blijft de plek te koop. `durationPrices` neemt een sleutel voor elke lengte van twee tot zes uur; een lengte zonder prijs gaat tegen het uurtarief van de laatste kortere lengte met een prijs. En `singleLevelBookings` (ook PATCH /unassigned): aan wordt een privéles geweigerd waarvan de leerlingen niveaus, of volwassenen en kinderen, mengen, en boekt de klant per groep; Courchevel zet een leraar per niveau en per leeftijd in.

  • 2026-10-20 — materiaalverhuur (API 1.11.0): een skischool kan materiaal verhuren bij haar lessen, tegen een prijs per dag per artikel die ze in haar paneel instelt. Booking krijgt `equipment` (null als er niets is gehuurd): het deel van deze boeking (amount, al inbegrepen in subtotal), de hele huur, de dagen en per leerling de artikelen met hun prijs en de maten (EU-schoenmaat, lengte in cm). De commissie en netAmount tellen het mee zoals de les, en een terugbetaling geeft het terug met de les.