Vai al contenuto
Per le scuole di sci

API per le scuole di sci

Collega il software con cui la tua scuola già lavora: disponibilità in entrata, prenotazioni in uscita via webhook firmato, e risposte di ritorno senza aprire il pannello.

Un account scuola su Alpine Pal ti dà l'organico dei maestri, i loro calendari e le loro richieste di prenotazione in un unico pannello. L'API è quello stesso pannello per una macchina: un channel manager, un CRM o uno script tuo può leggere e scrivere la disponibilità dei tuoi maestri, sapere di ogni prenotazione pagata nel momento in cui avviene, e accettarla, rifiutarla o proporre un altro orario.

È volutamente stretta. I clienti pagano su Alpine Pal, quindi l'API non crea prenotazioni: registra le lezioni vendute altrove come blocchi che tengono il calendario in ordine. Ciò che muove denaro per quello che è successo in pista (segnare un'assenza, chiudere un reclamo) resta un atto umano nel pannello.

Ottenere una chiave

Le chiavi sono della scuola, non di una persona, e solo il titolare della scuola può crearle o revocarle, dalla scheda Connessioni del pannello. Una chiave si mostra una sola volta, alla creazione; conserviamo un hash e non possiamo mostrarla di nuovo. Una scuola può avere fino a 5 chiavi attive, così ogni integrazione ha la sua e una vecchia si revoca senza toccare le altre. Le chiavi reali iniziano con ap_live_; quelle dell'ambiente di prova iniziano con ap_test_ e si contano a parte.

Una chiave smette di funzionare nel momento in cui viene revocata, e tutte ricevono un 403 school_not_active quando la scuola viene chiusa o non è approvata. Una scuola soltanto sospesa (nascosta dal sito, senza la sua pagina) conserva le chiavi: le sue prenotazioni vanno avanti, e l'API con loro. Le chiavi non si gestiscono dall'API stessa. Per ruotarne una: crea la nuova, passa il tuo sistema a quella e revoca la vecchia; nel frattempo valgono entrambe. Il pannello mostra quando ogni chiave è stata usata l'ultima volta, con 5 minuti di precisione. Le chiavi sono per i server: non metterne mai una in un browser o in un'app mobile.

Autenticazione, intestazioni e corpi

Ogni richiesta porta la chiave come bearer token. L'URL di base è https://alpinepal.com/api/v1; le risposte sono JSON, mai in cache, e ognuna porta un X-Request-Id che puoi citarci. Le ore del giorno sono sull'orologio della stazione come HH:mm, le date come YYYY-MM-DD e gli istanti in ISO 8601 in UTC con millisecondi.

I corpi devono essere JSON con intestazione Content-Type: application/json (altrimenti 415), sotto i 256 KB (413), e vengono controllati in modo stretto: un campo sconosciuto è un 400 validation_error che elenca i percorsi di troppo. Un corpo vuoto si legge come oggetto vuoto. Un metodo che il percorso non ammette è un 405 con intestazione Allow; una barra finale è un 404.

IntestazioneDove
Authorization: Bearer <chiave>Ogni richiesta
Content-Type: application/jsonOgni richiesta con corpo
X-Request-IdOgni risposta, errori compresi; citalo quando ci scrivi
Cache-Control: no-store, Vary: AuthorizationOgni risposta
WWW-Authenticate: Bearer realm="alpinepal-api"Risposte 401
AllowRisposte 405
Retry-After (secondi)Risposte 429; il corpo lo ripete come details.retryAfterSeconds
curl
curl https://alpinepal.com/api/v1/me \
  -H "Authorization: Bearer ap_live_…"

Non c'è CORS: l'API è da server a server, e una richiesta da un browser fallirà. È voluto: una chiave in un browser è una chiave che chiunque può leggere.

Limiti di utilizzo

Per chiave, in finestre fisse di un minuto: 600 letture (GET) e 120 scritture (tutto il resto, DELETE compreso) al minuto. Oltre, la risposta è 429 rate_limited con intestazione Retry-After, in secondi. Una chiave di prova ha una quota propria. I fallimenti di autenticazione si limitano per IP, 20 in 10 minuti; da lì in poi ogni altro tentativo riceve 429 prima ancora che la chiave venga cercata. Test e reinvii dei webhook condividono una quota a parte di 10 in 10 minuti per chiave, perché ognuno è una richiesta al server di qualcun altro.

Errori

Ogni errore è la stessa busta: uno stato HTTP, un code stabile su cui ramificare, un messaggio per una persona e l'id della richiesta. Cinque codici portano details: validation_error elenca i campi sbagliati come [{path, message}] (al massimo 10); booking_state dice in che stato è ora la prenotazione ({bookingStatus}); booking_conflict indica le ore che intralciano e, se quella prenotazione è pagata, il suo id e riferimento; rate_limited ripete l'attesa come {retryAfterSeconds}; inside_notice_window indica il preavviso del maestro in ore ({hours}).

StatoCodiciQuando
400validation_error, invalid_cursor, invalid_jsonLa richiesta in sé è sbagliata
401unauthenticated, invalid_api_keyNessuna chiave, o una malformata, sconosciuta, revocata o dell'altro ambiente
403forbidden, school_not_active, managed_by_school, not_managed_by_school, not_sandboxLa chiave vale ma questa azione non spetta a te; not_sandbox è una chiave reale su un endpoint dell'ambiente di prova
404not_foundNon esiste, o non è uno dei tuoi maestri. Non confermiamo mai che qualcosa esista fuori dalla tua scuola
405method_not_allowedMetodo sbagliato per il percorso; Allow elenca quelli validi
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_disabledUn vero conflitto di stato; leggi lo stato attuale e decidi di nuovo
413, 415payload_too_large, unsupported_media_typeI corpi devono essere JSON e sotto i 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_windowBen formata, ma infrange una regola del calendario, della lezione o della scuola
429rate_limitedVedi Retry-After
500internalNostro; l'id della richiesta ci aiuta a trovarlo
409 Conflict
{
  "error": {
    "code": "booking_state",
    "message": "Invalid booking state",
    "requestId": "req_929e0418a4b7",
    "details": { "bookingStatus": "confirmed" }
  }
}

Leggere i dati

Id. I maestri portano l'id del loro account (user_…), oppure unclaimed_… finché nessuno ha rivendicato un profilo che la tua scuola ha aggiunto senza account; il posto per lezioni non assegnate di una scuola è pool_<schoolId>; i maestri dell'ambiente di prova sono sandbox_ia_… e sandbox_ib_…. Le prenotazioni sono b_…, gli eventi evt_…, le consegne whd_…, gli endpoint wh_…, le richieste req_…. I riferimenti sono AP-<anno>-<5 cifre> per le prenotazioni reali e SB<6 hex> nell'ambiente di prova.

Orari. date, startTime ed endTime sono l'orologio a muro di resortTimeZone, mai UTC; gli istanti (createdAt, updatedAt, paidAt, instructorResponseDeadline…) sono UTC. Una lezione nella notte del cambio d'ora si risolve alla prima occorrenza di un'ora ambigua, e un'ora che quella notte non esiste va avanti. endTime può essere 24:00. Se un'ora è passata si giudica sull'orologio della stazione.

Denaro. subtotal = (hourlyPrice + extraStudentPrice × max(0, numberOfStudents − includedStudents)) × durationHours, arrotondato alla valuta; è ciò che paga il cliente. hourlyPrice è quello della data della lezione: quello di una stagione (seasonPrices) se la data vi rientra, altrimenti quello proprio del maestro o del posto; includedStudents è quanti allievi copre, 1 salvo che la scuola ne indichi di più. platformFee è la nostra commissione: il 10 %, o quella più alta che la tua scuola ha concordato per iscritto con noi; ogni prenotazione conserva quella con cui è stata venduta. netAmount è il resto, subtotal − platformFee, accreditato sul registro della tua scuola quando la lezione si completa, chiunque l'abbia tenuta e chiunque lo gestisca, e annullato se la prenotazione viene rimborsata. Ogni maestro nel tuo organico fattura nella valuta della tua scuola; i prezzi orari hanno un minimo per valuta (10 EUR, 11 USD, 10 CHF) ed extraStudentPrice è 0 o più. Anche l'attrezzatura che il cliente noleggia dalla tua scuola con la lezione si somma a subtotal (equipment.amount) e conta come la lezione.

CampoValori
bookingStatusVedi la tabella in Prenotazioni
paymentStatuspending, processing, succeeded, failed, authentication_required, refund_pending, refunded; all'API arrivano solo succeeded e i due stati di rimborso
acceptedViainstructor, school, auto, customer, o null finché non è accettata
studentLevelsUna voce per allievo: { level: beginner | intermediate | advanced, age: adult | child }; vuoto nell'ambiente di prova
disciplineski o snowboard, quella della lezione; le disciplines di un maestro possono dire anche both
language / customer.languageLa lingua della lezione come codice di due lettere, e la lingua in cui il cliente usa il sito (en, es, it, fr, de, pl, nl, ru)
meetingPointstatus not_agreed, proposed o confirmed, più il title quando esiste
verificationStatus (maestro)draft, pending_review, verified, rejected

La tua scuola, le stazioni e i maestri

EndpointRestituisce
GET /Il descrittore: nome, versione, link a questa pagina e allo spec (senza chiave)
GET /meLa tua scuola: id, slug, nome, valuta, paese, freeCancellationHours (la sua politica di cancellazione, in ore) e sandbox (true con una chiave di prova)
GET /resortsOgni stazione in cui insegna uno dei tuoi maestri o il tuo posto per lezioni non assegnate, con fuso orario e valuta
GET /instructorsIl tuo organico, attivi e inattivi; chi ha lasciato la scuola sparisce, e il posto non c'è mai
GET /instructors/{id}Un maestro: tariffe, durate delle lezioni, margine, preavviso, discipline, lingue, verificationStatus, managedBy, autoAcceptBookings
PATCH /instructors/{id}Le tariffe — hourlyPrice, extraStudentPrice, extraStudentPer, durationPrices, seasonPrices, includedStudents, groupClassesEnabled e groupPricePerStudent — di chiunque nell'organico (i prezzi orario e a posto hanno un minimo per valuta, un extraStudentPrice negativo è un 422 invalid_price, e attivare le lezioni di gruppo richiede un prezzo a posto); un nuovo groupPricePerStudent ritimbra anche le lezioni di gruppo attive. autoAcceptBookings solo per un maestro gestito dalla scuola. Tutto si controlla prima e si scrive insieme: un corpo rifiutato per un campo non cambia nulla

managedBy è la regola che decide chi risponde: quando dice instructor, calendario e richieste sono suoi e l'API può leggere ma non scrivere; quando dice school, la scuola, e quindi l'API, scrive il calendario e risponde alle richieste, e il maestro guarda. Si imposta dall'organico nel pannello, da una persona, e non si cambia via API. Restituire il calendario al maestro cancella i blocchi esterni che il tuo sistema vi aveva scritto.

Quando un maestro lascia la scuola, le sue prenotazioni spariscono da GET /bookings e i suoi eventi si fermano, mentre ciò che ha già guadagnato resta sul tuo registro. Né questo né un cambio di managedBy manda un evento: confronta GET /instructors a ogni sincronizzazione.

unclaimed indica un maestro che la scuola ha aggiunto nel pannello senza account: nessuno accede con il suo nome, quindi è sempre managedBy school e la scuola risponde per lui in tutto, chat compresa. Quando la persona rivendica il profilo con il proprio account, unclaimed diventa false e l'id del maestro cambia in uno definitivo: tutti i percorsi e il filtro instructorId continuano ad accettare il vecchio id, il maestro lo riporta in previousId e gli eventi da quel momento citano quello nuovo. Nessun evento lo annuncia: confronta GET /instructors a ogni sincronizzazione e abbina previousId.

Disponibilità

La disponibilità di un maestro è una lista di blocchi. Un blocco è available, blocked o vacation, appartiene a una stazione in cui il maestro insegna, va da startTime a endTime (endTime può essere 24:00), ed è ancorato o a un giorno della settimana (dayOfWeek 0–6, domenica per prima, opzionalmente con since e until come prima e ultima data in cui vale) o a una data precisa: esattamente uno dei due. Le nuove ore available devono stare tra le 07:00 e le 21:00, ora della stazione (altrimenti 422 outside_ski_day); i blocchi blocked e vacation possono coprire qualsiasi orario. I clienti possono prenotare solo dentro blocchi available che nient'altro copre.

PUT /instructors/{id}/availability sostituisce l'intero orario, ogni stazione e ogni blocco, fino a 400, con ciò che invii, esattamente come salvare il calendario nel pannello. Gli id sono facoltativi: il server ne assegna uno a un blocco che non ce l'ha, conserva quelli che invii e rifiuta duplicati o il prefisso xb_ dei blocchi esterni. I blocchi esterni (sotto) non vengono mai toccati dal PUT e non devono comparirvi. Un blocco malformato, una stazione sconosciuta, i due ancoraggi insieme o un since successivo all'until del blocco sono un 422 invalid_availability. I blocchi che non possono più valere (una data passata, un blocco settimanale oltre il suo until) vengono scartati: il PUT non li salva e il GET non li restituisce. Una data è passata quando lo è ovunque, alle 12:00 UTC del giorno dopo.

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 fa i conti per te: per una stazione e fino a 62 giorni restituisce, per giorno, le fasce libere che un cliente potrebbe prenotare e quelle occupate, ognuna etichettata booking (una lezione confermata), pending (una richiesta pagata in attesa di risposta) o external (un blocco dall'API). Applica il margine tra le lezioni e il preavviso fissato dal maestro; non scarta i buchi più corti della sua durata minima, quindi un buco di trenta minuti risulta libero.

La vista espansa

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

Blocchi esterni

Una lezione venduta fuori da Alpine Pal occupa il maestro. Registrala come blocco esterno e la fascia sparisce dalla ricerca, in ogni stazione in cui insegna: una persona non può stare in due valli insieme. I blocchi richiedono che la scuola gestisca il maestro (managedBy = school).

Ogni blocco porta il tuo externalRef. Inviare lo stesso riferimento con la stessa data e le stesse ore è idempotente e restituisce 200 con il blocco salvato, qualunque resortId tu passi; lo stesso riferimento con altre ore è un 409 external_ref_exists: cancella e ricrea. Un blocco che si sovrappone a una prenotazione viva è un 409 booking_conflict con le ore che intralciano e, se quella prenotazione è pagata, il suo id e riferimento: quella lezione è già venduta da noi.

Un maestro può avere al massimo 400 blocchi esterni da oggi in poi; uno in più è un 422 limit_reached. Un blocco la cui data è passata (alle 12:00 UTC del giorno dopo) se ne va da solo: GET /blocks non lo elenca più, un DELETE su di esso è un 404 e smette di contare per i 400.

EndpointNote
GET /instructors/{id}/blocksFiltri facoltativi from, to, externalRef
POST /instructors/{id}/blocksdate, startTime, endTime, externalRef (1–120 caratteri), resortId facoltativo (una in cui insegna; per default la prima); 07:00–21:00, non nel passato sull'orologio di quella stazione, al massimo 30 giorni oltre l'orizzonte di prenotazione di 182 giorni
DELETE /instructors/{id}/blocks/{blockId}204; un blocco dell'orario (non dell'API) qui è un 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" }'

Accettare una richiesta ricontrolla la fascia: un blocco esterno sulle sue ore, come una prenotazione viva o una lezione di gruppo, dà un 409 slot_conflict, e la scuola deve rifiutarla o proporre un altro orario. Sul posto senza maestro accettare non ricontrolla nulla: i suoi posti sono stati contati quando il cliente ha pagato. Quando il tuo blocco riceve 201 l'ora è tua: un pagamento già in corso su quell'ora viene rimborsato quando prova a concludersi e ti arriva come booking.expired.

Lezioni senza maestro assegnato

Una scuola può vendere ore senza dire chi terrà la lezione: il cliente prenota e paga, e la scuola assegna un maestro quando vuole, via API, dal pannello o al punto d'incontro con chi è libero. Nell'API questo è il posto della scuola stessa: ha un calendario, un prezzo e un interruttore di conferma immediata propri, non compare in /instructors, e le sue prenotazioni arrivano con unassigned: true e instructorId uguale al posto (pool_…). Vende tante lezioni contemporaneamente quante ne consente la capacity del suo calendario: un blocco available può indicarla (fino a 50; 1 se manca), contata nella stazione del blocco minuto per minuto: i blocchi del giorno stesso sostituiscono quelli settimanali nei minuti che coprono, e all'interno di ciascuno i blocchi sovrapposti si sommano. Una lezione ha bisogno di un posto libero per tutta la sua durata. Le lezioni di gruppo del posto non occupano posti: si tengono insieme alle private e ad altre lezioni.

GET /unassigned mostra le impostazioni; PATCH /unassigned imposta uno qualsiasi tra hourlyPrice, extraStudentPrice (0 o più; uno negativo è un 422 invalid_price), extraStudentPer, durationPrices, seasonPrices, includedStudents, autoAcceptBookings e maximumStudents. Il primo PATCH crea il posto, e fino ad allora i suoi endpoint di calendario e blocchi danno 404. Non c'è un interruttore: active dice se il posto è in vendita, e lo è non appena il suo prezzo orario supera il minimo (o, se vende formati, formati e stagioni sono completi); vende nelle ore che apre il suo calendario, quindi una scuola che non vuole queste lezioni non ne apre nessuna. Un active in un PATCH viene ignorato. Per il resto, il calendario del posto funziona come quello di un maestro, nelle stazioni della scuola.

Formati fissi. Invece che a ore, una scuola può vendere le lezioni private del suo posto in formati fissi, impostati nel pannello: fino a 6 (mattino, pomeriggio, giornata intera o altro, ognuno con il suo inizio e la sua fine) e stagioni, periodi di date con un prezzo per formato, a lezione per tutto il gruppo. saleMode in GET /unassigned dice quale dei due, e lessonFormats elenca formati e stagioni (in sola lettura nell'API). Finché il posto vende formati, ogni sua lezione privata è uno di essi: un formato senza prezzo nella stagione di una data non si vende quel giorno, e una data fuori da ogni stagione non vende nulla. Un formato ha bisogno di un posto libero per tutto il suo orario, come ogni lezione del posto, e non ha il limite di 6 ore delle lezioni a ore. Un posto così è in vendita non appena formati e stagioni sono completi, qualunque sia il suo prezzo orario. Le sue prenotazioni portano format (id e tipo), subtotal è il prezzo di quel formato quel giorno, e propose-time lo sposta solo allo stesso orario in un giorno che venda il suo formato al prezzo pagato (altrimenti 409 format_fixed_time o format_price_differs). Nell'ambiente di prova, POST /sandbox/bookings sul posto accetta formatId e usa i formati della tua scuola.

EndpointCome
GET, PUT /unassigned/availabilityGET, PUT /instructors/{id}/availability
GET /unassigned/availability/expandedLa vista espansa, per una delle stazioni della scuola; ogni tratto libero dice anche quanti posti restano (places)
GET, POST /unassigned/blocks · DELETE /unassigned/blocks/{blockId}Blocchi esterni sul posto: ognuno occupa un posto libero nella sua stazione (con più di una stazione, resortId è obbligatorio: 422 resort_required), ed è rifiutato (409 booking_conflict) solo dove lascerebbe più lezioni che posti con una delle nostre tra queste
GET /bookings?instructorId=pool_<schoolId>Solo le lezioni non assegnate
POST /bookings/{id}/assigninstructorId nel corpo: sposta una lezione non assegnata a qualcuno del tuo organico; vedi Rispondere a una richiesta
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 }]
      }
    ]
  }
}

Finché la lezione non è assegnata, la scuola è il suo lato maestro: risponde alla richiesta, chatta con il cliente e concorda il punto d'incontro. Una volta assegnata, la prenotazione è del maestro come qualsiasi altra, e ciò che la scuola può ancora farne segue managedBy.

Lezioni di gruppo

Una lezione di gruppo è un evento con posti che clienti indipendenti comprano separatamente: singola (date) o serie settimanale (dayOfWeek, con since e until facoltativi), con una capienza, un prezzo a posto per l'intera lezione, un livello, per chi è (audience: tutti, bambini o adulti; una lezione per bambini può indicare minAge e maxAge, e ogni allievo che prenota un posto deve rientrarci), un punto d'incontro fisso e un minimo di partecipanti perché si tenga. Le lezioni di un maestro prendono il prezzo a posto dalle impostazioni del maestro stesso (groupClassesEnabled e groupPricePerStudent, scrivibili con PATCH /instructors/{id}); solo il posto senza maestro lo indica alla creazione, e un cambio di prezzo ritimbra le lezioni attive. Le lezioni si pubblicano, si affidano a un maestro e si annullano dal pannello — sull'API sono di sola lettura nella v1 — e vivono su un maestro dell'organico o sul posto senza maestro assegnato.

GET /instructors/{id}/classes (e GET /unassigned/classes) elencano quelle attive. GET …/classes/expanded?from&to le risolve in date concrete con i posti occupati, pagamenti in corso entro i loro 10 minuti di blocco inclusi — al massimo 62 giorni per chiamata. Sul calendario di un maestro le ore di una lezione risultano inoltre occupate nella vista espansa della disponibilità, e un blocco esterno su di esse viene rifiutato con un 409 booking_conflict: quelle ore non sono tue da vendere altrove. Il posto senza maestro assegnato è una squadra: le sue lezioni non tolgono posti alle private, si tengono insieme a loro e ad altre lezioni, e un blocco esterno non si scontra mai con esse.

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

Ogni posto venduto arriva da /bookings come una prenotazione normale con il suo groupClassId, confermata appena pagata (acceptedVia auto). propose-time e assign la rifiutano — l'orario è della lezione e di tutti i suoi partecipanti, e una lezione passa a un maestro intera, dal pannello. Annullare una lezione (o un giorno della serie) rimborsa ogni posto per intero e invia un booking.cancelled per posto; una lezione che 24 ore prima dell'inizio è ancora sotto il suo minStudents viene annullata allo stesso modo, con cancelledBy platform.

Prenotazioni

All'API arrivano solo prenotazioni pagate: una prenotazione esiste per te dall'istante in cui entra il pagamento (paidAt), e una rimborsata resta elencata con il suo paymentStatus. Un cliente sceglie maestro e fascia, paga, e la richiesta arriva al maestro o alla scuola. Da lì, o riceve risposta, o scade senza risposta e il cliente viene rimborsato.

L'orologio: il termine di risposta è 48 ore dal pagamento, mai oltre l'inizio della lezione; una proposta o un suo rifiuto lo azzerano (mai oltre il primo tra l'inizio originale e quello proposto); una scansione oraria fa scadere ciò che è in ritardo, quindi la scadenza può arrivare fino a un'ora dopo. Una lezione confermata diventa upcoming 24 ore prima dell'inizio e in_progress mentre si svolge. Si completa quando il cliente la conferma, o da sola 48 ore dopo la fine; il completamento libera la tua parte. Il cliente può annullare gratis fino a freeCancellationHours ore prima dell'inizio: il termine della prenotazione stessa, quello che aveva la politica di cancellazione della tua scuola quando è stata fatta (48 di default), e che mantiene anche se la politica cambia dopo.

bookingStatusSignificato
pending_instructor_approvalPagata, in attesa di risposta prima di instructorResponseDeadline
change_proposedÈ stato proposto un altro orario; il cliente accetta, rifiuta o lo lascia scadere. La fascia originale resta occupata; quella proposta no, e la sua accettazione la ricontrolla
confirmedAccettata (acceptedVia dice da chi: instructor, school, auto, customer)
upcoming, in_progressConfermata e vicina alla lezione o durante
completedFatta; la tua parte del denaro è liberata
rejected_by_instructor, expiredRimborsata per intero
cancelled_by_customer, cancelled_by_instructorAnnullata; rimborso secondo la politica di cancellazione
no_show_customer, no_show_instructor, disputed, refundedImpostati dal pannello o da noi
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"
}

Del cliente ricevi un nome breve (nome e iniziale), la lingua preferita e il messaggio: quanto basta per tenere la lezione, e ciò che la nostra informativa sulla privacy gli promette. Email, telefono e qualsiasi identificativo dell'account non vengono mai inviati; la conversazione con il cliente resta nella chat di Alpine Pal, dov'è il maestro.

Elencare e restare sincronizzati

GET /bookings elenca le prenotazioni pagate dei tuoi maestri, dalla modifica più vecchia alla più nuova, con filtri per status (lista separata da virgole), instructorId, from e to (date della lezione) e updatedSince (qualsiasi istante ISO 8601; inclusivo). Le pagine vanno per cursore su (updatedAt, id): segui nextCursor finché è null. Il cursore è opaco e non scade, ma appartiene ai filtri con cui è stato creato: se li cambi, ricomincia; uno inventato è un 400.

Lo schema per una sincronizzazione notturna o oraria è lo stesso: ricorda l'updatedAt più recente che hai visto e chiedi tutto da lì, così come l'hai ricevuto. I webhook ti avvisano nel momento in cui qualcosa cambia; questo endpoint è come ti rimetti in pari dopo un'interruzione. updatedAt si muove anche per cambi che non mandano eventi (la lezione che diventa upcoming o in_progress, un promemoria inviato), quindi una riga può comparire in una sincronizzazione senza nulla di visibilmente diverso.

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

Rispondere a una richiesta

Le azioni sono aperte alla scuola sui maestri che gestisce e sul proprio posto per lezioni non assegnate; su un maestro che si gestisce da sé la risposta è 403 not_managed_by_school. Ogni azione è un aggiornamento condizionale: se la prenotazione non è più nello stato che l'azione si aspetta, la risposta è 409 booking_state con lo stato attuale in details, e non cambia nulla. Ripetere un'azione è quindi sicuro, e il modo per confermare cosa è successo è GET /bookings/{id}.

EndpointRegola
POST /bookings/{id}/acceptDa in attesa; la lezione non deve essere iniziata. La fascia viene ricontrollata: una prenotazione viva o una lezione di gruppo che vi si sovrappone (margine del maestro compreso, anche richieste pagate), o un blocco esterno sulle sue ore, è un 409 slot_conflict. Sul posto senza maestro non si ricontrolla nulla: i suoi posti sono stati contati al pagamento
POST /bookings/{id}/rejectDa in attesa o change_proposed; rimborsa il cliente per intero
POST /bookings/{id}/propose-timedate, startTime, message facoltativo; solo da in attesa, una proposta alla volta. Il cliente accetta, rifiuta o la lascia scadere; la fascia originale resta occupata e quella proposta no
POST /bookings/{id}/cancelUna lezione confermata, in arrivo o in corso non ancora finita; rimborsa il cliente per intero
POST /bookings/{id}/assigninstructorId nel corpo; solo una lezione con unassigned: true, da in attesa, confermata o in arrivo. Il maestro deve essere nel tuo organico, attivo e verificato, insegnare quella disciplina in quella stazione, fatturare in quella valuta, ammettere quel numero di allievi e quella durata, ed essere libero a quell'ora; le sue ore pubblicate non vengono controllate, quello spetta a te. Il termine non si sposta; da lì in poi risponde chi gestisce il maestro. Cliente e maestro vengono avvisati, e segue un booking.updated con 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?" }'

Due cose non ci sono di proposito: segnare un'assenza e dare una lezione per completata. Entrambe muovono denaro in base a ciò che è successo in pista, ed entrambe restano un atto umano nel pannello.

Webhook

Registra un URL https e facciamo POST di ogni evento di prenotazione lì: lo stesso JSON che restituiscono gli endpoint delle prenotazioni, avvolto in un evento. Fino a 10 endpoint per scuola, ognuno iscritto ad alcuni eventi o a tutti con "*". Registrali dal pannello o via API; il pannello mostra il registro delle consegne in entrambi i casi. L'URL deve essere https, con un nome host che abbia un punto, senza credenziali né frammento, e non un indirizzo privato né il nostro.

EndpointFa
GET /webhooksI tuoi endpoint, segreti compresi
POST /webhooksurl, events (1–20, o ["*"]), description facoltativa; 201 con il segreto
PATCH /webhooks/{id}Uno qualsiasi tra url, events, description, active; riaccenderlo azzera il conteggio dei fallimenti
DELETE /webhooks/{id}Lo rimuove con il suo registro delle consegne
POST /webhooks/{id}/rotate-secretUn segreto nuovo; il vecchio muore all'istante
POST /webhooks/{id}/testUn ping, accodato come una consegna qualsiasi e tentato ora; riferisce com'è andato quel tentativo, e se fallisce viene ritentato come le altre
GET /webhooks/{id}/deliveriesLe più recenti per prime, senza corpi: 50 di default, fino a 100 con ?limit=
POST /webhooks/{id}/deliveries/{deliveryId}/replayLo stesso evento (stesso id, stesso contenuto) di nuovo, come una consegna nuova e a sé, tentata ora e ritentata come le altre
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

Eventi. booking.requested: è arrivata una richiesta pagata. booking.confirmed: accettata dal maestro o dalla scuola, confermata automaticamente, o il cliente ha accettato un orario proposto; acceptedVia dice quale, e previousStatus è change_proposed nell'ultimo caso. booking.time_proposed: al cliente è stato proposto un altro orario. booking.rejected. booking.cancelled: cancelledBy è customer, instructor, school o platform (l'account dietro la prenotazione è stato eliminato, o una lezione di gruppo è stata annullata per non aver raggiunto il suo minimo). booking.expired: nessuno ha risposto in tempo. booking.completed: la lezione è finita, assenze comprese. booking.updated: qualsiasi altra cosa che meriti un avviso, con data.reason: proposal_declined, meeting_point_proposed, meeting_point_confirmed, dispute_opened, dispute_resolved, refund_settled (il denaro è tornato al cliente), refund_issued (da noi) o instructor_assigned (previousInstructorId indica il posto su cui stava la lezione). ping è l'evento di prova, con data vuoto; non è un tipo a cui iscriversi. Nessun evento per upcoming, in_progress, né per cambi di organico o di managedBy.

Cosa aspettarsi. La consegna è almeno una volta: una risposta che arriva dopo i nostri 10 secondi di attesa conta come fallimento e l'evento torna, quindi tratta l'id dell'evento come chiave e ignora le ripetizioni. L'ordine non è garantito, i nuovi tentativi e la scansione lo alterano, quindi ordina per data.booking.updatedAt. Non ogni prenotazione comincia con booking.requested: quella di un maestro con la conferma immediata attiva (autoAcceptBookings) arriva direttamente come booking.confirmed, con previousStatus pending_instructor_approval. Quindi registra la prenotazione nel tuo sistema al primo evento che vedi per il suo id, di qualunque tipo sia. Il payload è congelato alla creazione dell'evento; un nuovo tentativo porta ciò che era vero allora, e un evento successivo lo stato più nuovo. Un evento può andare perso se fallisce il nostro tentativo di registrarlo; GET /bookings?updatedSince è la rete di sicurezza. Ogni evento porta sandbox: true o false. Il registro conserva le consegne 30 giorni; un reinvio manda di nuovo una consegna passata.

Verificare le consegne

Ogni consegna è firmata con il segreto dell'endpoint: l'intestazione X-AlpinePal-Signature porta una marca temporale e un HMAC-SHA256 della marca, un punto e il corpo grezzo. Verificala prima di fidarti di qualsiasi cosa, confronta in tempo costante e rifiuta marche a più di 5 minuti dal tuo orologio. Il segreto viene restituito alla creazione dell'endpoint e da GET /webhooks, e si può ruotare dal pannello o con POST /webhooks/{id}/rotate-secret; il vecchio muore all'istante.

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

Rispondi con un qualsiasi 2xx entro 10 secondi. Qualsiasi altra cosa, un 3xx compreso perché non seguiamo i reindirizzamenti, è un fallimento: riproviamo dopo 5 minuti, 15 minuti, 1 ora, 4 ore, 12 ore e 24 ore, circa 42 ore in tutto, ogni volta con lo stesso id di evento così puoi deduplicare. Un endpoint viene spento, e gli amministratori della scuola avvisati, solo quando una consegna ha esaurito tutti i tentativi senza che nel frattempo ne arrivi nessun'altra; una breve interruzione non lo spegne, anche con molti eventi in corso. Mentre è spento non gli si accodano eventi nuovi (GET /bookings?updatedSince ti rimette in pari), e una consegna in sospeso il cui tentativo scade in quel tempo passa a fallita senza essere inviata; i tentativi che scadono dopo che l'hai riacceso partono normalmente. Reinvia quelle fallite. POST /webhooks/{id}/test accoda un ping come una consegna qualsiasi, lo tenta subito e riferisce com'è andato quel tentativo: un ping fallito viene ritentato allo stesso modo, conta come qualsiasi consegna per lo spegnimento dell'endpoint e, con l'endpoint spento, passa a fallito senza essere inviato. GET /webhooks/{id}/deliveries mostra le consegne più recenti per prime, 50 di default e fino a 100 con ?limit=, con il conteggio dei tentativi e senza i corpi: ogni evento ha una consegna per endpoint, più una per ogni reinvio, con lo stesso id di evento.

Ambiente di prova

Ogni scuola ha una gemella di prova dal momento in cui il titolare crea una chiave di prova nel pannello (Connessioni → Ambiente di prova): una copia della scuola con due maestri fittizi, sandbox_ia_…, che risponde alle richieste a mano, e sandbox_ib_…, con la conferma immediata attiva, e il posto per lezioni non assegnate, ciascuno con un calendario 09:00–17:00 tutti i giorni nelle stazioni della scuola, da 1 a 6 ore per lezione, fino a 6 allievi, senza margine né preavviso. Le chiavi di prova iniziano con ap_test_ e raggiungono solo la gemella; le chiavi reali mai. Tutto il resto è la stessa API: elencare i maestri della gemella, scrivere i loro calendari, registrare webhook, rispondere alle richieste, assegnare lezioni.

Quello che un cliente fa sul sito lo fa per te POST /sandbox/bookings: crea una prenotazione pagata per il maestro che indichi, con gli stessi controlli di una vera, disponibilità pubblicata e nessuna sovrapposizione, e senza soldi di mezzo. Da lì gira la macchina vera, e le azioni qui sotto fanno le veci del cliente e dell'orologio, così ogni evento si vede in pochi minuti. Nessuna email esce dall'ambiente di prova e niente di esso è mai pubblico; ogni evento che invia porta sandbox: true, come GET /me. POST /sandbox/reset lo svuota perché una batteria di test parta pulita.

EndpointNote
POST /sandbox/bookingsinstructorId (un maestro della gemella o il suo posto), date, startTime, durationHours (entro i limiti del maestro); facoltativi numberOfStudents, resortId, discipline, language, customerMessage. 201 con la prenotazione; 403 not_sandbox con una chiave reale
POST /sandbox/classesPubblica una lezione di gruppo su un maestro della gemella o sul suo posto, con le regole di produzione; i posti si comprano con POST /sandbox/bookings e groupClassId
POST /sandbox/bookings/{id}/accept-proposal · decline-proposalLa risposta del cliente a un orario proposto
POST /sandbox/bookings/{id}/cancelIl cliente annulla, con le stesse regole del sito
POST /sandbox/bookings/{id}/completeCiò che fa la conferma del cliente, o la scansione 48 ore dopo la lezione: completed e la tua parte liberata
POST /sandbox/bookings/{id}/expireCiò che fa la scansione oltre il termine: expired e rimborsata
POST /sandbox/resetElimina prenotazioni, chat, registrazioni contabili e lezioni di gruppo della gemella; chiavi, webhook, calendari e registro delle consegne restano
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 }'

Resta fuori dalla portata dell'ambiente di prova: reclami, assenze, un rimborso emesso da noi, un annullamento con cancelledBy platform e il punto d'incontro (i suoi eventi nascono sul sito). Le prenotazioni dell'ambiente di prova portano studentLevels vuoto e un cliente chiamato Sandbox C. che usa il sito in inglese.

OpenAPI

La descrizione leggibile da una macchina vive su https://alpinepal.com/api/v1/openapi.json: ogni percorso, metodo, parametro e schema qui sopra, in OpenAPI 3.1. Puntaci un generatore per avere un client tipizzato, o importala nel tuo strumento HTTP per sfogliarla. Si serve senza autenticazione e resta in cache per un'ora.

Versioni e cambiamenti

Questa è la v1. Al suo interno aggiungiamo soltanto: campi nuovi, parametri facoltativi nuovi, tipi di evento nuovi, endpoint nuovi. La tua integrazione deve ignorare i campi e gli eventi che non conosce. Qualsiasi cosa che romperebbe un client esistente andrebbe in una v2 con un altro URL di base, con la v1 in funzione mentre migri.

  • 2026-09 — v1: scuola, stazioni, maestri, disponibilità e vista espansa, blocchi esterni, prenotazioni con paginazione per cursore, accept / reject / propose-time / cancel, webhook firmati con nuovi tentativi.

  • 2026-09-18 — hourlyPrice ed extraStudentPrice su PATCH /instructors/{id}; il posto per lezioni non assegnate in /unassigned e POST /bookings/{id}/assign; reason e previousInstructorId su booking.updated; l'ambiente di prova, le chiavi ap_test_ e sandbox in ogni evento.

  • 2026-09-18 — revisione: booking_state porta lo stato attuale; updatedSince accetta qualsiasi istante ISO; il rimborso di una prenotazione senza pagamento emette anch'esso refund_settled; un booking.completed per lezione; assign controlla i limiti del maestro; meeting_point_proposed; POST /webhooks/{id}/deliveries/{deliveryId}/replay; le azioni cliente e orologio dell'ambiente di prova; questa pagina riscritta su ciò che fa il codice.

  • 2026-09-20 — PATCH /instructors/{id} scrive i suoi campi insieme o nessuno; un blocco con 201 ora vince su un pagamento in corso sulla stessa ora (il cliente viene rimborsato e tu ricevi booking.expired).

  • 2026-09-22 — lezioni di gruppo: GroupClass e le sue occorrenze in /instructors/{id}/classes e /unassigned/classes (sola lettura), groupClassId su ogni prenotazione, cancelledBy platform quando una lezione non raggiunge il suo minimo, POST /sandbox/classes e groupClassId su POST /sandbox/bookings.

  • 2026-09-23 — prezzi di gruppo sul maestro: groupClassesEnabled e groupPricePerStudent su Instructor e su PATCH /instructors/{id}; un nuovo prezzo a posto ritimbra le lezioni attive; il minimo a posto è 10 EUR / 10 USD.

  • 2026-09-24 — documentato: la prenotazione di un maestro con conferma immediata arriva solo come booking.confirmed, mai come booking.requested; registra le prenotazioni al primo evento del loro id. Nessun cambio di comportamento.

  • 2026-09-27 — documentato: proporre un punto d'incontro invia booking.updated, con reason meeting_point_proposed; la sezione sulla sincronizzazione diceva che non inviava eventi. Nessun cambio di comportamento.

  • 2026-09-27 — i blocchi la cui data è passata vengono scartati: PUT /instructors/{id}/availability non li salva né li restituisce più, e un blocco esterno esce da GET /blocks quando la sua data è passata e smette di contare per il limite dei blocchi esterni, che ora conta da oggi in poi. Finora gli uni e gli altri si accumulavano finché il maestro arrivava al limite.

  • 2026-10-05 — maestri che la scuola aggiunge senza account: unclaimed e previousId su Instructor. Quando la persona rivendica il profilo, il suo id cambia; tutti i percorsi e il filtro instructorId accettano ancora quello vecchio.

  • 2026-10-06 — capienza delle lezioni non assegnate: capacity sui blocchi available del posto (più lezioni contemporaneamente, contate per stazione), places su ogni tratto libero della sua vista espansa, e i suoi blocchi esterni occupano un posto ciascuno invece di chiudere le ore. Le sue lezioni di gruppo ora si scontrano solo nella loro stazione. I calendari dei maestri non cambiano: capacity su uno di essi è un 422 invalid_availability.

  • 2026-10-06 — il posto senza maestro tiene le sue lezioni di gruppo insieme ai posti privati e ad altre lezioni: una lezione non si prende più le ore (restano in free, con i loro posti) e un blocco esterno non si scontra più con essa. GroupClass ha ora audience (all, children, adults) con minAge e maxAge facoltativi; un posto di una lezione per bambini si vende solo ad allievi prenotati come bambini, di una per adulti solo ad adulti. POST /sandbox/classes accetta gli stessi campi.

  • 2026-10-06 — i blocchi settimanali e le serie di lezioni di gruppo possono iniziare più tardi: since, la loro prima data (inclusa), permette a una scuola di caricare la stagione prima che apra senza vendere già le sue ore. PUT /instructors/{id}/availability lo accetta sui blocchi con dayOfWeek (un since successivo all'until del blocco è un 422 invalid_availability), il GET lo restituisce, GroupClass lo riporta e POST /sandbox/classes lo accetta. Senza since una serie vale da subito, come prima.

  • 2026-10-08 — un endpoint viene spento solo quando una consegna ha esaurito tutti i tentativi senza che nel frattempo ne arrivi nessun'altra. Prima si spegneva dopo dieci tentativi falliti di fila contando tutte le consegne, quindi una breve interruzione con molti eventi in corso poteva spegnerlo e farne perdere i tentativi.

  • 2026-10-16 — la giornata di lezione va dalle 07:00 alle 21:00, ora della stazione (prima dalle 06:00 alle 23:00): i blocchi esterni, gli orari proposti e le prenotazioni e lezioni di gruppo della sandbox fuori da essa danno 422 outside_ski_day, così come le nuove ore available fuori da essa su PUT /instructors/{id}/availability e PUT /unassigned/availability. Le ore già salvate restano, i blocchi blocked e vacation possono coprire qualsiasi orario e una lezione di gruppo pubblicata prima mantiene il suo orario.

  • 2026-10-16 — formati fissi sul posto senza maestro assegnato (API 1.8.0): GET /unassigned ha ora saleMode e lessonFormats (formati e stagioni con un prezzo per formato, in sola lettura), Booking ha format (null sulle lezioni vendute a ore), POST /sandbox/bookings accetta formatId, e propose-time su una lezione di formato ne mantiene orario e prezzo (409 format_fixed_time, format_price_differs). Nulla cambia per un posto che vende a ore.

  • 2026-10-16 — niente più interruttore per le lezioni non assegnate (API 1.8.1): active in GET /unassigned ora dice se il posto è in vendita, e lo è non appena il prezzo orario supera il minimo (o formati e stagioni sono completi), e PATCH lo ignora; una scuola che non vuole queste lezioni non apre ore per loro.

  • 2026-10-20 — prezzi per più ore di fila (API 1.10.0): Instructor e UnassignedLessons aggiungono `durationPrices`, prezzi fissi facoltativi per una lezione intera di 2, 3 o 4 ore per un allievo. Quello di 4 ore vale anche per lezioni più lunghe, alla sua tariffa oraria; una durata senza prezzo proprio prende la tariffa della durata più lunga, tra quelle più brevi, che ha un prezzo; e gli allievi in più si pagano sempre a ore. Senza di essi, tutto resta a ore. PATCH /instructors/{id} e PATCH /unassigned li accettano (null li cancella); prezzi più cari all'ora di una lezione più breve, o più economici in totale di una più breve, rispondono 422 `invalid_duration_prices`.

  • 2026-10-19 — pacchetti di più giorni (API 1.9.0): un cliente può comprare con un solo pagamento più giorni del corso di una lezione di gruppo (GroupClass.packs, con prezzo a persona per fascia d'età, e courseId) o più giorni di fila di un formato fisso (UnassignedLessons: packDays e, per stagione, minDays e packPrices). Ogni giorno è una prenotazione a sé con la sua parte del prezzo e un `pack` che dice di quale pacchetto e quale giorno si tratta; studentLevels può riportare il `band` di ogni allievo.

  • 2026-11-02 — condizioni per stazione (API 1.22.0): UnassignedLessons aggiunge `resortTerms`: per id di stazione, ogni stazione che vende in modo diverso, con tutti i prezzi e le condizioni come valgono lì (gli stessi campi del livello superiore: `hourlyPrice`, `durationPrices`, `seasonPrices`, `saleMode`, `lessonFormats` e gli altri). È `{}` quando le condizioni sono una sola, e una stazione assente vende con i valori del livello superiore. PATCH /unassigned accetta `resortId` per scrivere le condizioni di quella stazione e lasciare le altre come sono; una stazione dove la scuola non lavora risponde 422 `unknown_resort`. Senza `resortId` scrive il livello superiore, che una stazione di `resortTerms` non legge. `active` è true finché si vende in almeno una stazione. È solo un'aggiunta: senza condizioni per stazione tutto si legge e si vende come prima.

  • 2026-10-29 — ore in cui un maestro è occupato nel proprio Google Calendar (API 1.21.1): un maestro può far chiudere su Alpine Pal le ore che il suo Google Calendar segna come occupate. Non fanno parte dell'orario: GET /instructors/{id}/availability non le elenca e un PUT non le vede né le cambia, mentre /availability/expanded lascia quelle ore fuori da `free`, come tutto ciò che non è in vendita. Nient'altro cambia.

  • 2026-10-27 — prezzi per stagione e allievi inclusi (API 1.21.0): Instructor e UnassignedLessons aggiungono `seasonPrices` e `includedStudents`, entrambi impostabili con PATCH /instructors/{id} e PATCH /unassigned. `seasonPrices` elenca fino a 4 stagioni, ognuna con un `id`, un `name` per il pannello (null se non c'è), da 1 a 10 intervalli di date (`from` e `to`, entrambi inclusi) e i propri `hourlyPrice` e `durationPrices`: una lezione in una data dentro una stagione si paga ai prezzi di quella stagione, e una data fuori da tutte a quelli propri del maestro o del posto, come finora. Gli intervalli non si sovrappongono mai, né dentro una stagione né tra stagioni, e i prezzi di ogni stagione seguono le stesse regole di quelli propri del maestro o del posto. Un PATCH invia l'elenco intero (null o un elenco vuoto lo cancella): conserva gli id restituiti da un GET e ometti l'id per una stagione nuova. GET restituisce null quando non ce n'è nessuna. `includedStudents` (da 1 a 20, 1 di default) è quanti allievi copre il prezzo: `extraStudentPrice` si paga solo per ogni allievo oltre quel numero. Stagioni che violano una regola rispondono 422 `invalid_season_prices`, e un numero fuori dal suo intervallo 422 `invalid_included_students`. È solo un'aggiunta: senza stagioni e con un allievo incluso, ogni prezzo resta come prima.

  • 2026-10-26 — un prezzo di 0 o meno viene rifiutato (API 1.20.4): in `durationPrices`, una durata che il maestro o il posto vende con un prezzo di 0 o negativo è ora un 422 `invalid_duration_prices`. Finora veniva scartato senza dire nulla e la richiesta rispondeva 200, quindi quella durata andava a ore. Per non darle un prezzo proprio, non inviare la sua chiave (o invia null per tutto il campo).

  • 2026-10-26 — correzioni della documentazione (API 1.20.3): questa pagina e lo spec dicono ora ciò che l'API faceva già. Accettare una richiesta ricontrolla la fascia (una prenotazione viva, una lezione di gruppo o un blocco esterno che vi si sovrappone è un 409 `slot_conflict`; sul posto senza maestro non si ricontrolla nulla). Le chiavi smettono di funzionare, con un 403 `school_not_active`, solo se la scuola è chiusa o non approvata; una scuola sospesa conserva la sua API. Con un endpoint spento, una consegna in sospeso passa a fallita solo quando scade il suo tentativo; i tentativi che scadono dopo la riaccensione vengono inviati. Un `extraStudentPer` sconosciuto è un 400 `validation_error`, non il 422 `invalid_price` indicato nella voce della 1.13.0; `invalid_price` è un `extraStudentPrice` negativo. `platformFee` è il 10 %, o la commissione più alta concordata per iscritto da una scuola, secondo come è stata venduta la prenotazione. La prenotazione d'esempio aggiunge `unassigned`, `groupClassId`, `format`, `pack`, `equipment` e `freeCancellationHours`, e il suo punto d'incontro dice `not_agreed`. La tabella degli errori elenca tutti i codici, tra cui `managed_by_school`, quelli di pacchetti, formati e lezioni di gruppo e `inside_notice_window` (con `hours`). Le consegne si elencano 50 di default e fino a 100 con `limit`, e un reinvio è una consegna a sé con lo stesso id di evento. Un ping è una consegna normale: ritentato se fallisce, conteggiato per lo spegnimento dell'endpoint e, con l'endpoint spento, segnato come fallito senza essere inviato. Nessun cambio di comportamento.

  • 2026-10-26 — due correzioni (API 1.20.2): PATCH /instructors/{id} ora valuta `durationPrices` sulle durate di lezione del maestro stesso, come il pannello, quindi non rifiuta più i prezzi di chi parte da 2 ore perché più cari di una lezione di 1 ora che non vende. E con una chiave di prova, GET e PATCH /unassigned descrivono i formati che vende la sandbox (quelli della scuola reale, che POST /sandbox/bookings usava già): `saleMode` e `lessonFormats` non dicono più `hours` e vuoto per un posto che vende formati.

  • 2026-10-26 — assegnare un giorno di un pacchetto (API 1.20.1): POST /bookings/{id}/assign risponde 409 `pack_pending` per un giorno di un pacchetto di più giorni ancora in attesa della risposta della scuola quando il maestro gestisce da sé le sue prenotazioni (`managedBy: instructor`). Un pacchetto si risponde intero, e quel maestro non potrebbe rispondere per la parte della scuola. Accetta prima il pacchetto, o assegna il giorno a un maestro gestito dalla scuola; i giorni già confermati si assegnano uno per uno come prima.

  • 2026-10-25 — chiudere un formato su un maestro (API 1.20.0): un blocco blocked con `formatId` ora è accettato nel calendario di un maestro che vende a formati, come sul posto senza maestro assegnato dalla 1.15.0, e chiude solo quel formato nella sua data o nei suoi giorni della settimana; gli altri formati di quelle ore restano in vendita. Un maestro che segue le ore della scuola (`followsSchoolHours`) riceve ora anche le chiusure di un formato del posto. Le prenotazioni rispettavano già queste chiusure su qualsiasi calendario.

  • 2026-10-25 — le ore aperte della scuola per i suoi maestri (API 1.19.0): Instructor guadagna `followsSchoolHours`. Un maestro che entra con l'invito della scuola, o che la scuola aggiunge senza account, ora parte con `managedBy: school` e questa opzione attiva: il suo calendario è una copia delle ore aperte del posto senza maestro assegnato, nelle stazioni dove insegna e con una lezione alla volta, e ogni modifica all'orario del posto gli arriva. La scuola la cambia nel pannello (Lezioni private). Un PUT /instructors/{id}/availability gli dà ore proprie da quel momento: `followsSchoolHours` diventa false. I maestri già nello staff non cambiano.

  • 2026-10-23 — maestri venduti a formati fissi (API 1.18.0): Instructor guadagna `saleMode`, `hours` o `formats`. La scuola ora può vendere qualsiasi suo maestro a formati fissi con prezzo per stagione, come il suo posto senza maestro assegnato (si imposta nel pannello, in Lezioni private); le sue prenotazioni private portano allora `format`, come quelle del posto, e le tariffe orarie non vendono nulla. Booking.format non è più solo del posto. Il 422 `format_required` ora dice «Choose one of the lesson formats». È solo un'aggiunta: ogni maestro vende a ore finché la scuola non lo cambia.

  • 2026-10-23 — franchi svizzeri (API 1.17.0): School.currency può essere `CHF`. Le scuole svizzere e i loro maestri fissano i prezzi e incassano in franchi, quindi tutti i loro importi (prezzi, subtotal, pagamenti) sono in CHF, con un minimo orario proprio.

  • 2026-10-23 — politica di cancellazione per scuola (API 1.16.0): Booking guadagna `freeCancellationHours`, anche in data.booking di ogni evento di prenotazione: fino a quante ore prima dell'inizio il cliente può annullare con rimborso totale, il termine con cui è stata venduta la prenotazione — 48, 72, 168, 336, 672 o 1008. GET /me restituisce il `freeCancellationHours` attuale della scuola. La scuola fissa la sua politica nel pannello (Profilo → Politica di cancellazione); vale per tutto ciò che la scuola vende (le lezioni private e di gruppo dei suoi maestri e il suo posto senza maestro assegnato), e una prenotazione mantiene il termine con cui è stata fatta, comunque cambi la politica. In sola lettura nell'API. È solo un'aggiunta: le prenotazioni precedenti hanno 48.

  • 2026-10-21 — chiudere un solo formato (API 1.15.0): un blocco `blocked` del posto senza maestro assegnato può portare `formatId` e allora chiude solo quel formato nella sua data o nei suoi giorni della settimana. I posti di quelle ore restano agli altri formati; chiudere le ore chiudeva tutti i formati che vi condividevano un minuto. Chiudere l'intera giornata resta chiuderne le ore. Il GET restituisce il campo; un PUT che lo omette toglie la chiusura.

  • 2026-10-21 — formati per Westendorf (API 1.14.0): inizio e fine di un formato ora vanno ai quarti d'ora (08:45, 13:15), non solo all'ora esatta o alla mezz'ora. Il `lessonFormats` di UnassignedLessons aggiunge `extraStudentPrice`: quanto aggiunge ogni allievo oltre il primo a una lezione a formati, una volta per lezione (per giorno di un pacchetto), separato dal supplemento orario del posto; 0 lascia il prezzo per tutto il gruppo. Si imposta nel pannello.

  • 2026-10-20 — supplemento per allievo a lezione (API 1.13.0): Instructor e UnassignedLessons aggiungono `extraStudentPer`, `hour` (come finora) o `lesson`: se `extraStudentPrice` si paga per ogni ora della lezione o una sola volta per l'intera lezione. Si imposta con PATCH /instructors/{id} e PATCH /unassigned; un altro valore è 422 `invalid_price`.

  • 2026-10-20 — condizioni di Courchevel e Westendorf (API 1.12.0): ogni formato di una stagione ha il proprio minimo di giorni di fila (`prices[].minDays`; il `minDays` della stagione diventa il minore di essi ed è deprecato), così le mattine possono vendersi da cinque giorni e i pomeriggi singoli nelle stesse settimane. UnassignedLessons aggiunge `minBookingNoticeHours` (ora anche due e tre giorni) e `disciplines`, entrambi impostabili con PATCH /unassigned. Un pacchetto di formati fissi su un posto senza conferma immediata ora aspetta la scuola, come qualsiasi altra richiesta; accettare o rifiutare uno qualsiasi dei suoi giorni risponde per l'intero pacchetto. Il posto aggiunge anche `minimumLessonDuration` e `maximumLessonDuration` (ore intere, PATCH /unassigned; 422 `invalid_lesson_length`): Snowsports Westendorf vende due e tre ore, mai una. Se nessuna lezione in vendita va a ore, `hourlyPrice` può essere 0 e il posto resta in vendita. `durationPrices` accetta una chiave per ogni durata da due a sei ore; una durata senza prezzo va alla tariffa oraria dell'ultima precedente con prezzo. E `singleLevelBookings` (anche PATCH /unassigned): attivo, una lezione privata i cui allievi mescolano livelli, o adulti e bambini, viene rifiutata e il cliente fa una prenotazione per gruppo; Courchevel mette un maestro per livello e per età.

  • 2026-10-20 — noleggio attrezzatura (API 1.11.0): una scuola può noleggiare attrezzatura con le sue lezioni, a un prezzo al giorno per articolo che fissa nel suo pannello. Booking guadagna `equipment` (null se non è stato noleggiato nulla): la parte di questa prenotazione (amount, già dentro subtotal), il noleggio intero, i suoi giorni e, per allievo, gli articoli con il loro prezzo e le misure (numero di scarpa EU, altezza in cm). La commissione e netAmount lo contano come la lezione, e un rimborso lo restituisce con la lezione.