Przejdź do treści
Dla szkół narciarskich

API dla szkół narciarskich

Połącz oprogramowanie, z którego twoja szkoła już korzysta: dostępność trafia do nas, rezerwacje wracają do ciebie podpisanym webhookiem, a odpowiedzi wysyłasz bez otwierania panelu.

Konto szkoły w Alpine Pal daje ci w jednym panelu zespół instruktorów, ich kalendarze i prośby o rezerwację. API to ten sam panel, tylko dla maszyny: channel manager, CRM albo twój własny skrypt może odczytywać i zapisywać dostępność twoich instruktorów, dowiadywać się o każdej opłaconej rezerwacji w chwili, gdy powstaje, oraz akceptować ją, odrzucać albo proponować dla niej inny termin.

Celowo jest wąskie. Klienci płacą w Alpine Pal, więc API nie tworzy rezerwacji; lekcje sprzedane gdzie indziej rejestruje jako bloki, dzięki którym kalendarz mówi prawdę. To, co na stoku przesuwa pieniądze – oznaczenie niestawienia się, zamknięcie reklamacji – pozostaje czynnością człowieka w panelu.

Uzyskanie klucza

Klucze należą do szkoły, a nie do osoby, i tylko właściciel szkoły może je tworzyć i unieważniać – w zakładce Połączenia panelu szkoły. Klucz jest wyświetlany raz, przy tworzeniu; przechowujemy tylko jego skrót (hash) i nie możemy pokazać go ponownie. Szkoła może mieć do 5 kluczy produkcyjnych, więc nowa integracja dostaje własny klucz, a stary można unieważnić bez ruszania pozostałych. Klucze produkcyjne zaczynają się od ap_live_; klucze sandboxa zaczynają się od ap_test_ i są liczone osobno.

Klucz przestaje działać w chwili unieważnienia, a wszystkie klucze dostają 403 school_not_active, gdy tylko szkoła zostanie zamknięta albo nie jest zatwierdzona. Szkoła tylko zawieszona (ukryta w serwisie, bez swojej strony) zachowuje klucze: jej rezerwacje trwają dalej, a wraz z nimi API. Kluczami nie da się zarządzać przez samo API. Aby wymienić klucz: utwórz nowy, przełącz na niego swój system, a potem unieważnij stary – w międzyczasie oba są ważne. Panel pokazuje, kiedy każdy klucz był ostatnio używany, z dokładnością do 5 min. Klucze są dla serwerów: nigdy nie umieszczaj klucza w przeglądarce ani w aplikacji mobilnej.

Uwierzytelnianie, nagłówki i treść żądań

Każde żądanie przesyła klucz jako token typu Bearer. Bazowy URL to https://alpinepal.com/api/v1; odpowiedzi są w formacie JSON, nigdy nie trafiają do cache, a każda zawiera nagłówek X-Request-Id, który możesz nam podać. Godziny są podawane według czasu ośrodka w formacie HH:mm, daty jako YYYY-MM-DD, a momenty w czasie w ISO 8601, w UTC, z milisekundami.

Treść żądania musi być w formacie JSON, z nagłówkiem Content-Type: application/json (w przeciwnym razie 415), mieć mniej niż 256 KB (413) i jest sprawdzana ściśle: nieznane pole to 400 validation_error z listą błędnych ścieżek. Pusta treść jest odczytywana jako pusty obiekt. Metoda, której ścieżka nie obsługuje, daje 405 z nagłówkiem Allow; ukośnik na końcu ścieżki daje 404.

NagłówekGdzie
Authorization: Bearer <klucz>Każde żądanie
Content-Type: application/jsonKażde żądanie z treścią
X-Request-IdKażda odpowiedź, także z błędem; podaj go, gdy do nas piszesz
Cache-Control: no-store, Vary: AuthorizationKażda odpowiedź
WWW-Authenticate: Bearer realm="alpinepal-api"Odpowiedzi 401
AllowOdpowiedzi 405
Retry-After (sekundy)Odpowiedzi 429; treść powtarza tę wartość jako details.retryAfterSeconds
curl
curl https://alpinepal.com/api/v1/me \
  -H "Authorization: Bearer ap_live_…"

Nie ma CORS: API działa między serwerami, a żądanie wysłane z przeglądarki się nie powiedzie. To celowe, bo klucz w przeglądarce to klucz, który każdy może odczytać.

Limity zapytań

Na klucz, w stałych oknach jednominutowych: odczyty (GET): 600 i zapisy (wszystko inne, także DELETE): 120 na minutę. Po przekroczeniu limitu odpowiedzią jest 429 rate_limited z nagłówkiem Retry-After, w sekundach. Klucz testowy ma osobny limit. Nieudane uwierzytelnienia są ograniczone dla każdego adresu IP: 20 w ciągu 10 min; potem każda kolejna próba dostaje 429, zanim klucz zostanie w ogóle sprawdzony. Testy webhooków i ponowne wysłania mają osobny wspólny limit: 10 w ciągu 10 min na klucz, bo każde z nich to żądanie do cudzego serwera.

Błędy

Każdy błąd ma tę samą kopertę: status HTTP, stały code, według którego możesz rozgałęziać logikę, komunikat dla człowieka i identyfikator żądania. Pięć kodów zawiera details: validation_error wymienia błędne pola jako [{path, message}] (najwyżej 10); booking_state podaje, w jakim stanie jest teraz rezerwacja ({bookingStatus}); booking_conflict wskazuje godziny, które stoją na przeszkodzie, a gdy ta rezerwacja jest opłacona – jej id i numer referencyjny; rate_limited powtarza czas oczekiwania jako {retryAfterSeconds}; inside_notice_window podaje minimalne wyprzedzenie instruktora w godzinach ({hours}).

StatusKodyKiedy
400validation_error, invalid_cursor, invalid_jsonSamo żądanie jest błędne
401unauthenticated, invalid_api_keyBrak klucza albo klucz błędny, nieznany, unieważniony lub z innego środowiska
403forbidden, school_not_active, managed_by_school, not_managed_by_school, not_sandboxKlucz jest w porządku, ale ta czynność nie należy do ciebie; not_sandbox to klucz produkcyjny użyty na endpoincie sandboxa
404not_foundNie istnieje albo nie jest jednym z twoich instruktorów. Nigdy nie potwierdzamy, że coś istnieje poza twoją szkołą
405method_not_allowedNiewłaściwa metoda dla tej ścieżki; Allow wymienia właściwe
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_disabledRzeczywisty konflikt stanu; odczytaj aktualny stan i zdecyduj ponownie
413, 415payload_too_large, unsupported_media_typeTreść musi być w formacie JSON i mieć mniej niż 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_windowPoprawne składniowo, ale łamie regułę kalendarza, lekcji lub szkoły
429rate_limitedZob. Retry-After
500internalBłąd po naszej stronie; identyfikator żądania pomoże nam go znaleźć
409 Conflict
{
  "error": {
    "code": "booking_state",
    "message": "Invalid booking state",
    "requestId": "req_929e0418a4b7",
    "details": { "bookingStatus": "confirmed" }
  }
}

Jak czytać dane

Identyfikatory. Instruktorzy mają id swojego konta (user_…) albo unclaimed_…, dopóki profil dodany przez twoją szkołę bez konta nie zostanie przejęty; pula lekcji bez przypisanego instruktora ma id pool_<schoolId>; instruktorzy sandboxa to sandbox_ia_… i sandbox_ib_…. Rezerwacje to b_…, zdarzenia evt_…, doręczenia whd_…, endpointy wh_…, żądania req_…. Numery referencyjne mają postać AP-<rok>-<5-cyfrowy numer> dla prawdziwych rezerwacji i SB<6 hex> w sandboxie.

Czas. date, startTime i endTime to czas lokalny strefy resortTimeZone, nigdy UTC; momenty (createdAt, updatedAt, paidAt, instructorResponseDeadline…) są w UTC. Lekcja w noc zmiany czasu przypada na pierwsze wystąpienie godziny, która się powtarza, a godzina, której tej nocy nie ma, przesuwa się do przodu. endTime może wynosić 24:00. To, czy dana godzina jest już w przeszłości, ocenia się według zegara ośrodka.

Pieniądze. subtotal = (hourlyPrice + extraStudentPrice × max(0, numberOfStudents − includedStudents)) × durationHours, zaokrąglone zgodnie z walutą; tyle płaci klient. hourlyPrice to stawka z dnia lekcji: stawka sezonu (seasonPrices), jeśli data w nim wypada, a w przeciwnym razie własna stawka instruktora lub puli; includedStudents mówi, ilu uczniów obejmuje ta stawka, 1, chyba że szkoła ustawi więcej. platformFee to nasza prowizja: 10% albo wyższa, uzgodniona z nami na piśmie przez twoją szkołę; każda rezerwacja zachowuje stawkę, po której została sprzedana. netAmount to reszta dla szkoły (subtotal − platformFee): trafia na saldo twojej szkoły po zakończeniu lekcji – niezależnie od tego, kto ją poprowadził i kto zarządza tą osobą – a przepada, jeśli klient otrzyma zwrot pieniędzy. Każdy instruktor z twojego zespołu pobiera opłaty w walucie twojej szkoły; stawki godzinowe mają minimum zależne od waluty (10 EUR, 11 USD, 10 CHF), a extraStudentPrice wynosi 0 lub więcej. Sprzęt, który klient wypożycza w twojej szkole razem z lekcją, również wlicza się do subtotal (equipment.amount) i jest rozliczany tak jak lekcja.

PoleWartości
bookingStatusZob. tabelę w sekcji Rezerwacje
paymentStatuspending, processing, succeeded, failed, authentication_required, refund_pending, refunded – do API trafiają w ogóle tylko succeeded i dwa stany zwrotu
acceptedViainstructor, school, auto, customer albo null, dopóki rezerwacja nie zostanie zaakceptowana
studentLevelsJeden wpis na uczestnika: { level: beginner | intermediate | advanced, age: adult | child }; puste w sandboxie
disciplineski albo snowboard – dyscyplina lekcji; w disciplines instruktora może też wystąpić both
language / customer.languageJęzyk lekcji jako dwuliterowy kod oraz język, w którym klient korzysta ze strony (en, es, it, fr, de, pl, nl, ru)
meetingPointstatus not_agreed, proposed albo confirmed oraz title, gdy miejsce spotkania już istnieje
verificationStatus (instruktor)draft, pending_review, verified, rejected

Twoja szkoła, ośrodki i instruktorzy

EndpointZwraca
GET /Deskryptor: nazwa, wersja, linki do tej strony i do specyfikacji (bez klucza)
GET /meTwoja szkoła: id, slug, name, currency, country, freeCancellationHours (jej zasady anulowania, w godzinach) i sandbox (true przy kluczu testowym)
GET /resortsKażdy ośrodek, w którym uczy któryś z twoich instruktorów albo twoja pula lekcji bez przypisanego instruktora, z jego strefą czasową i walutą
GET /instructorsTwój zespół, aktywni i nieaktywni; instruktorzy, którzy odeszli ze szkoły, znikają z listy, a pula nigdy się na niej nie pojawia
GET /instructors/{id}Jeden instruktor: stawki, długości lekcji, bufor, minimalne wyprzedzenie, dyscypliny, języki, verificationStatus, managedBy, autoAcceptBookings
PATCH /instructors/{id}Stawki – hourlyPrice, extraStudentPrice, extraStudentPer, durationPrices, seasonPrices, includedStudents, groupClassesEnabled i groupPricePerStudent – dla każdego z zespołu (stawka godzinowa i cena za miejsce mają minimum zależne od waluty, ujemna wartość extraStudentPrice daje 422 invalid_price, a włączenie lekcji grupowych wymaga ceny za miejsce); nowa wartość groupPricePerStudent aktualizuje też cenę aktywnych lekcji grupowych. autoAcceptBookings tylko dla instruktora, którym zarządza szkoła. Wszystko jest najpierw sprawdzane, a potem zapisywane razem: treść odrzucona z powodu jednego pola niczego nie zmienia

managedBy to reguła, która decyduje, kto odpowiada: gdy ma wartość instructor, kalendarz i prośby o rezerwację należą do instruktora, a API może je odczytywać, ale nie zapisywać; gdy ma wartość school, kalendarz zapisuje i na prośby odpowiada szkoła – a więc i API – a instruktor ma tylko podgląd. Ustawia ją człowiek, w widoku zespołu w panelu; przez API nie da się jej zmienić. Oddanie kalendarza instruktorowi usuwa bloki zewnętrzne, które zapisał w nim twój system.

Gdy instruktor odchodzi ze szkoły, jego rezerwacje znikają z GET /bookings, a jego zdarzenia przestają przychodzić; to, co już zostało zarobione, zostaje na twoim saldzie. Ani to, ani zmiana managedBy nie wysyła zdarzenia: porównuj GET /instructors przy każdej synchronizacji.

unclaimed oznacza instruktora, którego szkoła dodała w panelu bez konta: nikt nie loguje się jako ten instruktor, więc zawsze ma managedBy school, a szkoła odpowiada za niego we wszystkim, także na czacie. Gdy osoba przejmie profil własnym kontem, unclaimed zmienia się na false, a id instruktora zmienia się na stałe: wszystkie ścieżki i filtr instructorId nadal przyjmują stare id, instruktor ma je w polu previousId, a zdarzenia od tej chwili podają nowe. Żadne zdarzenie tego nie ogłasza: porównuj GET /instructors przy każdej synchronizacji i dopasowuj previousId.

Dostępność

Dostępność instruktora to lista bloków. Blok jest typu available, blocked albo vacation, należy do ośrodka, w którym instruktor uczy, obejmuje czas od startTime do endTime (endTime może wynosić 24:00) i jest zakotwiczony albo w dniu tygodnia (dayOfWeek 0–6, gdzie 0 to niedziela, opcjonalnie z since i until jako pierwszą i ostatnią datą, w której obowiązuje), albo w jednej dacie – dokładnie w jednym z tych dwóch. Nowe godziny available muszą mieścić się między 07:00 a 21:00 czasu ośrodka (w przeciwnym razie 422 outside_ski_day); bloki blocked i vacation mogą obejmować dowolne godziny. Klienci mogą rezerwować tylko w blokach available, których nie zakrywa nic innego.

PUT /instructors/{id}/availability zastępuje cały grafik – każdy ośrodek, każdy blok, do 400 bloków – tym, co wysyłasz, dokładnie tak jak zapisanie kalendarza w panelu. Identyfikatory są opcjonalne: serwer nadaje id blokowi, który go nie ma, zachowuje te, które wysyłasz, i odrzuca duplikaty oraz prefiks xb_ bloków zewnętrznych. Bloków zewnętrznych (zob. niżej) PUT nigdy nie rusza i nie mogą się w nim pojawić. Błędnie zbudowany blok, nieznany ośrodek, oba zakotwiczenia naraz albo since późniejsze niż until bloku to 422 invalid_availability. Bloki, które nie mogą już obowiązywać (data, która minęła, blok tygodniowy po swoim until), są pomijane: PUT ich nie zapisuje, a GET ich nie zwraca. Data mija wtedy, gdy minie wszędzie, czyli o 12:00 UTC następnego dnia.

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 liczy za ciebie: dla jednego ośrodka i do 62 dni zwraca dla każdego dnia wolne przedziały, które klient mógłby zarezerwować, oraz zajęte, a każdy zajęty przedział ma etykietę booking (potwierdzona lekcja), pending (opłacona prośba czekająca na odpowiedź) albo external (blok z API). Uwzględnia bufor między lekcjami i minimalne wyprzedzenie ustawione przez instruktora; nie pomija przerw krótszych niż jego minimalna długość lekcji, więc trzydziestominutowa przerwa jest pokazywana jako wolna.

Widok rozszerzony

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": [] }
  ]
}

Bloki zewnętrzne

Lekcja sprzedana poza Alpine Pal zajmuje instruktora. Zarejestruj ją jako blok zewnętrzny, a ten termin zniknie z wyszukiwania w każdym ośrodku, w którym instruktor uczy – człowiek nie może być w dwóch dolinach naraz. Bloki wymagają, by instruktorem zarządzała szkoła (managedBy = school).

Każdy blok ma twój własny externalRef. Ponowne wysłanie tej samej wartości externalRef z tą samą datą i godzinami jest idempotentne i zwraca 200 z zapisanym blokiem, niezależnie od przekazanego resortId; ta sama wartość z innymi godzinami to 409 external_ref_exists – usuń blok i utwórz go od nowa. Blok, który nakłada się na aktywną rezerwację, to 409 booking_conflict z godzinami, które stoją na przeszkodzie, a gdy ta rezerwacja jest opłacona – z jej id i numerem referencyjnym: ta lekcja jest już u nas sprzedana.

Licząc od dziś, instruktor może mieć do 400 bloków zewnętrznych; kolejny to 422 limit_reached. Blok, którego data minęła (o 12:00 UTC następnego dnia), znika sam: GET /blocks już go nie zwraca, DELETE na nim daje 404 i przestaje się on liczyć do limitu 400.

EndpointUwagi
GET /instructors/{id}/blocksOpcjonalne filtry from, to, externalRef
POST /instructors/{id}/blocksdate, startTime, endTime, externalRef (od 1 do 120 znaków), opcjonalnie resortId (ośrodek, w którym instruktor uczy; domyślnie pierwszy z nich); 07:00–21:00, nie w przeszłości według zegara tego ośrodka, najwyżej 30 dni poza horyzontem rezerwacji wynoszącym 182 dni
DELETE /instructors/{id}/blocks/{blockId}204; blok z grafiku (niepochodzący z API) daje tu 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" }'

Akceptacja prośby o rezerwację ponownie sprawdza termin: blok zewnętrzny w tych godzinach, tak jak aktywna rezerwacja czy lekcja grupowa, daje 409 slot_conflict, a szkoła musi wtedy prośbę odrzucić albo zaproponować inny termin. W puli lekcji bez przypisanego instruktora akceptacja niczego ponownie nie sprawdza: jej miejsca zostały policzone, gdy klient zapłacił. Gdy twój blok dostanie odpowiedź 201, godzina należy do ciebie: płatność, która była już w toku na tę godzinę, zostanie zwrócona, gdy tylko wpłynie, i dotrze do ciebie jako booking.expired.

Lekcje bez przypisanego instruktora

Szkoła może sprzedawać godziny, nie mówiąc, kto je poprowadzi: klient rezerwuje i płaci, a szkoła przydziela instruktora, kiedy chce – przez API, z panelu albo na miejscu spotkania, z tym, kto akurat jest wolny. W API to własna pula szkoły: ma swój kalendarz, swoją cenę i własny przełącznik natychmiastowego potwierdzenia, nie ma jej na liście /instructors, a jej rezerwacje przychodzą z unassigned: true i instructorId ustawionym na pulę (pool_…). Sprzedaje jednocześnie tyle lekcji, ile miejsc ma jej kalendarz: blok available może mieć capacity (do 50; domyślnie 1), liczone w ośrodku bloku minuta po minucie – bloki konkretnego dnia zastępują tygodniowe w minutach, które obejmują, a w obrębie każdego z tych rodzajów nakładające się bloki się sumują. Lekcja potrzebuje miejsca przez cały swój czas. Lekcje grupowe puli nie zajmują miejsc: odbywają się równolegle z lekcjami prywatnymi i ze sobą nawzajem.

GET /unassigned pokazuje ustawienia; PATCH /unassigned ustawia dowolne z pól hourlyPrice, extraStudentPrice (0 lub więcej; wartość ujemna daje 422 invalid_price), extraStudentPer, durationPrices, seasonPrices, includedStudents, autoAcceptBookings i maximumStudents. Pierwszy PATCH tworzy pulę, a do tego czasu jej endpointy kalendarza i bloków zwracają 404. Nie ma przełącznika: active mówi, czy pula jest w sprzedaży – a jest, gdy tylko jej stawka godzinowa przekracza minimum (albo, jeśli sprzedaje formaty, gdy jej formaty i sezony są kompletne); sprzedaje w godzinach, które otwiera jej kalendarz, więc szkoła, która nie chce takich lekcji, po prostu żadnych nie otwiera. Pole active w PATCH jest ignorowane. Poza tym kalendarz puli działa jak kalendarz instruktora, w ośrodkach szkoły.

Stałe formaty. Zamiast na godziny szkoła może sprzedawać lekcje prywatne swojej puli w stałych formatach, ustawianych w panelu: maksymalnie 6 (rano, po południu, cały dzień albo inny, każdy z godziną rozpoczęcia i zakończenia) oraz sezony, czyli zakresy dat z ceną dla każdego formatu, za lekcję dla całej grupy. saleMode w GET /unassigned mówi, który sposób obowiązuje, a lessonFormats zawiera formaty i sezony (w API tylko do odczytu). Dopóki pula sprzedaje formaty, każda jej lekcja prywatna jest jednym z nich: format bez ceny w sezonie danej daty nie jest sprzedawany tego dnia, a w dniu poza wszystkimi sezonami nic nie jest sprzedawane. Format potrzebuje wolnego miejsca przez cały swój czas, jak każda lekcja puli, i nie obowiązuje go limit 6 godz. lekcji sprzedawanych na godziny. Taka pula jest w sprzedaży, gdy tylko jej formaty i sezony są kompletne, niezależnie od stawki godzinowej. Jego rezerwacje mają pole format (id i rodzaj), subtotal to cena tego formatu w danym dniu, a propose-time przenosi taką lekcję tylko na te same godziny w dniu, w którym jej format jest sprzedawany w zapłaconej cenie (w przeciwnym razie 409 format_fixed_time albo format_price_differs). W sandboxie POST /sandbox/bookings dla puli przyjmuje formatId i korzysta z formatów twojej szkoły.

EndpointOdpowiednik
GET, PUT /unassigned/availabilityGET, PUT /instructors/{id}/availability
GET /unassigned/availability/expandedWidok rozszerzony dla jednego z ośrodków szkoły; każdy wolny przedział podaje też, ile miejsc zostało (places)
GET, POST /unassigned/blocks · DELETE /unassigned/blocks/{blockId}Bloki zewnętrzne puli: każdy zajmuje jedno miejsce w swoim ośrodku (przy więcej niż jednym ośrodku resortId jest wymagane: 422 resort_required) i jest odrzucany (409 booking_conflict) tylko tam, gdzie zostawiłby więcej lekcji niż miejsc, a wśród nich jedną z naszych
GET /bookings?instructorId=pool_<schoolId>Tylko lekcje bez przypisanego instruktora
POST /bookings/{id}/assigninstructorId w treści: przenosi lekcję bez przypisanego instruktora na kogoś z twojego zespołu – zob. Odpowiadanie na prośby
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 }]
      }
    ]
  }
}

Dopóki lekcja nie zostanie przypisana, stroną instruktora jest szkoła: odpowiada na prośbę, pisze z klientem na czacie i ustala miejsce spotkania. Po przypisaniu rezerwacja należy do instruktora jak każda inna, a to, co szkoła może jeszcze z nią zrobić, zależy od managedBy.

Lekcje grupowe

Lekcja grupowa to wydarzenie z miejscami, które niezależni klienci kupują osobno. Może być jednorazowa (date) albo tworzyć cotygodniową serię (dayOfWeek, opcjonalnie z since i until); ma limit miejsc, cenę za miejsce za całe zajęcia, poziom, grupę docelową (audience: all, children albo adults – zajęcia dla dzieci mogą podawać minAge i maxAge, a każdy uczestnik rezerwujący miejsce musi się w nich mieścić), stałe miejsce spotkania i minimalną liczbę uczestników potrzebną, by zajęcia się odbyły. Zajęcia instruktora biorą cenę za miejsce z jego własnych ustawień (groupClassesEnabled i groupPricePerStudent, zapisywalne przez PATCH /instructors/{id}); tylko pula podaje ją przy tworzeniu zajęć, a zmiana ceny aktualizuje aktywne zajęcia. Zajęcia publikuje się, przekazuje instruktorowi i odwołuje w panelu – w API v1 są tylko do odczytu – i należą do instruktora z zespołu albo do puli lekcji bez przypisanego instruktora.

GET /instructors/{id}/classes (oraz GET /unassigned/classes) zwracają aktywne zajęcia. GET …/classes/expanded?from&to rozwija je w konkretne terminy z liczbą zajętych miejsc, wliczając płatności w toku w ramach ich 10-minutowej blokady – najwyżej 62 dni na wywołanie. W kalendarzu instruktora godziny zajęć są też widoczne jako zajęte (busy) w widoku rozszerzonym dostępności, a blok zewnętrzny na nich zostaje odrzucony z 409 booking_conflict: tych godzin nie możesz sprzedać gdzie indziej. Pula działa jak zespół: jej zajęcia nie zabierają jej miejsc na lekcje prywatne, odbywają się równolegle z nimi i ze sobą nawzajem, a blok zewnętrzny nigdy z nimi nie koliduje.

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
    }
  ]
}

Każde sprzedane miejsce przychodzi przez /bookings jako zwykła rezerwacja z polem groupClassId, potwierdzona w chwili opłacenia (acceptedVia auto). propose-time i assign odrzucają miejsce – godzina należy do zajęć i do wszystkich pozostałych uczestników, a zajęcia przekazuje się instruktorowi w całości, z panelu. Odwołanie zajęć (albo jednego terminu serii) zwraca pełną kwotę za każde miejsce i wysyła jedno booking.cancelled na miejsce; zajęcia, które 24 godz. przed rozpoczęciem terminu wciąż nie osiągnęły minStudents, są odwoływane w ten sam sposób, z cancelledBy platform.

Rezerwacje

Do API trafiają tylko opłacone rezerwacje: rezerwacja istnieje dla ciebie od chwili, gdy wpłynie za nią płatność (paidAt), a rezerwacja, za którą zwrócono pieniądze, pozostaje na liście ze swoim paymentStatus. Klient wybiera instruktora i termin, płaci, a prośba trafia do instruktora albo do szkoły. Od tej chwili albo dostaje odpowiedź, albo wygasa bez odpowiedzi, a klient otrzymuje zwrot pieniędzy.

Zegar: termin odpowiedzi to 48 godz. od płatności, nigdy później niż początek lekcji; propozycja innego terminu albo jej odrzucenie liczy go od nowa (nigdy poza wcześniejszy z dwóch początków: pierwotnego i proponowanego); cogodzinne zadanie cykliczne oznacza jako wygasłe to, co przekroczyło termin, więc wygaśnięcie może nastąpić nawet z godzinnym opóźnieniem. Potwierdzona lekcja przechodzi w stan upcoming 24 godz. przed rozpoczęciem i w in_progress na czas trwania. Przechodzi w completed, gdy klient ją potwierdzi, albo sama 48 godz. po zakończeniu; zakończenie zwalnia twoją część. Klient może anulować bezpłatnie do freeCancellationHours godz. przed rozpoczęciem: to termin samej rezerwacji, czyli ten, który miały zasady anulowania twojej szkoły w chwili jej złożenia (domyślnie 48), i rezerwacja zachowuje go bez względu na późniejsze zmiany zasad.

bookingStatusZnaczenie
pending_instructor_approvalOpłacona, czeka na odpowiedź przed instructorResponseDeadline
change_proposedZaproponowano inny termin; klient go akceptuje, odrzuca albo pozwala mu wygasnąć. Pierwotny termin pozostaje zarezerwowany; proponowany nie, a jego akceptacja sprawdza go ponownie
confirmedZaakceptowana (acceptedVia mówi, przez kogo: instructor, school, auto, customer)
upcoming, in_progressPotwierdzona, tuż przed lekcją albo w jej trakcie
completedZakończona; twoja część pieniędzy jest zwolniona
rejected_by_instructor, expiredPełny zwrot pieniędzy
cancelled_by_customer, cancelled_by_instructorAnulowana; zwrot zgodnie z zasadami anulowania
no_show_customer, no_show_instructor, disputed, refundedUstawiane w panelu albo przez nas
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"
}

O kliencie dostajesz skrócone imię (imię i inicjał nazwiska), jego preferowany język i jego wiadomość – tyle, ile trzeba, by poprowadzić lekcję, i tyle, ile obiecuje mu nasza polityka prywatności. Adres e-mail, telefon ani żaden identyfikator konta nigdy nie są przesyłane; rozmowa z klientem zostaje na czacie w Alpine Pal, tam gdzie jest instruktor.

Listowanie i synchronizacja

GET /bookings zwraca opłacone rezerwacje twoich instruktorów, od najstarszej zmiany, z filtrami status (lista rozdzielona przecinkami), instructorId, from i to (daty lekcji) oraz updatedSince (dowolny moment w ISO 8601, włącznie). Stronicowanie jest typu keyset po (updatedAt, id): podążaj za nextCursor, aż będzie null. Kursor jest nieprzezroczysty i nigdy nie wygasa, ale należy do filtrów, z którymi powstał – jeśli je zmienisz, zacznij od początku; zmyślony kursor to 400.

Wzorzec synchronizacji co noc albo co godzinę jest zawsze ten sam: zapamiętaj najnowsze otrzymane updatedAt i pobierz wszystko od tej chwili, przekazując je dokładnie w takiej postaci, w jakiej przyszło. Webhooki informują cię w chwili, gdy coś się zmienia; GET /bookings pozwala nadrobić zaległości po przerwie w działaniu. updatedAt zmienia się też przy zmianach, które nie wysyłają zdarzenia – lekcja przechodzi w upcoming albo in_progress, zostaje wysłane przypomnienie – więc wiersz może pojawić się w synchronizacji, choć nic się w nim widocznie nie zmieniło.

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_…"

Odpowiadanie na prośby

Akcje są dostępne dla szkoły w przypadku instruktorów, którymi zarządza, oraz jej własnej puli lekcji bez przypisanego instruktora; w przypadku instruktora, który sam sobą zarządza, odpowiedzią jest 403 not_managed_by_school. Każda akcja jest aktualizacją warunkową: jeśli rezerwacja nie jest już w stanie, którego akcja oczekuje, odpowiedzią jest 409 booking_state z aktualnym stanem w details i nic się nie zmienia. Powtórzenie akcji jest więc bezpieczne, a sposobem na sprawdzenie, co się stało, jest GET /bookings/{id}.

EndpointZasada
POST /bookings/{id}/acceptZe stanu pending; lekcja nie może być już rozpoczęta. Termin jest sprawdzany ponownie: aktywna rezerwacja lub lekcja grupowa nakładająca się na niego (z buforem instruktora, także opłacone prośby) albo blok zewnętrzny w tych godzinach daje 409 slot_conflict. W puli nic nie jest sprawdzane ponownie: jej miejsca policzono przy płatności
POST /bookings/{id}/rejectZe stanu pending albo change_proposed; zwraca klientowi pełną kwotę
POST /bookings/{id}/propose-timedate, startTime, opcjonalnie message; tylko ze stanu pending, jedna propozycja naraz. Klient ją akceptuje, odrzuca albo pozwala jej wygasnąć; pierwotny termin pozostaje zarezerwowany, a proponowany nie
POST /bookings/{id}/cancelPotwierdzona, nadchodząca lub trwająca lekcja, która się jeszcze nie skończyła; zwraca klientowi pełną kwotę
POST /bookings/{id}/assigninstructorId w treści; tylko lekcja z unassigned: true, ze stanu pending, confirmed albo upcoming. Instruktor musi być w twoim zespole, aktywny i zweryfikowany, uczyć tej dyscypliny w tym ośrodku, pobierać opłaty w tej walucie, dopuszczać taką liczbę uczestników i taką długość lekcji oraz być wtedy wolny; jego opublikowane godziny nie są sprawdzane – to twoja decyzja. Termin odpowiedzi się nie przesuwa; od tej chwili odpowiada ten, kto zarządza instruktorem. Klient i instruktor dostają powiadomienie, a potem przychodzi booking.updated z 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?" }'

Dwóch rzeczy celowo tu nie ma: oznaczania niestawienia się i kończenia lekcji. Obie przesuwają pieniądze na podstawie tego, co wydarzyło się na stoku, i obie pozostają czynnością człowieka w panelu.

Webhooki

Zarejestruj adres URL https, a każde zdarzenie dotyczące rezerwacji wyślemy na niego żądaniem POST – ten sam JSON, który zwracają endpointy rezerwacji, opakowany w zdarzenie. Do 10 endpointów na szkołę; każdy subskrybuje wybrane zdarzenia albo wszystkie przez "*". Rejestruj je w panelu albo przez API; panel w obu przypadkach pokazuje dziennik doręczeń. URL musi używać https, mieć nazwę hosta z kropką, nie zawierać danych logowania ani fragmentu i nie może być adresem prywatnym ani naszym.

EndpointDziałanie
GET /webhooksTwoje endpointy, razem z sekretami
POST /webhooksurl, events (1–20 albo ["*"]), opcjonalnie description; 201 z sekretem
PATCH /webhooks/{id}Dowolne z pól url, events, description, active; ponowne włączenie zeruje licznik błędów
DELETE /webhooks/{id}Usuwa endpoint razem z jego dziennikiem doręczeń
POST /webhooks/{id}/rotate-secretNowy sekret; stary przestaje działać od razu
POST /webhooks/{id}/testPing dodany do kolejki jak zwykłe doręczenie i wysyłany od razu; zwraca, jak poszła ta próba, a nieudany jest ponawiany jak każde inne
GET /webhooks/{id}/deliveriesNajnowsze najpierw, bez treści: domyślnie 50, a z ?limit= najwyżej 100
POST /webhooks/{id}/deliveries/{deliveryId}/replayTo samo zdarzenie (to samo id, ta sama treść) jeszcze raz jako osobne nowe doręczenie, wysłane od razu i ponawiane jak każde inne
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

Zdarzenia. booking.requested – przyszła opłacona prośba o rezerwację. booking.confirmed – rezerwacja zaakceptowana przez instruktora lub szkołę, potwierdzona automatycznie albo proponowany termin został zaakceptowany przez klienta: acceptedVia mówi, który to przypadek, a w ostatnim previousStatus ma wartość change_proposed. booking.time_proposed – klientowi zaproponowano inny termin. booking.rejected. booking.cancelled – cancelledBy ma wartość customer, instructor, school albo platform (konto stojące za rezerwacją zostało usunięte albo lekcja grupowa została odwołana, bo nie osiągnęła minimalnej liczby uczestników). booking.expired – nikt nie odpowiedział na czas. booking.completed – lekcja się skończyła, także w przypadku niestawienia się. booking.updated – wszystko inne, o czym warto poinformować, z data.reason: proposal_declined, meeting_point_proposed, meeting_point_confirmed, dispute_opened, dispute_resolved, refund_settled (pieniądze wróciły do klienta), refund_issued (zwrot zlecony przez nas) albo instructor_assigned (previousInstructorId wskazuje pulę, w której była lekcja). ping to zdarzenie testowe, z pustym data; nie jest typem, który się subskrybuje. Przejście w upcoming lub in_progress, zmiany w zespole ani zmiany managedBy nie wywołują żadnego zdarzenia.

Czego się spodziewać. Doręczenie następuje co najmniej raz: odpowiedź, która dotrze po upływie naszego limitu czasu (10 s), liczy się jako błąd i zdarzenie przychodzi ponownie, więc traktuj id zdarzenia jako klucz i ignoruj powtórzenia. Kolejność nie jest gwarantowana – ponowne próby i zadanie cykliczne zmieniają kolejność zdarzeń – więc sortuj według data.booking.updatedAt. Nie każda rezerwacja zaczyna się od booking.requested: rezerwacja instruktora z włączonym natychmiastowym potwierdzeniem (autoAcceptBookings) przychodzi od razu jako booking.confirmed, z previousStatus pending_instructor_approval. Twórz więc rezerwację w swoim systemie przy pierwszym zdarzeniu z jej id, niezależnie od jego typu. Payload jest zamrażany w chwili utworzenia zdarzenia; ponowna próba niesie to, co było prawdą wtedy, a późniejsze zdarzenie – nowszy stan. Zdarzenie może przepaść, jeśli nie uda nam się go zapisać; GET /bookings?updatedSince jest siatką bezpieczeństwa. Każde zdarzenie zawiera sandbox: true albo false. Dziennik przechowuje doręczenia przez 30 dni; ponowne wysłanie wysyła dawne doręczenie jeszcze raz.

Weryfikacja doręczeń

Każde doręczenie jest podpisane sekretem endpointu: nagłówek X-AlpinePal-Signature zawiera znacznik czasu i HMAC-SHA256 obliczony ze znacznika czasu, kropki i surowej treści. Zweryfikuj podpis, zanim czemukolwiek zaufasz, porównuj w stałym czasie i odrzucaj znaczniki czasu oddalone od twojego zegara o więcej niż 5 min. Sekret jest zwracany przy tworzeniu endpointu i przez GET /webhooks, a zmienić go można w panelu albo przez POST /webhooks/{id}/rotate-secret; stary przestaje działać od razu.

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"));
}

Odpowiedz dowolnym kodem 2xx w ciągu 10 s. Wszystko inne – także 3xx, bo nie podążamy za przekierowaniami – to błąd, a my ponawiamy próby w odstępach: 5 minut, 15 minut, 1 godzina, 4 godziny, 12 godzin i 24 godziny (łącznie około 42 godz.), za każdym razem z tym samym id zdarzenia, żeby można było usuwać duplikaty. Endpoint zostaje wyłączony, a zarządzający szkołą dostają powiadomienie, dopiero gdy któreś doręczenie wyczerpie wszystkie próby i w tym czasie do endpointu nie dotrze ani jedno doręczenie; krótka awaria go nie wyłącza, niezależnie od liczby zdarzeń w drodze. Dopóki jest wyłączony, nowe zdarzenia nie trafiają do jego kolejki (nadrobisz je przez GET /bookings?updatedSince), a oczekujące doręczenie, którego próba przypada w tym czasie, zostaje oznaczone jako nieudane bez wysyłania; próby przypadające już po ponownym włączeniu są wysyłane normalnie. Wyślij ponownie te, które się nie udały. POST /webhooks/{id}/test dodaje ping do kolejki jak zwykłe doręczenie, od razu go wysyła i zwraca, jak poszła ta próba: nieudany ping jest ponawiany tak samo, liczy się jak każde doręczenie przy wyłączaniu endpointu, a na wyłączonym endpoincie zostaje oznaczony jako nieudany bez wysyłania. GET /webhooks/{id}/deliveries pokazuje najnowsze doręczenia najpierw, domyślnie 50, a z ?limit= najwyżej 100, z liczbą prób i bez ich treści: każde zdarzenie ma jedno doręczenie na endpoint i po jednym więcej na każde ponowne wysłanie, z tym samym id zdarzenia.

Sandbox

Każda szkoła dostaje testową kopię w chwili, gdy jej właściciel utworzy w panelu klucz testowy (Połączenia → Środowisko testowe): kopię szkoły z dwoma zmyślonymi instruktorami – sandbox_ia_…, który odpowiada na prośby ręcznie, i sandbox_ib_…, z włączonym natychmiastowym potwierdzeniem – oraz pulą lekcji bez przypisanego instruktora; każdy z nich ma kalendarz 09:00–17:00 codziennie w ośrodkach szkoły, od 1 do 6 godz. na lekcję, do 6 uczestników, bez bufora i bez minimalnego wyprzedzenia. Klucze testowe zaczynają się od ap_test_ i działają tylko w kopii; klucze produkcyjne nigdy. Wszystko inne to to samo API: lista instruktorów kopii, zapisywanie ich kalendarzy, rejestrowanie webhooków, odpowiadanie na prośby, przypisywanie lekcji.

To, co klient robi na stronie, POST /sandbox/bookings robi za ciebie: tworzy opłaconą rezerwację u wskazanego przez ciebie instruktora, z tymi samymi kontrolami co prawdziwa – opublikowana dostępność, brak nakładania się – i bez udziału pieniędzy. Od tej chwili działa prawdziwy mechanizm, a akcje poniżej zastępują klienta i zegar, więc każde zdarzenie można zobaczyć w kilka minut. Z sandboxa nie wychodzi żaden e-mail i nic w nim nigdy nie jest publiczne; każde wysyłane przez niego zdarzenie zawiera sandbox: true, podobnie jak GET /me. POST /sandbox/reset opróżnia go, żeby zestaw testów mógł zacząć od czysta.

EndpointUwagi
POST /sandbox/bookingsinstructorId (instruktor kopii albo jej pula), date, startTime, durationHours (w granicach ustawionych przez instruktora); opcjonalnie numberOfStudents, resortId, discipline, language, customerMessage. 201 z rezerwacją; 403 not_sandbox z kluczem produkcyjnym
POST /sandbox/classesPublikuje lekcję grupową u instruktora sandboxa albo w puli, na zasadach produkcyjnych; jej miejsca rezerwujesz przez POST /sandbox/bookings z groupClassId
POST /sandbox/bookings/{id}/accept-proposal · decline-proposalOdpowiedź klienta na zaproponowany termin
POST /sandbox/bookings/{id}/cancelKlient anuluje, na tych samych zasadach co na stronie
POST /sandbox/bookings/{id}/completeTo, co robi potwierdzenie klienta albo zadanie cykliczne 48 godz. po lekcji: completed i zwolnienie twojej części
POST /sandbox/bookings/{id}/expireTo, co robi zadanie cykliczne po upływie terminu odpowiedzi: expired i zwrot pieniędzy
POST /sandbox/resetUsuwa rezerwacje, czaty, wpisy salda i lekcje grupowe kopii; klucze, webhooki, kalendarze i dziennik doręczeń zostają
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 }'

W sandboxie nadal niedostępne są: reklamacje, niestawienie się, zwrot zlecony przez nas, anulowanie z cancelledBy platform i miejsce spotkania (jego zdarzenia pochodzą ze strony). Rezerwacje w sandboxie mają puste studentLevels i klienta o imieniu Sandbox C., który korzysta ze strony po angielsku.

OpenAPI

Opis do odczytu maszynowego znajdziesz pod adresem https://alpinepal.com/api/v1/openapi.json – każda ścieżka, metoda, parametr i schemat opisane powyżej, w formacie OpenAPI 3.1. Skieruj na niego generator, by uzyskać typowanego klienta, albo zaimportuj go do swojego narzędzia HTTP, żeby go przeglądać. Jest udostępniany bez uwierzytelniania i trzymany w cache przez godzinę.

Wersje i zmiany

To jest v1. W jej ramach tylko dodajemy: nowe pola, nowe opcjonalne parametry, nowe typy zdarzeń, nowe endpointy. Twoja integracja powinna ignorować pola i zdarzenia, których nie zna. Wszystko, co zepsułoby działanie istniejącego klienta, trafi do v2 pod nowym bazowym URL-em, a v1 będzie działać dalej, dopóki się nie przeniesiesz.

  • 2026-09 – v1: szkoła, ośrodki, instruktorzy, dostępność i widok rozszerzony, bloki zewnętrzne, rezerwacje ze stronicowaniem keyset, accept / reject / propose-time / cancel, podpisane webhooki z ponownymi próbami.

  • 2026-09-18 – hourlyPrice i extraStudentPrice w PATCH /instructors/{id}; pula lekcji bez przypisanego instruktora pod /unassigned i POST /bookings/{id}/assign; reason i previousInstructorId w booking.updated; sandbox, klucze ap_test_ i pole sandbox w każdym zdarzeniu.

  • 2026-09-18 – przegląd: booking_state podaje aktualny stan; updatedSince przyjmuje dowolny moment w ISO; zwrot za rezerwację bez płatności także emituje refund_settled; jedno booking.completed na lekcję; assign sprawdza ograniczenia instruktora; meeting_point_proposed; POST /webhooks/{id}/deliveries/{deliveryId}/replay; akcje klienta i zegara w sandboxie; ta strona napisana od nowa na podstawie tego, co robi kod.

  • 2026-09-20 – PATCH /instructors/{id} zapisuje swoje pola razem albo wcale; blok, który dostał odpowiedź 201, wygrywa teraz z płatnością, która była w toku na tę samą godzinę (klient dostaje zwrot pieniędzy, a ty – booking.expired).

  • 2026-09-22 – lekcje grupowe: GroupClass i jego terminy pod /instructors/{id}/classes i /unassigned/classes (tylko do odczytu), groupClassId w każdej rezerwacji, cancelledBy platform, gdy zajęcia nie osiągną minimum, POST /sandbox/classes i groupClassId w POST /sandbox/bookings.

  • 2026-09-23 – ceny lekcji grupowych u instruktora: groupClassesEnabled i groupPricePerStudent w Instructor i w PATCH /instructors/{id}; nowa cena za miejsce aktualizuje aktywne zajęcia; minimum za miejsce wynosi 10 EUR / 10 USD.

  • 2026-09-24 – udokumentowano: rezerwacja instruktora z natychmiastowym potwierdzeniem przychodzi tylko jako booking.confirmed, nigdy jako booking.requested; twórz rezerwacje przy pierwszym zdarzeniu z ich id. Bez zmian w działaniu.

  • 2026-09-27 – udokumentowano: zaproponowanie miejsca spotkania jednak wysyła booking.updated, z reason meeting_point_proposed; sekcja o synchronizacji mówiła, że nie wysyła żadnego zdarzenia. Bez zmian w działaniu.

  • 2026-09-27 – bloki, których data minęła, są pomijane: PUT /instructors/{id}/availability już ich nie zapisuje ani nie zwraca, a blok zewnętrzny znika z GET /blocks, gdy tylko jego data minie, i przestaje się liczyć do limitu bloków zewnętrznych, który teraz liczy się od dziś. Do tej pory oba rodzaje gromadziły się, aż instruktor osiągnął limit.

  • 2026-10-05 – instruktorzy dodani przez szkołę bez konta: unclaimed i previousId w Instructor. Gdy osoba przejmie profil, jego id się zmienia; wszystkie ścieżki i filtr instructorId nadal przyjmują stare.

  • 2026-10-06 – miejsca w puli lekcji bez przypisanego instruktora: capacity w jej blokach available (kilka lekcji naraz, liczone dla każdego ośrodka), places w każdym wolnym przedziale jej widoku rozszerzonego, a jej bloki zewnętrzne zajmują po jednym miejscu, zamiast zamykać godziny. Jej lekcje grupowe kolidują teraz tylko we własnym ośrodku. Kalendarze instruktorów się nie zmieniają: capacity w którymś z nich to 422 invalid_availability.

  • 2026-10-06 – pula prowadzi swoje lekcje grupowe równolegle z miejscami na lekcje prywatne i ze sobą nawzajem: zajęcia nie zajmują już godzin (zostają one w free, ze swoimi miejscami), a blok zewnętrzny już z nimi nie koliduje. GroupClass dostaje audience (all, children, adults) z opcjonalnymi minAge i maxAge; miejsce na zajęciach dla dzieci jest sprzedawane tylko uczestnikom zarezerwowanym jako dzieci, a na zajęciach dla dorosłych – tylko dorosłym. POST /sandbox/classes przyjmuje te same pola.

  • 2026-10-06 – bloki tygodniowe i serie lekcji grupowych mogą zaczynać się później: since, ich pierwsza data (włącznie), pozwala szkole wprowadzić sezon przed jego otwarciem, bez sprzedawania jego godzin już teraz. PUT /instructors/{id}/availability przyjmuje je w blokach z dayOfWeek (since późniejsze niż until bloku to 422 invalid_availability), GET je zwraca, GroupClass je zawiera, a POST /sandbox/classes je przyjmuje. Bez since seria obowiązuje od teraz, jak dotąd.

  • 2026-10-08 – endpoint jest wyłączany dopiero wtedy, gdy któreś doręczenie wyczerpie wszystkie próby i w tym czasie nie dotrze do niego ani jedno doręczenie. Wcześniej wyłączał się po dziesięciu nieudanych próbach z rzędu, liczonych łącznie dla wszystkich doręczeń, więc krótka awaria z wieloma zdarzeniami w drodze mogła go wyłączyć i przekreślić ich ponowne próby.

  • 2026-10-16 – dzień zajęć trwa od 07:00 do 21:00 czasu ośrodka (wcześniej od 06:00 do 23:00): bloki zewnętrzne, proponowane godziny oraz rezerwacje i zajęcia grupowe w sandboxie poza nim zwracają 422 outside_ski_day, podobnie jak nowe godziny available poza nim w PUT /instructors/{id}/availability i PUT /unassigned/availability. Zapisane wcześniej godziny zostają, bloki blocked i vacation mogą obejmować dowolne godziny, a opublikowane wcześniej zajęcia grupowe zachowują swój termin.

  • 2026-10-16 – stałe formaty w puli lekcji bez przypisanego instruktora (API 1.8.0): GET /unassigned dostaje saleMode i lessonFormats (formaty i sezony z ceną dla każdego formatu, tylko do odczytu), Booking dostaje format (null przy lekcjach sprzedawanych na godziny), POST /sandbox/bookings przyjmuje formatId, a propose-time dla lekcji w formacie zachowuje jej godziny i cenę (409 format_fixed_time, format_price_differs). Dla puli sprzedającej na godziny nic się nie zmienia.

  • 2026-10-16 – brak przełącznika dla lekcji bez przypisanego instruktora (API 1.8.1): active w GET /unassigned mówi teraz, czy pula jest w sprzedaży – a jest, gdy tylko jej stawka godzinowa przekracza minimum (albo jej formaty i sezony są kompletne) – a PATCH je ignoruje; szkoła, która nie chce takich lekcji, nie otwiera dla nich godzin.

  • 2026-10-20 – ceny za kilka godzin z rzędu (API 1.10.0): Instructor i UnassignedLessons zyskują `durationPrices`, opcjonalne stałe ceny za całą lekcję trwającą 2, 3 lub 4 godziny dla jednego ucznia. Cena za 4 godziny obowiązuje też przy dłuższych lekcjach według swojej stawki godzinowej, długość bez własnej ceny bierze stawkę najdłuższej krótszej długości, która ją ma, a dodatkowi uczniowie są nadal liczeni za godzinę. Bez nich wszystko zostaje na godziny. PATCH /instructors/{id} i PATCH /unassigned je przyjmują (null je usuwa); ceny droższe za godzinę niż krótsza lekcja albo tańsze w sumie niż krótsza dają 422 `invalid_duration_prices`.

  • 2026-10-19 – pakiety kilkudniowe (API 1.9.0): klient może jedną płatnością kupić kilka dni kursu lekcji grupowej (GroupClass.packs, z ceną za osobę według przedziału wiekowego, oraz courseId) albo kilka dni z rzędu stałego formatu (UnassignedLessons: packDays oraz, w każdym sezonie, minDays i packPrices). Każdy dzień to osobna rezerwacja ze swoją częścią ceny i polem `pack`, które wskazuje pakiet i dzień; studentLevels może zawierać `band` każdego uczestnika.

  • 2026-11-02 – warunki według ośrodka (API 1.22.0): UnassignedLessons zyskuje pole `resortTerms`: według id ośrodka każdy ośrodek, który sprzedaje inaczej, ze wszystkimi cenami i warunkami, jakie tam obowiązują (te same pola co na najwyższym poziomie: `hourlyPrice`, `durationPrices`, `seasonPrices`, `saleMode`, `lessonFormats` i pozostałe). Gdy warunki są jedne, to `{}`, a ośrodek, którego nie ma na liście, sprzedaje według wartości z najwyższego poziomu. PATCH /unassigned przyjmuje `resortId`, by zapisać warunki tego ośrodka i nie zmieniać pozostałych; ośrodek, w którym szkoła nie działa, daje 422 `unknown_resort`. Bez `resortId` zapisywany jest najwyższy poziom, którego ośrodek z `resortTerms` nie czyta. `active` ma wartość true, dopóki sprzedaż trwa w co najmniej jednym ośrodku. To wyłącznie dodatek: bez warunków według ośrodka wszystko czyta się i sprzedaje jak dotąd.

  • 2026-10-29 – godziny, w których instruktor jest zajęty we własnym Kalendarzu Google (API 1.21.1): instruktor może sprawić, że godziny oznaczone w jego Kalendarzu Google jako zajęte zostaną zamknięte w Alpine Pal. Nie są częścią harmonogramu: GET /instructors/{id}/availability ich nie zwraca, a PUT ich nie widzi ani nie zmienia, natomiast /availability/expanded pomija te godziny w `free`, jak wszystko, co nie jest w sprzedaży. Nic więcej się nie zmienia.

  • 2026-10-27 – ceny sezonowe i uczniowie wliczeni w cenę (API 1.21.0): Instructor i UnassignedLessons zyskują pola `seasonPrices` i `includedStudents`, oba ustawiane przez PATCH /instructors/{id} i PATCH /unassigned. `seasonPrices` to lista do 4 sezonów, każdy z `id`, nazwą `name` do panelu (null, gdy jej brak), od 1 do 10 zakresami dat (`from` i `to`, oba włącznie) oraz własnymi `hourlyPrice` i `durationPrices`: lekcja w dniu należącym do sezonu jest liczona według cen tego sezonu, a dzień poza wszystkimi sezonami według cen własnych instruktora lub puli, jak dotąd. Zakresy nigdy się nie nakładają, ani w obrębie sezonu, ani między sezonami, a ceny każdego sezonu podlegają tym samym regułom co własne ceny instruktora lub puli. PATCH wysyła całą listę (null albo pusta lista ją usuwa): zachowaj id zwrócone przez GET, a dla nowego sezonu pomiń id. GET zwraca null, gdy nie ma żadnego. `includedStudents` (od 1 do 20, domyślnie 1) mówi, ilu uczniów obejmuje cena: `extraStudentPrice` jest liczona tylko za każdego ucznia ponad tę liczbę. Sezony łamiące regułę dają 422 `invalid_season_prices`, a liczba spoza zakresu 422 `invalid_included_students`. To wyłącznie dodatek: bez sezonów i z jednym uczniem wliczonym w cenę wszystkie ceny pozostają jak dotąd.

  • 2026-10-26 – cena 0 lub niższa jest odrzucana (API 1.20.4): w `durationPrices` długość, którą sprzedaje instruktor lub miejsce, z ceną 0 albo ujemną daje teraz 422 `invalid_duration_prices`. Dotąd była po cichu pomijana, a żądanie odpowiadało 200, więc ta długość szła według stawki godzinowej. Żeby nie dawać jej własnej ceny, nie wysyłaj jej klucza (albo wyślij null dla całego pola).

  • 2026-10-26 – poprawki dokumentacji (API 1.20.3): ta strona i specyfikacja mówią teraz to, co API już robiło. Akceptacja prośby ponownie sprawdza termin (aktywna rezerwacja, lekcja grupowa albo blok zewnętrzny na nim daje 409 `slot_conflict`; w puli lekcji bez przypisanego instruktora nic nie jest sprawdzane ponownie). Klucze przestają działać, z 403 `school_not_active`, tylko gdy szkoła jest zamknięta albo niezatwierdzona; zawieszona szkoła zachowuje swoje API. Przy wyłączonym endpoincie oczekujące doręczenie jest oznaczane jako nieudane dopiero wtedy, gdy przypada jego próba; próby przypadające po ponownym włączeniu są wysyłane. Nieznana wartość `extraStudentPer` daje 400 `validation_error`, a nie 422 `invalid_price`, jak podawał wpis o 1.13.0; `invalid_price` oznacza ujemną wartość `extraStudentPrice`. `platformFee` wynosi 10% albo wyższą prowizję uzgodnioną na piśmie przez szkołę, zależnie od tego, jak sprzedano rezerwację. Przykładowa rezerwacja zyskuje `unassigned`, `groupClassId`, `format`, `pack`, `equipment` i `freeCancellationHours`, a jej miejsce spotkania ma status `not_agreed`. Tabela błędów wymienia wszystkie kody, w tym `managed_by_school`, kody pakietów, formatów i lekcji grupowych oraz `inside_notice_window` (z `hours`). Doręczenia są listowane domyślnie po 50, a z `limit` najwyżej po 100, a ponowne wysłanie to osobne doręczenie z tym samym id zdarzenia. Ping to zwykłe doręczenie: ponawiany, gdy się nie uda, liczony przy wyłączaniu endpointu i, przy wyłączonym endpoincie, oznaczany jako nieudany bez wysyłania. Bez zmian w działaniu.

  • 2026-10-26 – dwie poprawki (API 1.20.2): PATCH /instructors/{id} ocenia teraz `durationPrices` według długości lekcji samego instruktora, tak jak panel, więc nie odrzuca już cen kogoś, kto zaczyna od 2 godzin, jako droższych niż 1-godzinna lekcja, której nie sprzedaje. A z kluczem testowym GET i PATCH /unassigned opisują formaty sprzedawane w piaskownicy (te z prawdziwej szkoły, których POST /sandbox/bookings już używał): `saleMode` i `lessonFormats` nie pokazują już `hours` i pustej listy dla miejsca, które sprzedaje formaty.

  • 2026-10-26 – przydzielanie dnia pakietu (API 1.20.1): POST /bookings/{id}/assign odpowiada 409 `pack_pending` dla dnia pakietu kilkudniowego, który wciąż czeka na odpowiedź szkoły, gdy instruktor sam zarządza swoimi rezerwacjami (`managedBy: instructor`). Na pakiet odpowiada się w całości, a ten instruktor nie mógłby odpowiedzieć za część szkoły. Najpierw zaakceptuj pakiet albo przydziel dzień instruktorowi zarządzanemu przez szkołę; dni już potwierdzone przydziela się pojedynczo, jak dotąd.

  • 2026-10-25 – zamknięcie formatu u instruktora (API 1.20.0): blok blocked z `formatId` jest teraz przyjmowany w kalendarzu instruktora, który sprzedaje w formatach, tak jak w miejscu bez przypisanego instruktora od wersji 1.15.0, i zamyka tylko ten format w jego dacie lub dniach tygodnia; pozostałe formaty w tych godzinach są nadal w sprzedaży. Instruktor, który korzysta z godzin szkoły (`followsSchoolHours`), dostaje teraz także zamknięcia pojedynczych formatów z miejsca. Rezerwacje już wcześniej respektowały te zamknięcia w każdym kalendarzu.

  • 2026-10-25 – otwarte godziny szkoły dla jej instruktorów (API 1.19.0): Instructor zyskuje pole `followsSchoolHours`. Instruktor, który dołącza przez zaproszenie szkoły albo którego szkoła dodaje bez konta, zaczyna teraz z `managedBy: school` i tą opcją włączoną: jego kalendarz jest kopią otwartych godzin miejsca bez przypisanego instruktora, w ośrodkach, w których uczy, z jedną lekcją naraz, a każda zmiana w grafiku miejsca trafia też do niego. Szkoła przełącza to w panelu (Lekcje prywatne). PUT /instructors/{id}/availability daje mu od tej chwili własne godziny: `followsSchoolHours` zmienia się na false. Instruktorzy, którzy już są w zespole, się nie zmieniają.

  • 2026-10-23 – instruktorzy sprzedawani w stałych formatach (API 1.18.0): Instructor zyskuje pole `saleMode`, `hours` lub `formats`. Szkoła może teraz sprzedawać każdego ze swoich instruktorów w stałych formatach z ceną na sezon, tak jak swoje miejsce bez przypisanego instruktora (ustawia się to w panelu, w sekcji Lekcje prywatne); jego prywatne rezerwacje mają wtedy pole `format`, jak rezerwacje miejsca, a stawki godzinowe niczego nie sprzedają. Booking.format nie dotyczy już tylko miejsca. Błąd 422 `format_required` brzmi teraz „Choose one of the lesson formats”. To wyłącznie dodatek: każdy instruktor sprzedaje na godziny, dopóki szkoła tego nie zmieni.

  • 2026-10-23 – franki szwajcarskie (API 1.17.0): School.currency może mieć wartość `CHF`. Szwajcarskie szkoły i ich instruktorzy ustalają ceny i otrzymują wypłaty we frankach, więc wszystkie ich kwoty (ceny, subtotal, wypłaty) są w CHF, z własną minimalną stawką godzinową.

  • 2026-10-23 – zasady anulowania dla każdej szkoły (API 1.16.0): Booking zyskuje pole `freeCancellationHours`, także w data.booking każdego zdarzenia rezerwacji: na ile godzin przed rozpoczęciem klient może najpóźniej anulować z pełnym zwrotem pieniędzy, czyli termin, na jakim rezerwację sprzedano – 48, 72, 168, 336, 672 lub 1008. GET /me zwraca aktualne `freeCancellationHours` szkoły. Szkoła ustala swoje zasady w panelu (Profil → Zasady anulowania); dotyczą one wszystkiego, co szkoła sprzedaje (lekcji prywatnych i grupowych jej instruktorów oraz jej miejsca bez przypisanego instruktora), a rezerwacja zachowuje termin, na jakim ją złożono, bez względu na późniejsze zmiany zasad. Przez API tylko do odczytu. To wyłącznie dodatek: wcześniejsze rezerwacje mają 48.

  • 2026-10-21 – zamykanie jednego formatu (API 1.15.0): blok `blocked` miejsca bez przypisanego instruktora może mieć `formatId` i wtedy zamyka tylko ten format w swojej dacie lub dniach tygodnia. Miejsca w tych godzinach zostają dla pozostałych formatów; zamknięcie godzin zamykało każdy format, który dzielił z nimi choć minutę. Zamknięcie całego dnia nadal oznacza zamknięcie jego godzin. GET zwraca to pole; PUT bez niego usuwa zamknięcie.

  • 2026-10-21 – formaty dla Westendorf (API 1.14.0): początek i koniec formatu mogą teraz wypadać co kwadrans (08:45, 13:15), nie tylko o pełnej godzinie lub o wpół do. `lessonFormats` w UnassignedLessons zyskuje `extraStudentPrice`: ile dolicza każdy uczeń od drugiego do lekcji według formatów, raz za lekcję (za każdy dzień pakietu), niezależnie od godzinowej dopłaty miejsca; 0 zostawia cenę za całą grupę. Ustawia się w panelu.

  • 2026-10-20 – dopłata za ucznia za lekcję (API 1.13.0): Instructor i UnassignedLessons zyskują `extraStudentPer`, `hour` (jak dotąd) lub `lesson`: czy `extraStudentPrice` jest liczona za każdą godzinę lekcji, czy raz za całą lekcję. Ustawiane przez PATCH /instructors/{id} i PATCH /unassigned; inna wartość daje 422 `invalid_price`.

  • 2026-10-20 – warunki Courchevel i Westendorf (API 1.12.0): każdy format w sezonie ma własną minimalną liczbę dni z rzędu (`prices[].minDays`; `minDays` sezonu jest teraz najmniejszą z nich i jest przestarzałe), więc poranki można sprzedawać od pięciu dni, a popołudnia pojedynczo w tych samych tygodniach. UnassignedLessons zyskuje `minBookingNoticeHours` (teraz także dwa i trzy dni) i `disciplines`, oba ustawiane przez PATCH /unassigned. Pakiet stałych formatów na miejscu bez natychmiastowego potwierdzenia czeka teraz na szkołę, jak każda inna prośba; przyjęcie lub odrzucenie dowolnego z jego dni dotyczy całego pakietu. Miejsce zyskuje też `minimumLessonDuration` i `maximumLessonDuration` (pełne godziny, PATCH /unassigned; 422 `invalid_lesson_length`): Snowsports Westendorf sprzedaje dwie i trzy godziny, nigdy jedną. Jeśli żadna sprzedawana lekcja nie jest liczona za godzinę, `hourlyPrice` może wynosić 0, a miejsce nadal jest w sprzedaży. `durationPrices` przyjmuje klucz dla każdej długości od dwóch do sześciu godzin; długość bez ceny jest liczona według stawki godzinowej ostatniej krótszej długości z ceną. Oraz `singleLevelBookings` (także PATCH /unassigned): po włączeniu lekcja prywatna, której uczniowie mieszają poziomy albo dorosłych z dziećmi, jest odrzucana, a klient rezerwuje osobno dla każdej grupy; Courchevel daje jednego instruktora na poziom i wiek.

  • 2026-10-20 – wypożyczenie sprzętu (API 1.11.0): szkoła może wypożyczać sprzęt razem ze swoimi lekcjami, po cenie za dzień za każdą pozycję, którą ustala w swoim panelu. Booking zyskuje pole `equipment` (null, gdy nic nie wypożyczono): część tej rezerwacji (amount, już wliczona w subtotal), całe wypożyczenie, jego dni oraz, dla każdego uczestnika, pozycje z ceną i rozmiarami (rozmiar buta EU, wzrost w cm). Prowizja i netAmount liczą je tak jak lekcję, a zwrot pieniędzy oddaje je razem z lekcją.