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 API 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 cinque 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 smettono se la scuola viene disattivata o chiusa. 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 cinque 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, venti in dieci 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 dieci in dieci 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. Quattro codici portano details: validation_error elenca i campi sbagliati come [{path, message}] (al massimo dieci); 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}.

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, 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, slot_conflict, booking_conflict, external_ref_exists, concurrent_update, not_unassigned, not_on_sale, cancel_window_closed, lesson_started, lesson_endedUn 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, price_below_minimum, invalid_price, no_resorts, invalid_max_students, currency_mismatch, invalid_duration, too_many_students, instructor_not_verified, wrong_disciplineBen 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_…); 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 × (numberOfStudents − 1)) × durationHours, arrotondato alla valuta; è ciò che paga il cliente. platformFee è il nostro 10 %. netAmount è il 90 % 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) ed extraStudentPrice è 0 o più.

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)
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 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}hourlyPrice ed extraStudentPrice di chiunque nell'organico (il prezzo orario ha un minimo per valuta); autoAcceptBookings solo per un maestro gestito dalla scuola. I campi si applicano uno dopo l'altro: se il secondo è rifiutato, il primo resta

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.

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 until come ultima data in cui vale) o a una data precisa: esattamente uno dei due. 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 o i due ancoraggi insieme sono un 422 invalid_availability.

GET /instructors/{id}/availability
{
  "blocks": [
    { "id": "ab_1", "resortId": "r_baqueira", "type": "available",
      "dayOfWeek": 6, "startTime": "09:00", "endTime": "13:00", "until": "2027-04-15" },
    { "id": "ab_2", "resortId": "r_baqueira", "type": "vacation",
      "date": "2026-12-25", "startTime": "06:00", "endTime": "23: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.

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); 06:00–23: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 non ricontrolla i blocchi: se la richiesta è arrivata prima del tuo blocco, la scuola deve rifiutarla o proporre un altro orario. Un blocco e un pagamento in gara sulla stessa ora possono riuscire entrambi: quando il tuo blocco riceve 201, rielenca le prenotazioni del maestro se l'ora conta.

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_…). Una lezione per fascia oraria.

GET /unassigned mostra le impostazioni; PATCH /unassigned imposta uno qualsiasi tra active, hourlyPrice, extraStudentPrice, autoAcceptBookings e maximumStudents. Il primo PATCH crea il posto, e fino ad allora i suoi endpoint di calendario e blocchi danno 404. Accenderlo richiede un prezzo sopra il minimo nella stessa chiamata o in una precedente e le stazioni della scuola scelte nel pannello; spegnerlo è sempre permesso. Il calendario del posto funziona esattamente come quello di un maestro, nelle stazioni della scuola.

EndpointCome
GET, PUT /unassigned/availabilityGET, PUT /instructors/{id}/availability
GET /unassigned/availability/expandedLa vista espansa, per una delle stazioni della scuola
GET, POST /unassigned/blocks · DELETE /unassigned/blocks/{blockId}Blocchi esterni sul posto: la fascia non viene più venduta come lezione non assegnata
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"]
}

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.

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 48 ore prima dell'inizio.

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",
  "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" }],
  "customerMessage": "Two adults, first time on skis.",
  "customer": { "name": "Santi P.", "language": "es" },
  "meetingPoint": { "status": "pending" },
  "subtotal": 130,
  "platformFee": 13,
  "netAmount": 117,
  "currency": "EUR",
  "bookingStatus": "pending_instructor_approval",
  "paymentStatus": "succeeded",
  "acceptedVia": null,
  "instructorResponseDeadline": "2027-01-06T10:00:00.000Z",
  "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, un punto d'incontro proposto), 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; qualsiasi prenotazione viva che si sovrappone alla fascia (margine compreso, anche richieste pagate) è un 409 slot_conflict
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 dieci 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, consegnato ora; riferisce com'è andata
GET /webhooks/{id}/deliveriesLe ultime cento consegne, senza corpi
POST /webhooks/{id}/deliveries/{deliveryId}/replayLo stesso evento di nuovo come consegna nuova, inviata ora
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). 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 dieci 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. 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 trenta 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 cinque 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 dieci 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 che fallisce dieci tentativi di fila viene spento e i gestori della scuola avvisati; le sue consegne in sospeso passano a fallite e non vengono inviate quando lo riaccendi: reinvia quelle che ti servono. POST /webhooks/{id}/test manda un ping subito e riferisce com'è andata, e GET /webhooks/{id}/deliveries mostra le ultime cento consegne (una per evento ed endpoint, con il conteggio dei tentativi) senza i corpi.

Ambiente di prova

Ogni scuola ha una gemella di prova dal momento in cui il titolare crea una chiave di prova nel pannello (Collega il tuo sistema di prenotazioni → 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/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 e registrazioni contabili 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.