Ein Skischulkonto auf Alpine Pal gibt dir den Kader deiner Skilehrer, ihre Kalender und ihre Buchungsanfragen in einem Dashboard. Die API ist dasselbe Dashboard für eine Maschine: Ein Channel-Manager, ein CRM oder ein eigenes Skript kann die Verfügbarkeit deiner Skilehrer lesen und schreiben, von jeder bezahlten Buchung in dem Moment erfahren, in dem sie entsteht, und sie annehmen, ablehnen oder einen anderen Termin dafür vorschlagen.
Sie ist bewusst schmal gehalten. Kunden bezahlen auf Alpine Pal, deshalb erstellt die API keine Buchungen; sie trägt anderswo verkauften Unterricht als Blöcke ein, die den Kalender ehrlich halten. Was auf der Piste Geld bewegt – ein Nichterscheinen vermerken, eine Reklamation abschließen –, bleibt eine menschliche Handlung im Dashboard.
Einen Schlüssel erhalten
Schlüssel gehören der Skischule, nicht einer Person, und nur der Inhaber der Skischule kann sie erstellen oder widerrufen, im Reiter Verbindungen des Skischul-Dashboards. Ein Schlüssel wird einmal angezeigt, bei der Erstellung; wir speichern einen Hash und können ihn nicht noch einmal anzeigen. Eine Skischule kann bis zu 5 Live-Schlüssel halten, sodass eine neue Integration ihren eigenen bekommt und ein alter widerrufen werden kann, ohne die übrigen anzurühren. Live-Schlüssel beginnen mit ap_live_; die Schlüssel der Testumgebung beginnen mit ap_test_ und werden getrennt gezählt.
Ein Schlüssel funktioniert ab dem Moment nicht mehr, in dem er widerrufen wird, und alle Schlüssel erhalten ein 403 school_not_active, sobald die Skischule geschlossen oder nicht freigegeben ist. Eine nur gesperrte Skischule (auf der Website ausgeblendet, ohne ihre Seite) behält ihre Schlüssel: Ihre Buchungen laufen weiter, und die API mit ihnen. Schlüssel lassen sich nicht über die API selbst verwalten. Zum Rotieren: Erstelle den neuen Schlüssel, stelle dein System darauf um und widerrufe dann den alten – dazwischen sind beide gültig. Das Dashboard zeigt, wann jeder Schlüssel zuletzt benutzt wurde, auf 5 Minuten genau. Schlüssel sind für Server: Liefere nie einen in einem Browser oder einer Mobile-App aus.
Authentifizierung, Header und Request-Body
Jede Anfrage trägt den Schlüssel als Bearer-Token. Die Basis-URL ist https://alpinepal.com/api/v1; Antworten sind JSON, werden nie gecacht, und jede trägt eine X-Request-Id, die du uns nennen kannst. Uhrzeiten gelten nach der Ortszeit des Skigebiets als HH:mm, Daten sind YYYY-MM-DD, und Zeitpunkte sind ISO 8601 in UTC mit Millisekunden.
Request-Bodys müssen JSON sein, mit dem Header Content-Type: application/json (sonst 415), unter 256 KB (413), und werden strikt geprüft: Ein unbekanntes Feld ist ein 400 validation_error, der die betroffenen Pfade auflistet. Ein leerer Body wird als leeres Objekt gelesen. Eine Methode, die der Pfad nicht unterstützt, ist ein 405 mit einem Allow-Header; ein Schrägstrich am Ende ist ein 404.
| Header | Wo |
|---|---|
Authorization: Bearer <Schlüssel> | Jede Anfrage |
Content-Type: application/json | Jede Anfrage mit Body |
X-Request-Id | Jede Antwort, Fehler eingeschlossen; nenne sie, wenn du uns schreibst |
Cache-Control: no-store, Vary: Authorization | Jede Antwort |
WWW-Authenticate: Bearer realm="alpinepal-api" | 401-Antworten |
Allow | 405-Antworten |
Retry-After (Sekunden) | 429-Antworten; der Body wiederholt es als details.retryAfterSeconds |
curl https://alpinepal.com/api/v1/me \
-H "Authorization: Bearer ap_live_…"Es gibt kein CORS: Die API ist Server-zu-Server, und eine Anfrage aus einem Browser schlägt fehl. Das ist Absicht, denn ein Schlüssel im Browser ist ein Schlüssel, den jeder lesen kann.
Anfragelimits
Pro Schlüssel, in festen Fenstern von einer Minute: 600 Lesezugriffe (GET) und 120 Schreibzugriffe (alles andere, DELETE eingeschlossen) pro Minute. Darüber hinaus lautet die Antwort 429 rate_limited mit einem Retry-After-Header, in Sekunden. Ein Test-Schlüssel hat sein eigenes Kontingent. Fehlgeschlagene Authentifizierungen sind pro IP-Adresse begrenzt, 20 in 10 Minuten; danach bekommt jeder weitere Versuch 429, noch bevor der Schlüssel überhaupt nachgeschlagen wird. Webhook-Tests und erneute Zustellungen teilen sich ein eigenes Kontingent von 10 in 10 Minuten pro Schlüssel, denn jede einzelne ist eine Anfrage an einen fremden Server.
Fehler
Jeder Fehler hat dieselbe Form: einen HTTP-Status, einen stabilen code, nach dem du verzweigen kannst, eine Nachricht für Menschen und die Request-Id. Fünf Codes tragen details: validation_error listet die betroffenen Felder als [{path, message}] (höchstens 10); booking_state sagt, in welchem Zustand die Buchung jetzt ist ({bookingStatus}); booking_conflict nennt die Stunden, die im Weg sind, und, wenn diese Buchung bezahlt ist, ihre Id und Referenz; rate_limited wiederholt die Wartezeit als {retryAfterSeconds}; inside_notice_window nennt die Vorlaufzeit des Skilehrers in Stunden ({hours}).
| Status | Codes | Wann |
|---|---|---|
400 | validation_error, invalid_cursor, invalid_json | Die Anfrage selbst ist fehlerhaft |
401 | unauthenticated, invalid_api_key | Kein Schlüssel, oder ein fehlerhafter, unbekannter, widerrufener oder zur falschen Umgebung gehörender |
403 | forbidden, school_not_active, managed_by_school, not_managed_by_school, not_sandbox | Der Schlüssel ist in Ordnung, aber diese Aktion steht dir nicht zu; not_sandbox ist ein Live-Schlüssel auf einem Endpoint der Testumgebung |
404 | not_found | Nicht vorhanden, oder keiner deiner Skilehrer. Wir bestätigen nie, dass etwas außerhalb deiner Skischule existiert |
405 | method_not_allowed | Falsche Methode für den Pfad; Allow listet die richtigen auf |
409 | booking_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_disabled | Ein echter Zustandskonflikt; lies den aktuellen Zustand und entscheide neu |
413, 415 | payload_too_large, unsupported_media_type | Bodys müssen JSON und unter 256 KB sein |
422 | outside_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_window | Wohlgeformt, aber verstößt gegen eine Regel des Kalenders, des Unterrichts oder der Skischule |
429 | rate_limited | Siehe Retry-After |
500 | internal | Unser Fehler; die Request-Id hilft uns, ihn zu finden |
{
"error": {
"code": "booking_state",
"message": "Invalid booking state",
"requestId": "req_929e0418a4b7",
"details": { "bookingStatus": "confirmed" }
}
}Die Daten lesen
Ids. Skilehrer tragen die Id ihres Kontos (user_…) oder unclaimed_…, solange ein Profil, das deine Skischule ohne Konto angelegt hat, noch nicht übernommen wurde; der Platz einer Skischule für Unterricht ohne zugewiesenen Skilehrer ist pool_<schoolId>; die Skilehrer der Testumgebung sind sandbox_ia_… und sandbox_ib_…. Buchungen sind b_…, Ereignisse evt_…, Zustellungen whd_…, Endpoints wh_…, Anfragen req_…. Referenzen sind AP-<Jahr>-<5 Ziffern> für echte Buchungen und SB<6 hex> in der Testumgebung.
Zeiten. date, startTime und endTime sind die Ortszeit von resortTimeZone, nie UTC; die Zeitpunkte (createdAt, updatedAt, paidAt, instructorResponseDeadline…) sind UTC. Ein Unterricht in der Nacht der Zeitumstellung wird auf das erste Vorkommen einer doppelten Stunde gelegt, und eine Stunde, die es in dieser Nacht nicht gibt, wird auf die nächstfolgende existierende Stunde verschoben. endTime darf 24:00 sein. Ob eine Zeit in der Vergangenheit liegt, wird nach der Uhr des Skigebiets beurteilt.
Geld. subtotal = (hourlyPrice + extraStudentPrice × max(0, numberOfStudents − includedStudents)) × durationHours, auf die Währung gerundet; das ist, was der Kunde bezahlt. hourlyPrice ist der des Unterrichtsdatums: der einer Saison (seasonPrices), wenn das Datum in eine fällt, sonst der eigene des Skilehrers oder des Platzes; includedStudents gibt an, wie viele Schüler er abdeckt, 1, sofern die Skischule nicht mehr festlegt. platformFee ist unsere Provision: 10 % oder die höhere, die deine Skischule schriftlich mit uns vereinbart hat; jede Buchung behält den Satz, zu dem sie verkauft wurde. netAmount ist der Rest, subtotal − platformFee, der dem Guthaben deiner Skischule gutgeschrieben wird, wenn der Unterricht abgeschlossen ist – wer auch immer ihn gegeben hat und wer auch immer diese Person verwaltet –, und der entfällt, wenn die Buchung erstattet wird. Jeder Skilehrer in deinem Kader rechnet in der Währung deiner Skischule ab; Stundenpreise haben eine Untergrenze pro Währung (10 EUR, 11 USD, 10 CHF), und extraStudentPrice ist 0 oder mehr. Ausrüstung, die der Kunde mit dem Unterricht bei deiner Skischule mietet, kommt ebenfalls zu subtotal hinzu (equipment.amount) und zählt wie der Unterricht.
| Feld | Werte |
|---|---|
bookingStatus | Siehe die Tabelle unter Buchungen |
paymentStatus | pending, processing, succeeded, failed, authentication_required, refund_pending, refunded – nur succeeded und die beiden Erstattungszustände erreichen die API überhaupt |
acceptedVia | instructor, school, auto, customer, oder null, solange nicht angenommen |
studentLevels | Ein Eintrag pro Teilnehmer: { level: beginner | intermediate | advanced, age: adult | child }; leer in der Testumgebung |
discipline | ski oder snowboard – die des Unterrichts; die disciplines eines Skilehrers können auch both sagen |
language / customer.language | Die Sprache des Unterrichts als Zwei-Buchstaben-Code, und die Sprache, in der der Kunde die Website nutzt (en, es, it, fr, de, pl, nl, ru) |
meetingPoint | status not_agreed, proposed oder confirmed, plus title, sobald einer existiert |
verificationStatus (Skilehrer) | draft, pending_review, verified, rejected |
Deine Skischule, Skigebiete und Skilehrer
| Endpoint | Liefert |
|---|---|
GET / | Der Deskriptor: Name, Version, Links zu dieser Seite und zur Spezifikation (kein Schlüssel nötig) |
GET /me | Deine Skischule: id, slug, name, currency, country, freeCancellationHours (ihre Stornobedingungen, in Stunden) und sandbox (true hinter einem Test-Schlüssel) |
GET /resorts | Jedes Skigebiet, in dem einer deiner Skilehrer oder dein Platz für Unterricht ohne zugewiesenen Skilehrer unterrichtet, mit Zeitzone und Währung |
GET /instructors | Dein Kader, aktive wie inaktive; Skilehrer, die die Skischule verlassen haben, sind daraus verschwunden, und der Platz ist nie darin |
GET /instructors/{id} | Ein Skilehrer: Preise, Unterrichtsdauern, Puffer, Vorlaufzeit, Disziplinen, Sprachen, verificationStatus, managedBy, autoAcceptBookings |
PATCH /instructors/{id} | Die Tarife — hourlyPrice, extraStudentPrice, extraStudentPer, durationPrices, seasonPrices, includedStudents, groupClassesEnabled und groupPricePerStudent — für jeden im Kader (Stunden- und Platzpreis haben eine Untergrenze pro Währung, ein negativer extraStudentPrice ist ein 422 invalid_price, und das Einschalten von Gruppenkursen verlangt einen Platzpreis); ein neuer groupPricePerStudent stempelt auch die aktiven Gruppenkurse neu. autoAcceptBookings nur für einen Skilehrer, den die Skischule verwaltet. Alles wird zuerst geprüft und zusammen geschrieben: Ein Body, der wegen eines Feldes abgelehnt wird, ändert nichts |
managedBy ist die Regel, die entscheidet, wer antwortet: Steht dort instructor, gehören Kalender und Anfragen dem Skilehrer, und die API kann lesen, aber nicht schreiben; steht dort school, schreibt die Skischule – und damit die API – den Kalender und beantwortet die Anfragen, und der Skilehrer schaut zu. Gesetzt wird es von einer Person, im Kader des Dashboards; über die API lässt es sich nicht ändern. Gibst du einen Kalender an den Skilehrer zurück, werden die externen Blöcke gelöscht, die dein System darin geschrieben hat.
Verlässt ein Skilehrer die Skischule, verschwinden seine Buchungen aus GET /bookings und seine Ereignisse hören auf, während das, was er schon verdient hat, in deinem Guthaben bleibt. Weder das noch eine Änderung von managedBy sendet ein Ereignis: Vergleiche GET /instructors bei jeder Synchronisation.
unclaimed kennzeichnet einen Skilehrer, den die Skischule im Dashboard ohne Konto angelegt hat: Niemand meldet sich als er an, daher ist er immer managedBy school, und die Skischule ist in allem für ihn zuständig, auch im Chat. Übernimmt die Person das Profil mit ihrem eigenen Konto, wird unclaimed false und die Id des Skilehrers ändert sich in eine dauerhafte: Alle Pfade und der Filter instructorId akzeptieren weiterhin die alte Id, der Skilehrer trägt sie in previousId, und Ereignisse nennen ab dann die neue. Kein Ereignis kündigt das an: Vergleiche GET /instructors bei jeder Synchronisation und ordne previousId zu.
Verfügbarkeit
Die Verfügbarkeit eines Skilehrers ist eine Liste von Blöcken. Ein Block ist available, blocked oder vacation, gehört zu einem Skigebiet, in dem der Skilehrer unterrichtet, reicht von startTime bis endTime (endTime darf 24:00 sein) und ist entweder an einen Wochentag gebunden (dayOfWeek 0–6, Sonntag zuerst, optional mit since und until als erstem und letztem Datum, an dem er gilt) oder an ein Datum – genau eines von beiden. Neue available-Stunden müssen zwischen 07:00 und 21:00 Uhr Ortszeit des Skigebiets liegen (sonst 422 outside_ski_day); blocked- und vacation-Blöcke dürfen jede Uhrzeit abdecken. Kunden können nur innerhalb von available-Blöcken buchen, die nichts anderes überdeckt.
PUT /instructors/{id}/availability ersetzt den ganzen Plan – jedes Skigebiet, jeden Block, bis zu 400 – durch das, was du sendest, genau wie das Speichern des Kalenders im Dashboard. Ids sind optional: Der Server vergibt eine an einen Block ohne Id, behält die, die du sendest, und lehnt Duplikate oder das Präfix xb_ der externen Blöcke ab. Externe Blöcke (siehe unten) werden von PUT nie angerührt und dürfen darin nicht vorkommen. Ein fehlerhafter Block, ein unbekanntes Skigebiet, beide Anker zugleich oder ein since nach dem until des Blocks sind ein 422 invalid_availability. Blöcke, die nicht mehr gelten können (ein vergangenes Datum, ein Wochenblock nach seinem until), werden verworfen: PUT speichert sie nicht, GET liefert sie nicht. Ein Datum ist vergangen, wenn es überall vergangen ist, also um 12:00 UTC am Folgetag.
{
"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 übernimmt das Rechnen für dich: Für ein Skigebiet und bis zu 62 Tage liefert es pro Tag die freien Bereiche, die ein Kunde buchen könnte, und die belegten, jeder belegte Bereich beschriftet mit booking (ein bestätigter Unterricht), pending (eine bezahlte Anfrage, die auf Antwort wartet) oder external (ein Block aus der API). Es wendet den Puffer zwischen zwei Unterrichtseinheiten und die Vorlaufzeit an, die der Skilehrer eingestellt hat; Lücken, die kürzer sind als seine minimale Unterrichtsdauer, lässt es nicht weg, sodass eine Lücke von dreißig Minuten als frei erscheint.
Die erweiterte Ansicht
{
"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 Blöcke
Ein Unterricht, den du außerhalb von Alpine Pal verkauft hast, belegt den Skilehrer. Trage ihn als externen Block ein, und das Zeitfenster verschwindet aus der Suche, in jedem Skigebiet, in dem der Skilehrer unterrichtet – eine Person kann nicht in zwei Tälern zugleich sein. Blöcke setzen voraus, dass die Skischule den Skilehrer verwaltet (managedBy = school).
Jeder Block trägt deine eigene externalRef. Dieselbe Referenz noch einmal mit demselben Datum und denselben Stunden zu senden ist idempotent und liefert 200 mit dem gespeicherten Block, egal welche resortId du übergibst; dieselbe Referenz mit anderen Stunden ist ein 409 external_ref_exists – löschen und neu anlegen. Ein Block, der sich mit einer aktiven Buchung überschneidet, ist ein 409 booking_conflict mit den Stunden, die im Weg sind, und, wenn diese Buchung bezahlt ist, ihrer Id und Referenz: Dieser Unterricht ist auf unserer Seite schon verkauft.
Ein Skilehrer kann ab heute höchstens 400 externe Blöcke haben; einer mehr ist ein 422 limit_reached. Ein Block, dessen Datum vergangen ist (um 12:00 UTC am Folgetag), verschwindet von selbst: GET /blocks listet ihn nicht mehr, ein DELETE darauf ist ein 404, und er zählt nicht mehr zu den 400.
| Endpoint | Hinweise |
|---|---|
GET /instructors/{id}/blocks | Optionale Filter from, to, externalRef |
POST /instructors/{id}/blocks | date, startTime, endTime, externalRef (1–120 Zeichen), optional resortId (eines, in dem der Skilehrer unterrichtet; Standard ist sein erstes); 07:00–21:00, nicht in der Vergangenheit nach der Uhr dieses Skigebiets, höchstens 30 Tage über den Buchungshorizont von 182 Tagen hinaus |
DELETE /instructors/{id}/blocks/{blockId} | 204; ein Block des Plans (nicht aus der API) ist hier ein 404 |
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" }'Das Annehmen einer Buchungsanfrage prüft das Zeitfenster erneut: Ein externer Block auf seinen Stunden ergibt, wie eine aktive Buchung oder ein Gruppenkurs, ein 409 slot_conflict, und die Skischule muss die Anfrage ablehnen oder einen anderen Termin vorschlagen. Auf dem Platz ohne Skilehrer prüft das Annehmen nichts erneut: Seine Plätze wurden gezählt, als der Kunde bezahlt hat. Sobald dein Block mit 201 beantwortet ist, gehört dir die Stunde: Eine Bezahlung, die darauf schon im Gang war, wird erstattet, sobald ihre Zahlung eintrifft, und erreicht dich als booking.expired.
Unterricht ohne zugewiesenen Skilehrer
Eine Skischule kann Stunden verkaufen, ohne zu sagen, wer sie geben wird: Der Kunde bucht und bezahlt, und die Skischule weist einen Skilehrer zu, wann immer sie möchte – über die API, aus dem Dashboard oder am Treffpunkt mit dem, der gerade frei ist. In der API ist das der eigene Platz der Skischule: Er hat einen eigenen Kalender, einen eigenen Preis und einen eigenen Schalter für die Sofortbestätigung, er ist nicht unter /instructors aufgeführt, und seine Buchungen kommen mit unassigned: true und instructorId gleich dem Platz (pool_…) an. Er verkauft so viele Privatstunden gleichzeitig, wie es die capacity seines Kalenders erlaubt: Ein available-Block kann sie angeben (bis 50; ohne Angabe 1), gezählt im Skigebiet des Blocks, Minute für Minute: Die eigenen Blöcke eines Tages ersetzen die wöchentlichen in den Minuten, die sie abdecken, und innerhalb beider addieren sich überlappende Blöcke. Eine Privatstunde braucht über ihre ganze Dauer einen freien Platz. Die Gruppenkurse des Platzes belegen keine Plätze: Sie laufen neben den Privatstunden und nebeneinander.
GET /unassigned zeigt die Einstellungen; PATCH /unassigned setzt beliebige von hourlyPrice, extraStudentPrice (0 oder mehr; ein negativer ist ein 422 invalid_price), extraStudentPer, durationPrices, seasonPrices, includedStudents, autoAcceptBookings und maximumStudents. Der erste PATCH erstellt den Platz, und bis dahin sind seine Kalender- und Block-Endpoints 404. Es gibt keinen Schalter: active sagt, ob der Platz im Verkauf ist, und das ist er, sobald sein Stundenpreis über der Untergrenze liegt (oder, wenn er Formate verkauft, seine Formate und Saisons vollständig sind); er verkauft in den Stunden, die sein Kalender öffnet, eine Skischule, die diesen Unterricht nicht will, öffnet also keine. Ein active in einem PATCH wird ignoriert. Ansonsten funktioniert der Kalender des Platzes wie der eines Skilehrers, in den Skigebieten der Skischule.
Feste Formate. Statt nach Stunden kann eine Skischule die Privatstunden ihres Platzes in festen Formaten verkaufen, die im Dashboard eingerichtet werden: bis zu 6 (Vormittag, Nachmittag, ganzer Tag oder andere, jedes mit Beginn und Ende) und Saisons, Zeiträume mit einem Preis pro Format, pro Einheit für die ganze Gruppe. saleMode in GET /unassigned sagt, welches von beiden gilt, und lessonFormats listet Formate und Saisons auf (in der API nur lesbar). Solange der Platz Formate verkauft, ist jede seiner Privatstunden eines davon: Ein Format ohne Preis in der Saison eines Datums wird an diesem Tag nicht verkauft, und an einem Datum außerhalb jeder Saison wird nichts verkauft. Ein Format braucht über seine ganze Zeit einen freien Platz, wie jeder Unterricht auf dem Platz, und hat nicht die Grenze von 6 Stunden des Unterrichts nach Stunden. Ein solcher Platz ist im Verkauf, sobald seine Formate und Saisons vollständig sind, unabhängig von seinem Stundenpreis. Seine Buchungen tragen format (Id und Art), subtotal ist der Preis dieses Formats an diesem Tag, und propose-time verschiebt es nur auf dieselben Zeiten an einem Tag, der sein Format zum bezahlten Preis verkauft (sonst 409 format_fixed_time oder format_price_differs). In der Testumgebung nimmt POST /sandbox/bookings auf dem Platz formatId an und verwendet die Formate deiner Skischule.
| Endpoint | Entspricht |
|---|---|
GET, PUT /unassigned/availability | GET, PUT /instructors/{id}/availability |
GET /unassigned/availability/expanded | Die erweiterte Ansicht, für eines der Skigebiete der Skischule; jeder freie Abschnitt sagt zusätzlich, wie viele Plätze noch frei sind (places) |
GET, POST /unassigned/blocks · DELETE /unassigned/blocks/{blockId} | Externe Blöcke auf dem Platz: Jeder belegt einen freien Platz in seinem Skigebiet (bei mehr als einem Skigebiet ist resortId Pflicht: 422 resort_required) und wird nur abgelehnt (409 booking_conflict), wo er mehr Privatstunden als Plätze hinterließe, darunter eine von uns |
GET /bookings?instructorId=pool_<schoolId> | Nur der Unterricht ohne zugewiesenen Skilehrer |
POST /bookings/{id}/assign | instructorId im Body: verschiebt einen Unterricht ohne zugewiesenen Skilehrer zu jemandem aus deinem Kader – siehe Eine Anfrage beantworten |
{
"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 }]
}
]
}
}Bis der Unterricht zugewiesen ist, übernimmt die Skischule die Skilehrer-Seite: Sie beantwortet die Anfrage, chattet mit dem Kunden und vereinbart den Treffpunkt. Einmal zugewiesen, ist die Buchung die des Skilehrers wie jede andere, und was die Skischule damit noch tun darf, richtet sich nach managedBy.
Gruppenkurse
Ein Gruppenkurs ist ein Termin mit Plätzen, die unabhängige Kunden einzeln kaufen: einmalig (date) oder als wöchentliche Serie (dayOfWeek, optional mit since und until), mit einer Kapazität, einem Preis pro Platz für den ganzen Kurs, einem Niveau, einer Zielgruppe (audience: alle, Kinder oder Erwachsene; ein Kinderkurs kann minAge und maxAge angeben, und jeder Teilnehmer, der einen Platz bucht, muss dazu passen), einem festen Treffpunkt und einer Mindestteilnehmerzahl, damit er stattfindet. Die Kurse eines Skilehrers übernehmen ihren Platzpreis aus dessen eigenen Einstellungen (groupClassesEnabled und groupPricePerStudent, schreibbar über PATCH /instructors/{id}); nur der Platz ohne Skilehrer nennt ihn beim Anlegen, und eine Preisänderung stempelt die aktiven Kurse neu. Kurse werden im Panel veröffentlicht, einem Skilehrer übergeben und abgesagt — auf der API sind sie in v1 nur lesbar — und liegen auf einem Skilehrer des Kaders oder auf dem Platz ohne zugewiesenen Skilehrer.
GET /instructors/{id}/classes (und GET /unassigned/classes) listen die aktiven Kurse. GET …/classes/expanded?from&to löst sie in konkrete Termine mit ihren belegten Plätzen auf, laufende Bezahlvorgänge innerhalb ihrer 10 Minuten Haltefrist eingeschlossen — höchstens 62 Tage pro Aufruf. Im Kalender eines Skilehrers erscheinen die Stunden eines Kurses außerdem als belegt in der erweiterten Verfügbarkeit, und ein externer Block darauf wird mit einem 409 booking_conflict abgelehnt: Diese Stunden kannst du nicht anderswo verkaufen. Der Platz ohne zugewiesenen Skilehrer ist ein Team: Seine Kurse nehmen seinen Privatstunden keine Plätze weg, laufen neben ihnen und nebeneinander, und ein externer Block kollidiert nie mit ihnen.
{
"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"
}
]
}{
"data": [
{
"classId": "gc_8c4d21f0a9b3",
"date": "2027-01-13",
"startTime": "10:00",
"endTime": "12:00",
"capacity": 8,
"seatsTaken": 5
}
]
}Jeder verkaufte Platz kommt über /bookings als gewöhnliche Buchung mit ihrer groupClassId, bestätigt in dem Moment, in dem sie bezahlt ist (acceptedVia auto). propose-time und assign lehnen einen Platz ab — die Zeit gehört dem Kurs und allen seinen Teilnehmern, und ein Kurs wandert als Ganzes zu einem Skilehrer, aus dem Panel. Das Absagen eines Kurses (oder eines Tags der Serie) erstattet jeden Platz voll und sendet ein booking.cancelled pro Platz; ein Kurs, der 24 Stunden vor Beginn noch unter seinem minStudents liegt, wird genauso abgesagt, mit cancelledBy platform.
Buchungen
Nur bezahlte Buchungen erreichen die API: Eine Buchung existiert für dich ab dem Augenblick, in dem ihre Zahlung eingeht (paidAt), und eine erstattete bleibt mit ihrem paymentStatus gelistet. Ein Kunde wählt einen Skilehrer und ein Zeitfenster, bezahlt, und die Anfrage landet beim Skilehrer oder bei der Skischule. Von da an wird sie entweder beantwortet, oder sie läuft unbeantwortet ab und der Kunde bekommt sein Geld zurück.
Die Uhr: Die Antwortfrist beträgt 48 Stunden ab Zahlung, nie später als der Beginn des Unterrichts; ein Vorschlag oder dessen Ablehnung setzt sie zurück (nie über die frühere der beiden Anfangszeiten hinaus, der ursprünglichen und der vorgeschlagenen); ein stündlicher Durchlauf lässt ablaufen, was überfällig ist, sodass der Ablauf bis zu eine Stunde verspätet eintreten kann. Ein bestätigter Unterricht wird 24 Stunden vor Beginn zu upcoming und ist in_progress, während er läuft. Er wird abgeschlossen, wenn der Kunde ihn bestätigt, oder von selbst 48 Stunden nach seinem Ende; der Abschluss gibt deinen Anteil frei. Der Kunde kann bis freeCancellationHours Stunden vor Beginn kostenlos stornieren: die Frist der Buchung selbst, also die, die die Stornobedingungen deiner Skischule bei der Buchung vorsahen (standardmäßig 48); die Buchung behält sie, egal wie sich die Bedingungen später ändern.
| bookingStatus | Bedeutung |
|---|---|
pending_instructor_approval | Bezahlt, wartet auf eine Antwort vor instructorResponseDeadline |
change_proposed | Ein anderer Termin wurde vorgeschlagen; der Kunde nimmt ihn an, lehnt ihn ab oder lässt ihn ablaufen. Das ursprüngliche Zeitfenster bleibt gehalten; das vorgeschlagene nicht, und seine Annahme prüft es erneut |
confirmed | Angenommen (acceptedVia sagt, von wem: instructor, school, auto, customer) |
upcoming, in_progress | Bestätigt und kurz vor dem Unterricht oder währenddessen |
completed | Abgeschlossen; dein Anteil am Geld ist freigegeben |
rejected_by_instructor, expired | Vollständig erstattet |
cancelled_by_customer, cancelled_by_instructor | Storniert; Erstattung gemäß Stornierungsbedingungen |
no_show_customer, no_show_instructor, disputed, refunded | Aus dem Dashboard oder von uns gesetzt |
{
"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"
}Vom Kunden erhältst du einen Kurznamen (Vorname und Initiale), seine bevorzugte Sprache und seine Nachricht – genug, um den Unterricht zu geben, und das, was unsere Datenschutzerklärung ihm verspricht. E-Mail, Telefon und jede Kontokennung werden nie gesendet; das Gespräch mit dem Kunden bleibt im Chat auf Alpine Pal, wo der Skilehrer ist.
Auflisten und synchron bleiben
GET /bookings listet die bezahlten Buchungen deiner Skilehrer, älteste Änderung zuerst, mit Filtern für status (eine kommagetrennte Liste), instructorId, from und to (Unterrichtsdaten) und updatedSince (ein beliebiger ISO-8601-Zeitpunkt; einschließlich). Die Seiten sind Keyset-basiert auf (updatedAt, id): Folge nextCursor, bis er null ist. Der Cursor ist opak und läuft nie ab, aber er gehört zu den Filtern, mit denen er erzeugt wurde – änderst du sie, fängst du von vorn an; ein ausgedachter ist ein 400.
Das Muster für eine nächtliche oder stündliche Synchronisation ist dasselbe: Merke dir das neueste updatedAt, das du gesehen hast, und frage alles seitdem ab, genau so, wie du es erhalten hast. Webhooks sagen dir in dem Moment Bescheid, in dem sich etwas ändert; dieser Endpoint ist der Weg, nach einem Ausfall aufzuholen. updatedAt bewegt sich auch bei Änderungen, die kein Ereignis senden – der Unterricht wird upcoming oder in_progress, eine Erinnerung wurde gesendet –, sodass eine Zeile in einer Synchronisation auftauchen kann, ohne dass sich sichtbar etwas geändert hat.
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_…"Eine Anfrage beantworten
Aktionen stehen der Skischule bei Skilehrern offen, die sie verwaltet, und bei ihrem eigenen Platz für Unterricht ohne zugewiesenen Skilehrer; bei einem Skilehrer, der sich selbst verwaltet, lautet die Antwort 403 not_managed_by_school. Jede Aktion ist ein bedingtes Update: Ist die Buchung nicht mehr in dem Zustand, den die Aktion erwartet, lautet die Antwort 409 booking_state mit dem aktuellen Zustand in details, und nichts ändert sich. Eine Aktion zu wiederholen ist deshalb ungefährlich, und der Weg zu bestätigen, was passiert ist, ist GET /bookings/{id}.
| Endpoint | Regel |
|---|---|
POST /bookings/{id}/accept | Aus pending; der Unterricht darf nicht begonnen haben. Das Zeitfenster wird erneut geprüft: Eine aktive Buchung oder ein Gruppenkurs, die sich damit überschneiden (der Puffer des Skilehrers eingeschlossen, bezahlte Anfragen ebenso), oder ein externer Block auf seinen Stunden ist ein 409 slot_conflict. Auf dem Platz ohne Skilehrer wird nichts erneut geprüft: Seine Plätze wurden bei der Zahlung gezählt |
POST /bookings/{id}/reject | Aus pending oder change_proposed; erstattet dem Kunden den vollen Betrag |
POST /bookings/{id}/propose-time | date, startTime, optional message; nur aus pending, ein Vorschlag auf einmal. Der Kunde nimmt ihn an, lehnt ihn ab oder lässt ihn ablaufen; das ursprüngliche Zeitfenster bleibt gehalten und das vorgeschlagene nicht |
POST /bookings/{id}/cancel | Ein bestätigter, bevorstehender oder laufender Unterricht, der noch nicht geendet hat; erstattet dem Kunden den vollen Betrag |
POST /bookings/{id}/assign | instructorId im Body; nur ein Unterricht mit unassigned: true, aus pending, confirmed oder upcoming. Der Skilehrer muss in deinem Kader sein, aktiv und verifiziert, diese Disziplin in diesem Skigebiet unterrichten, in dieser Währung abrechnen, so viele Teilnehmer und diese Dauer zulassen und zu der Zeit frei sein; seine veröffentlichten Zeiten werden nicht geprüft – das ist deine Entscheidung. Die Frist bewegt sich nicht; ab hier antwortet, wer den Skilehrer verwaltet. Kunde und Skilehrer werden benachrichtigt, und ein booking.updated mit reason instructor_assigned folgt |
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?" }'Zwei Dinge fehlen hier mit Absicht: ein Nichterscheinen vermerken und einen Unterricht abschließen. Beide bewegen Geld aufgrund dessen, was auf der Piste passiert ist, und beide bleiben eine menschliche Handlung im Dashboard.
Webhooks
Registriere eine https-URL, und wir senden jedes Buchungsereignis per POST dorthin – dasselbe JSON, das die Buchungs-Endpoints liefern, verpackt in ein Ereignis. Bis zu 10 Endpoints pro Skischule, jeder abonniert einige Ereignisse oder mit "*" alle. Registriere sie aus dem Dashboard oder über die API; das Dashboard zeigt in beiden Fällen das Zustellprotokoll. Die URL muss https sein, mit einem Hostnamen, der einen Punkt enthält, ohne Zugangsdaten oder Fragment, und weder eine private Adresse noch unsere.
| Endpoint | Wirkung |
|---|---|
GET /webhooks | Deine Endpoints, Secrets eingeschlossen |
POST /webhooks | url, events (1–20, oder ["*"]), optional description; 201 mit dem Secret |
PATCH /webhooks/{id} | Beliebige von url, events, description, active; Wiedereinschalten setzt den Fehlerzähler zurück |
DELETE /webhooks/{id} | Entfernt ihn samt Zustellprotokoll |
POST /webhooks/{id}/rotate-secret | Ein neues Secret; das alte stirbt sofort |
POST /webhooks/{id}/test | Ein Ping, wie eine gewöhnliche Zustellung eingereiht und sofort versucht; meldet, wie dieser Versuch gelaufen ist, und schlägt er fehl, wird er wie jede andere wiederholt |
GET /webhooks/{id}/deliveries | Die neuesten zuerst, ohne Bodys: standardmäßig 50, mit ?limit= bis zu 100 |
POST /webhooks/{id}/deliveries/{deliveryId}/replay | Dasselbe Ereignis (dieselbe Id, derselbe Inhalt) noch einmal als eigene neue Zustellung, sofort versucht und bei Fehlschlag wie jede andere wiederholt |
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{
"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"
}
}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…e2a9Ereignisse. booking.requested – eine bezahlte Anfrage ist eingetroffen. booking.confirmed – vom Skilehrer oder von der Skischule angenommen, automatisch bestätigt, oder der Kunde hat einen vorgeschlagenen Termin angenommen: acceptedVia sagt, was davon, und previousStatus ist im letzten Fall change_proposed. booking.time_proposed – dem Kunden wurde ein anderer Termin vorgeschlagen. booking.rejected. booking.cancelled – cancelledBy ist customer, instructor, school oder platform (das Konto hinter der Buchung wurde gelöscht, oder ein Gruppenkurs wurde abgesagt, weil er sein Minimum nicht erreicht hat). booking.expired – niemand hat rechtzeitig geantwortet. booking.completed – der Unterricht ist zu Ende, Nichterscheinen eingeschlossen. booking.updated – alles andere, was sich zu melden lohnt, mit data.reason: proposal_declined, meeting_point_proposed, meeting_point_confirmed, dispute_opened, dispute_resolved, refund_settled (das Geld ist wieder beim Kunden), refund_issued (von uns) oder instructor_assigned (previousInstructorId nennt den Platz, auf dem der Unterricht lag). ping ist das Test-Ereignis, mit leerem data; es ist kein Typ, den du abonnierst. Für upcoming, in_progress, Kader- oder managedBy-Änderungen wird kein Ereignis ausgelöst.
Was dich erwartet. Die Zustellung erfolgt mindestens einmal: Eine Antwort, die nach unserem Timeout von 10 Sekunden eintrifft, zählt als Fehlschlag, und das Ereignis kommt erneut; behandle also die Ereignis-Id als Schlüssel und ignoriere Wiederholungen. Die Reihenfolge ist nicht garantiert – Wiederholungsversuche und der Durchlauf ordnen Ereignisse um –, sortiere also nach data.booking.updatedAt. Nicht jede Buchung beginnt mit booking.requested: Die eines Skilehrers mit eingeschalteter Sofortbestätigung (autoAcceptBookings) kommt direkt als booking.confirmed an, mit previousStatus pending_instructor_approval. Lege die Buchung in deinem System also beim ersten Ereignis an, das du für ihre Id siehst, gleich welchen Typs. Der Payload wird beim Erzeugen des Ereignisses eingefroren; ein Wiederholungsversuch trägt, was damals galt, und ein späteres Ereignis den neueren Zustand. Ein Ereignis kann verloren gehen, wenn unser Versuch, es aufzuzeichnen, fehlschlägt; GET /bookings?updatedSince ist das Sicherheitsnetz. Jedes Ereignis trägt sandbox: true oder false. Das Protokoll bewahrt Zustellungen 30 Tage auf; eine erneute Zustellung sendet eine vergangene noch einmal.
Zustellungen verifizieren
Jede Zustellung ist mit dem Secret des Endpoints signiert: Der Header X-AlpinePal-Signature trägt einen Zeitstempel und einen HMAC-SHA256 über den Zeitstempel, einen Punkt und den rohen Body. Verifiziere sie, bevor du irgendetwas vertraust, vergleiche in konstanter Zeit und lehne Zeitstempel ab, die mehr als 5 Minuten von deiner Uhr entfernt sind. Das Secret wird beim Erstellen des Endpoints und von GET /webhooks zurückgegeben und kann aus dem Dashboard oder mit POST /webhooks/{id}/rotate-secret rotiert werden; das alte stirbt sofort.
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"));
}Antworte mit einem beliebigen 2xx innerhalb von 10 Sekunden. Alles andere – ein 3xx eingeschlossen, denn wir folgen keinen Weiterleitungen – ist ein Fehlschlag: Wir versuchen es erneut nach 5 Minuten, 15 Minuten, 1 Stunde, 4 Stunden, 12 Stunden und 24 Stunden, insgesamt etwa 42 Stunden, jedes Mal mit derselben Ereignis-Id, damit du deduplizieren kannst. Ein Endpoint wird erst abgeschaltet, und die Verwalter der Skischule werden benachrichtigt, wenn eine Zustellung alle Versuche ausgeschöpft hat, ohne dass in der Zwischenzeit eine andere Zustellung angekommen ist; ein kurzer Ausfall schaltet ihn nicht ab, auch wenn viele Ereignisse unterwegs sind. Solange er abgeschaltet ist, werden keine neuen Ereignisse für ihn eingereiht (GET /bookings?updatedSince holt sie nach), und eine ausstehende Zustellung, deren Versuch in dieser Zeit fällig wird, wird als fehlgeschlagen markiert, ohne gesendet zu werden; Wiederholungen, die nach dem Wiedereinschalten fällig werden, gehen normal raus. Stelle die fehlgeschlagenen erneut zu. POST /webhooks/{id}/test reiht einen Ping wie eine gewöhnliche Zustellung ein, versucht ihn sofort und meldet, wie dieser Versuch gelaufen ist: Ein fehlgeschlagener Ping wird genauso wiederholt, zählt wie jede Zustellung für das Abschalten des Endpoints und wird bei einem abgeschalteten Endpoint als fehlgeschlagen markiert, ohne gesendet zu werden. GET /webhooks/{id}/deliveries zeigt die neuesten Zustellungen zuerst, standardmäßig 50 und mit ?limit= bis zu 100, mit ihrer Versuchszahl und ohne ihre Bodys: Jedes Ereignis hat eine Zustellung pro Endpoint, plus eine für jede erneute Zustellung, mit derselben Ereignis-Id.
Testumgebung
Jede Skischule bekommt einen Testzwilling, sobald ihr Inhaber im Dashboard einen Test-Schlüssel erzeugt (Verbindungen → Testumgebung): eine Kopie der Skischule mit zwei erfundenen Skilehrern – sandbox_ia_…, der Anfragen von Hand beantwortet, und sandbox_ib_…, mit eingeschalteter Sofortbestätigung – und dem Platz für Unterricht ohne zugewiesenen Skilehrer, jeder mit einem Kalender von 09:00–17:00 an jedem Tag in den Skigebieten der Skischule, 1 bis 6 Stunden pro Unterricht, bis zu 6 Teilnehmer, ohne Puffer und ohne Vorlaufzeit. Test-Schlüssel beginnen mit ap_test_ und erreichen nur den Zwilling; Live-Schlüssel nie. Alles andere ist dieselbe API: die Skilehrer des Zwillings auflisten, ihre Kalender schreiben, Webhooks registrieren, Anfragen beantworten, Unterricht zuweisen.
Was ein Kunde auf der Website tut, erledigt POST /sandbox/bookings für dich: Der Aufruf erstellt eine bezahlte Buchung für den Skilehrer, den du nennst, mit denselben Prüfungen wie eine echte – veröffentlichte Verfügbarkeit, keine Überschneidung – und ohne Geld im Spiel. Von da an läuft die echte Maschinerie, und die Aktionen unten vertreten den Kunden und die Uhr, sodass sich jedes Ereignis in Minuten beobachten lässt. Keine E-Mail verlässt die Testumgebung, und nichts darin ist je öffentlich; jedes Ereignis, das sie sendet, trägt sandbox: true, ebenso GET /me. POST /sandbox/reset leert sie, damit eine Testsuite sauber starten kann.
| Endpoint | Hinweise |
|---|---|
POST /sandbox/bookings | instructorId (ein Skilehrer des Zwillings oder sein Platz), date, startTime, durationHours (innerhalb der Grenzen des Skilehrers); optional numberOfStudents, resortId, discipline, language, customerMessage. 201 mit der Buchung; 403 not_sandbox mit einem Live-Schlüssel |
POST /sandbox/classes | Veröffentlicht einen Gruppenkurs auf einem Skilehrer des Zwillings oder seinem Platz, nach den Produktionsregeln; die Plätze kauft man mit POST /sandbox/bookings und groupClassId |
POST /sandbox/bookings/{id}/accept-proposal · decline-proposal | Die Antwort des Kunden auf einen vorgeschlagenen Termin |
POST /sandbox/bookings/{id}/cancel | Der Kunde storniert, nach denselben Regeln wie auf der Website |
POST /sandbox/bookings/{id}/complete | Was die Bestätigung des Kunden oder der Durchlauf 48 Stunden nach dem Unterricht tut: completed und dein Anteil freigegeben |
POST /sandbox/bookings/{id}/expire | Was der Durchlauf nach Ablauf der Frist tut: expired und erstattet |
POST /sandbox/reset | Löscht die Buchungen, Chats, Guthabenbewegungen und Gruppenkurse des Zwillings; Schlüssel, Webhooks, Kalender und das Zustellprotokoll bleiben |
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 }'In der Testumgebung weiterhin außer Reichweite: Reklamationen, Nichterscheinen, eine von uns ausgestellte Erstattung, eine Stornierung mit cancelledBy platform und der Treffpunkt (seine Ereignisse kommen von der Website). Buchungen der Testumgebung tragen ein leeres studentLevels und einen Kunden namens Sandbox C., der die Website auf Englisch liest.
OpenAPI
Die maschinenlesbare Beschreibung liegt unter https://alpinepal.com/api/v1/openapi.json – jeder Pfad, jede Methode, jeder Parameter und jedes Schema von oben, in OpenAPI 3.1. Richte einen Generator darauf, um einen typisierten Client zu bekommen, oder importiere sie in dein HTTP-Werkzeug zum Stöbern. Sie wird ohne Authentifizierung ausgeliefert und eine Stunde lang gecacht.
Versionen und Änderungen
Das ist v1. Innerhalb davon fügen wir nur hinzu: neue Felder, neue optionale Parameter, neue Ereignistypen, neue Endpoints. Deine Integration sollte Felder und Ereignisse ignorieren, die sie nicht kennt. Alles, was einen bestehenden Client brechen würde, kommt in eine v2 unter einer neuen Basis-URL, wobei v1 weiterläuft, während du umziehst.
2026-09 — v1: Skischule, Skigebiete, Skilehrer, Verfügbarkeit und erweiterte Ansicht, externe Blöcke, Buchungen mit Keyset-Paginierung, accept / reject / propose-time / cancel, signierte Webhooks mit Wiederholungsversuchen.
2026-09-18 — hourlyPrice und extraStudentPrice auf PATCH /instructors/{id}; der Platz für Unterricht ohne zugewiesenen Skilehrer unter /unassigned und POST /bookings/{id}/assign; reason und previousInstructorId auf booking.updated; die Testumgebung, ap_test_-Schlüssel und sandbox in jedem Ereignis.
2026-09-18 — Überarbeitung: booking_state trägt den aktuellen Zustand; updatedSince akzeptiert jeden ISO-Zeitpunkt; die Erstattung einer Buchung ohne Zahlung sendet ebenfalls refund_settled; ein booking.completed pro Unterricht; assign prüft die Grenzen des Skilehrers; meeting_point_proposed; POST /webhooks/{id}/deliveries/{deliveryId}/replay; die Kunden- und Uhr-Aktionen der Testumgebung; diese Seite rund um das, was der Code tut, neu geschrieben.
2026-09-20 — PATCH /instructors/{id} schreibt seine Felder zusammen oder gar nicht; ein Block, der mit 201 beantwortet wird, setzt sich jetzt gegen eine Bezahlung durch, die auf derselben Stunde im Gang war (der Kunde wird erstattet, du erhältst booking.expired).
2026-09-22 — Gruppenkurse: GroupClass und seine Termine unter /instructors/{id}/classes und /unassigned/classes (nur lesend), groupClassId auf jeder Buchung, cancelledBy platform, wenn ein Kurs sein Minimum verfehlt, POST /sandbox/classes und groupClassId auf POST /sandbox/bookings.
2026-09-23 — Gruppenpreise am Skilehrer: groupClassesEnabled und groupPricePerStudent auf Instructor und auf PATCH /instructors/{id}; ein neuer Platzpreis stempelt die aktiven Kurse neu; die Untergrenze pro Platz liegt bei 10 EUR / 10 USD.
2026-09-24 — dokumentiert: Die Buchung eines Skilehrers mit Sofortbestätigung kommt nur als booking.confirmed an, nie als booking.requested; lege Buchungen beim ersten Ereignis ihrer Id an. Keine Verhaltensänderung.
2026-09-27 — dokumentiert: Einen Treffpunkt vorzuschlagen sendet sehr wohl booking.updated, mit reason meeting_point_proposed; der Abschnitt zur Synchronisation sagte, es werde kein Ereignis gesendet. Keine Verhaltensänderung.
2026-09-27 — Blöcke mit vergangenem Datum werden verworfen: PUT /instructors/{id}/availability speichert und liefert sie nicht mehr, und ein externer Block verschwindet aus GET /blocks, sobald sein Datum vergangen ist, und zählt nicht mehr zur Grenze externer Blöcke, die jetzt ab heute zählt. Bisher sammelten sich beide an, bis der Skilehrer die Grenze erreichte.
2026-10-05 — Skilehrer, die die Skischule ohne Konto anlegt: unclaimed und previousId auf Instructor. Übernimmt die Person das Profil, ändert sich die Id; alle Pfade und der Filter instructorId akzeptieren weiterhin die alte.
2026-10-06 — Kapazität für Unterricht ohne zugewiesenen Skilehrer: capacity auf den available-Blöcken des Platzes (mehrere Privatstunden gleichzeitig, pro Skigebiet gezählt), places auf jedem freien Abschnitt seiner erweiterten Ansicht, und seine externen Blöcke belegen je einen Platz, statt die Stunden zu schließen. Seine Gruppenkurse kollidieren nur noch in ihrem eigenen Skigebiet. Die Kalender der Skilehrer bleiben unverändert: capacity auf einem davon ist ein 422 invalid_availability.
2026-10-06 — der Platz ohne zugewiesenen Skilehrer gibt seine Gruppenkurse neben seinen Privatplätzen und nebeneinander: Ein Kurs nimmt nicht mehr die Stunden (sie bleiben in free, mit ihren Plätzen), und ein externer Block kollidiert nicht mehr mit ihm. GroupClass erhält audience (all, children, adults) mit optionalem minAge und maxAge; ein Platz in einem Kinderkurs wird nur an als Kinder gebuchte Teilnehmer verkauft, in einem Erwachsenenkurs nur an Erwachsene. POST /sandbox/classes akzeptiert dieselben Felder.
2026-10-06 — Wochenblöcke und Serien von Gruppenkursen können später beginnen: since, ihr erstes Datum (einschließlich), lässt eine Skischule die Saison eintragen, bevor sie beginnt, ohne ihre Stunden schon jetzt zu verkaufen. PUT /instructors/{id}/availability nimmt es auf Blöcken mit dayOfWeek an (ein since nach dem until des Blocks ist ein 422 invalid_availability), GET liefert es, GroupClass enthält es und POST /sandbox/classes nimmt es an. Ohne since gilt eine Serie ab sofort, wie bisher.
2026-10-08 — ein Endpoint wird erst abgeschaltet, wenn eine Zustellung alle Versuche ausgeschöpft hat, ohne dass in der Zwischenzeit eine andere ankommt. Bisher wurde er nach zehn fehlgeschlagenen Versuchen hintereinander über alle Zustellungen hinweg abgeschaltet, sodass ein kurzer Ausfall mit vielen Ereignissen unterwegs ihn abschalten und ihre Wiederholungen verlieren konnte.
2026-10-16 — der Unterrichtstag geht von 07:00 bis 21:00 Uhr Ortszeit des Skigebiets (vorher 06:00 bis 23:00): externe Blöcke, vorgeschlagene Zeiten sowie Buchungen und Gruppenkurse in der Sandbox außerhalb davon ergeben 422 outside_ski_day, ebenso neue available-Stunden außerhalb davon bei PUT /instructors/{id}/availability und PUT /unassigned/availability. Bereits gespeicherte Stunden bleiben erhalten, blocked- und vacation-Blöcke dürfen jede Uhrzeit abdecken, und ein vorher veröffentlichter Gruppenkurs behält seine Zeit.
2026-10-16 — feste Formate auf dem Platz ohne zugewiesenen Skilehrer (API 1.8.0): GET /unassigned erhält saleMode und lessonFormats (Formate und Saisons mit einem Preis pro Format, nur lesbar), Booking erhält format (null bei Unterricht nach Stunden), POST /sandbox/bookings akzeptiert formatId, und propose-time auf einem Format-Unterricht behält dessen Zeiten und Preis (409 format_fixed_time, format_price_differs). Für einen Platz, der nach Stunden verkauft, ändert sich nichts.
2026-10-16 — kein Schalter mehr für nicht zugewiesenen Unterricht (API 1.8.1): active in GET /unassigned sagt jetzt, ob der Platz im Verkauf ist – das ist er, sobald sein Stundenpreis über der Untergrenze liegt (oder seine Formate und Saisons vollständig sind) –, und PATCH ignoriert es; eine Skischule, die diesen Unterricht nicht will, öffnet dafür keine Stunden.
2026-10-20 — Preise für mehrere Stunden am Stück (API 1.10.0): Instructor und UnassignedLessons erhalten `durationPrices`, optionale Festpreise für einen ganzen Unterricht von 2, 3 oder 4 Stunden für einen Schüler. Der 4-Stunden-Preis gilt auch für längeren Unterricht zu seinem Stundensatz, eine Dauer ohne eigenen Preis nimmt den Satz der längsten kürzeren Dauer, die einen hat, und zusätzliche Schüler werden weiterhin pro Stunde berechnet. Ohne sie bleibt alles stundenweise. PATCH /instructors/{id} und PATCH /unassigned nehmen sie an (null löscht sie); Preise, die pro Stunde teurer sind als ein kürzerer Unterricht oder insgesamt billiger als ein kürzerer, ergeben 422 `invalid_duration_prices`.
2026-10-19 — Mehrtagespakete (API 1.9.0): Ein Kunde kann mit einer Zahlung mehrere Tage des Kurses eines Gruppenkurses (GroupClass.packs, Preis pro Person nach Altersgruppe, mit courseId) oder mehrere Tage hintereinander eines festen Formats (UnassignedLessons: packDays und je Saison minDays und packPrices) kaufen. Jeder Tag ist eine eigene Buchung mit ihrem Anteil am Preis und einem `pack`, das Paket und Tag nennt; studentLevels kann für jeden Teilnehmer `band` enthalten.
2026-11-02 — Bedingungen pro Skigebiet (API 1.22.0): UnassignedLessons erhält `resortTerms`: pro Skigebiet-ID jedes Skigebiet, das anders verkauft, mit allen Preisen und Bedingungen, wie sie dort gelten (dieselben Felder wie auf oberster Ebene: `hourlyPrice`, `durationPrices`, `seasonPrices`, `saleMode`, `lessonFormats` und die übrigen). Bei nur einem Satz Bedingungen ist es `{}`, und ein nicht aufgeführtes Skigebiet verkauft mit den Werten der obersten Ebene. PATCH /unassigned akzeptiert `resortId`, um die Bedingungen dieses Skigebiets zu schreiben und die anderen unverändert zu lassen; ein Skigebiet, in dem die Schule nicht arbeitet, ergibt 422 `unknown_resort`. Ohne `resortId` wird die oberste Ebene geschrieben, die ein Skigebiet aus `resortTerms` nicht liest. `active` ist true, solange in mindestens einem Skigebiet verkauft wird. Rein additiv: Ohne Bedingungen pro Skigebiet wird alles gelesen und verkauft wie bisher.
2026-10-29 — Zeiten, in denen ein Skilehrer in seinem eigenen Google Kalender belegt ist (API 1.21.1): Ein Skilehrer kann die Zeiten, die sein Google Kalender als belegt anzeigt, auf Alpine Pal sperren lassen. Sie gehören nicht zum Zeitplan: GET /instructors/{id}/availability listet sie nicht, und ein PUT sieht und ändert sie nicht, während /availability/expanded diese Zeiten aus `free` herauslässt, wie alles, was nicht zum Verkauf steht. Sonst ändert sich nichts.
2026-10-27 — Preise nach Saison und inbegriffene Schüler (API 1.21.0): Instructor und UnassignedLessons erhalten `seasonPrices` und `includedStudents`, beide änderbar mit PATCH /instructors/{id} und PATCH /unassigned. `seasonPrices` listet bis zu 4 Saisons, jede mit einer `id`, einem `name` für das Dashboard (null, wenn keiner), 1 bis 10 Datumsbereichen (`from` und `to`, beide einschließlich) und eigenem `hourlyPrice` und `durationPrices`: Unterricht an einem Datum innerhalb einer Saison wird zu deren Preisen berechnet, ein Datum außerhalb jeder Saison zu den eigenen Preisen des Skilehrers oder des Platzes, wie bisher. Bereiche überschneiden sich nie, weder innerhalb einer Saison noch zwischen Saisons, und die Preise jeder Saison folgen denselben Regeln wie die eigenen des Skilehrers oder des Platzes. Ein PATCH sendet die ganze Liste (null oder eine leere Liste löscht sie): Behalte die Ids, die ein GET geliefert hat, und lass die Id bei einer neuen Saison weg. GET liefert null, wenn es keine gibt. `includedStudents` (1 bis 20, standardmäßig 1) gibt an, wie viele Schüler der Preis abdeckt: `extraStudentPrice` wird nur für jeden Schüler darüber hinaus berechnet. Saisons, die gegen eine Regel verstoßen, ergeben 422 `invalid_season_prices`, eine Zahl außerhalb ihres Bereichs 422 `invalid_included_students`. Rein additiv: Ohne Saisons und mit einem inbegriffenen Schüler bleibt jeder Preis wie bisher.
2026-10-26 — ein Preis von 0 oder weniger wird abgelehnt (API 1.20.4): In `durationPrices` ergibt eine Dauer, die der Skilehrer oder der Platz verkauft, mit einem Preis von 0 oder einem negativen Preis jetzt ein 422 `invalid_duration_prices`. Bisher wurde er stillschweigend verworfen und die Anfrage antwortete mit 200, sodass diese Dauer nach Stunden ging. Soll eine Dauer keinen eigenen Preis haben, lass ihren Schlüssel weg (oder sende null für das ganze Feld).
2026-10-26 — Korrekturen der Dokumentation (API 1.20.3): Diese Seite und die Spezifikation sagen jetzt, was die API bereits tat. Das Annehmen einer Anfrage prüft das Zeitfenster erneut (eine aktive Buchung, ein Gruppenkurs oder ein externer Block darauf ist ein 409 `slot_conflict`; auf dem Platz ohne Skilehrer wird nichts erneut geprüft). Schlüssel hören nur dann auf zu funktionieren, mit einem 403 `school_not_active`, wenn die Skischule geschlossen oder nicht freigegeben ist; eine gesperrte Skischule behält ihre API. Bei einem abgeschalteten Endpoint wird eine ausstehende Zustellung erst dann als fehlgeschlagen markiert, wenn ihr Versuch fällig wird; Wiederholungen, die nach dem Wiedereinschalten fällig werden, werden gesendet. Ein unbekannter `extraStudentPer` ist ein 400 `validation_error`, nicht das 422 `invalid_price`, das der Eintrag zu 1.13.0 nannte; `invalid_price` ist ein negativer `extraStudentPrice`. `platformFee` beträgt 10 % oder die höhere Provision, die eine Skischule schriftlich vereinbart hat, je nachdem, wie die Buchung verkauft wurde. Die Beispielbuchung erhält `unassigned`, `groupClassId`, `format`, `pack`, `equipment` und `freeCancellationHours`, und ihr Treffpunkt lautet `not_agreed`. Die Fehlertabelle listet alle Codes, darunter `managed_by_school`, die Codes für Pakete, Formate und Gruppenkurse sowie `inside_notice_window` (mit `hours`). Zustellungen werden standardmäßig zu 50 und mit `limit` bis zu 100 gelistet, und eine erneute Zustellung ist eine eigene Zustellung mit derselben Ereignis-Id. Ein Ping ist eine gewöhnliche Zustellung: Er wird bei Fehlschlag wiederholt, zählt für das Abschalten des Endpoints und wird bei abgeschaltetem Endpoint als fehlgeschlagen markiert, ohne gesendet zu werden. Keine Verhaltensänderung.
2026-10-26 — zwei Korrekturen (API 1.20.2): PATCH /instructors/{id} prüft `durationPrices` jetzt mit den eigenen Unterrichtsdauern des Skilehrers, wie das Panel, und lehnt die Preise von jemandem, dessen Unterricht bei 2 Stunden beginnt, nicht mehr als teurer als eine 1-Stunden-Einheit ab, die er gar nicht verkauft. Und mit einem Testschlüssel beschreiben GET und PATCH /unassigned die Formate, die die Sandbox verkauft (die der echten Skischule, die POST /sandbox/bookings schon verwendete): `saleMode` und `lessonFormats` zeigen bei einem Platz, der Formate verkauft, nicht mehr `hours` und leer.
2026-10-26 — einen Tag eines Pakets zuweisen (API 1.20.1): POST /bookings/{id}/assign antwortet 409 `pack_pending` für einen Tag eines Mehrtagespakets, der noch auf die Antwort der Skischule wartet, wenn der Skilehrer seine Buchungen selbst verwaltet (`managedBy: instructor`). Ein Paket wird als Ganzes beantwortet, und dieser Skilehrer könnte den Teil der Skischule nicht beantworten. Nimm zuerst das Paket an oder weise den Tag einem Skilehrer zu, den die Skischule verwaltet; bereits bestätigte Tage werden wie bisher einzeln zugewiesen.
2026-10-25 — ein Format bei einem Skilehrer schließen (API 1.20.0): Ein blocked-Block mit `formatId` wird jetzt im Kalender eines Skilehrers angenommen, der in Formaten verkauft, wie beim Platz ohne zugewiesenen Skilehrer seit 1.15.0, und schließt nur dieses Format an seinem Datum oder seinen Wochentagen; die anderen Formate dieser Stunden bleiben buchbar. Ein Skilehrer, der den Stunden der Skischule folgt (`followsSchoolHours`), erhält jetzt auch die Schließungen einzelner Formate des Platzes. Buchungen haben diese Schließungen schon in jedem Kalender beachtet.
2026-10-25 — die offenen Stunden der Skischule für ihre Skilehrer (API 1.19.0): Instructor erhält `followsSchoolHours`. Ein Skilehrer, der über die Einladung der Skischule kommt oder den die Skischule ohne Konto anlegt, startet jetzt mit `managedBy: school` und dieser Option aktiv: Sein Kalender ist eine Kopie der offenen Stunden des Platzes ohne zugewiesenen Skilehrer, in den Skigebieten, in denen er unterrichtet, und mit einem Unterricht zur Zeit, und jede Änderung am Zeitplan des Platzes kommt bei ihm an. Die Skischule schaltet es in ihrem Bereich um (Privatunterricht). Ein PUT /instructors/{id}/availability gibt ihm ab diesem Moment eigene Stunden: `followsSchoolHours` wird false. Skilehrer, die schon im Team sind, ändern sich nicht.
2026-10-23 — Skilehrer in festen Formaten (API 1.18.0): Instructor erhält `saleMode`, `hours` oder `formats`. Die Skischule kann jetzt jeden ihrer Skilehrer in festen Formaten mit einem Preis pro Saison verkaufen, wie ihren Platz ohne zugewiesenen Skilehrer (einzustellen in ihrem Bereich unter Privatunterricht); seine privaten Buchungen tragen dann `format`, wie die des Platzes, und die Stundentarife verkaufen nichts. Booking.format gehört nicht mehr nur dem Platz. Der 422 `format_required` lautet jetzt „Choose one of the lesson formats“. Rein additiv: Jeder Skilehrer verkauft stundenweise, bis die Skischule es ändert.
2026-10-23 — Schweizer Franken (API 1.17.0): School.currency kann `CHF` sein. Schweizer Skischulen und ihre Skilehrer legen ihre Preise in Franken fest und werden in Franken bezahlt, alle ihre Beträge (Preise, subtotal, Auszahlungen) sind also in CHF, mit einem eigenen Mindeststundenpreis.
2026-10-23 — Stornobedingungen pro Skischule (API 1.16.0): Booking erhält `freeCancellationHours`, auch in data.booking jedes Buchungsereignisses: bis wie viele Stunden vor Beginn der Kunde mit voller Erstattung stornieren kann, die Frist, unter der die Buchung verkauft wurde – 48, 72, 168, 336, 672 oder 1008. GET /me liefert das aktuelle `freeCancellationHours` der Skischule. Die Skischule legt ihre Bedingungen in ihrem Bereich fest (Profil → Stornobedingungen); sie gelten für alles, was die Skischule verkauft (Privatunterricht und Gruppenkurse ihrer Skilehrer und ihren Platz ohne zugewiesenen Skilehrer), und eine Buchung behält die Frist, unter der sie gemacht wurde, egal wie sich die Bedingungen ändern. Über die API nur lesbar. Rein additiv: frühere Buchungen tragen 48.
2026-10-21 — nur ein Format schließen (API 1.15.0): Ein `blocked`-Block des Platzes ohne zugewiesenen Skilehrer darf `formatId` tragen und schließt dann nur dieses Format an seinem Datum oder seinen Wochentagen. Die Plätze dieser Stunden bleiben für die anderen Formate; die Stunden zu schließen schloss jedes Format, das eine Minute mit ihnen teilte. Den ganzen Tag zu schließen heißt weiterhin, seine Stunden zu schließen. GET liefert das Feld; ein PUT ohne das Feld hebt die Schließung auf.
2026-10-21 — Formate für Westendorf (API 1.14.0): Beginn und Ende eines Formats liegen jetzt auf der Viertelstunde (08:45, 13:15), nicht nur zur vollen oder halben Stunde. Das `lessonFormats` von UnassignedLessons erhält `extraStudentPrice`: was jeder Schüler ab dem zweiten zu einem Kurs nach Formaten hinzufügt, einmal pro Kurs (pro Tag eines Pakets), getrennt vom stündlichen Aufpreis des Platzes; 0 lässt den Preis für die ganze Gruppe. Wird im Panel eingestellt.
2026-10-20 — Aufpreis pro Schüler und Kurs (API 1.13.0): Instructor und UnassignedLessons erhalten `extraStudentPer`, `hour` (wie bisher) oder `lesson`: ob `extraStudentPrice` für jede Stunde des Kurses oder einmal für den ganzen Kurs berechnet wird. Änderbar mit PATCH /instructors/{id} und PATCH /unassigned; jeder andere Wert ergibt 422 `invalid_price`.
2026-10-20 — Bedingungen von Courchevel und Westendorf (API 1.12.0): Jedes Format einer Saison hat seine eigene Mindestzahl an Tagen am Stück (`prices[].minDays`; das `minDays` der Saison ist jetzt das kleinste davon und veraltet), sodass Vormittage ab fünf Tagen und Nachmittage einzeln in denselben Wochen verkauft werden können. UnassignedLessons erhält `minBookingNoticeHours` (jetzt auch zwei und drei Tage) und `disciplines`, beide mit PATCH /unassigned änderbar. Ein Paket fester Formate auf einem Platz ohne Sofortbestätigung wartet jetzt auf die Skischule, wie jede andere Anfrage; wer einen seiner Tage annimmt oder ablehnt, beantwortet das ganze Paket. Der Platz erhält außerdem `minimumLessonDuration` und `maximumLessonDuration` (ganze Stunden, PATCH /unassigned; 422 `invalid_lesson_length`): Snowsports Westendorf verkauft zwei und drei Stunden, nie eine. Wird keine verkaufte Länge nach Stunden berechnet, darf `hourlyPrice` 0 sein, und der Platz bleibt im Verkauf. `durationPrices` nimmt für jede Länge von zwei bis sechs Stunden einen Schlüssel; eine Länge ohne Preis kostet den Stundensatz der letzten kürzeren mit Preis. Und `singleLevelBookings` (auch PATCH /unassigned): Eingeschaltet wird ein Privatkurs abgelehnt, dessen Schüler Niveaus oder Erwachsene und Kinder mischen, und der Kunde bucht für jede Gruppe einzeln; Courchevel setzt einen Lehrer pro Niveau und Alter ein.
2026-10-20 — Ausrüstungsverleih (API 1.11.0): Eine Skischule kann mit ihrem Unterricht Ausrüstung vermieten, zu einem Preis pro Tag und Artikel, den sie in ihrem Bereich festlegt. Booking erhält `equipment` (null, wenn nichts gemietet wurde): den Anteil dieser Buchung (amount, bereits in subtotal enthalten), die gesamte Miete, ihre Tage und je Teilnehmer die Artikel mit ihrem Preis und den Größen (EU-Schuhgröße, Körpergröße in cm). Provision und netAmount zählen sie wie den Unterricht, und eine Erstattung gibt sie mit dem Unterricht zurück.