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.
| Intestazione | Dove |
|---|---|
Authorization: Bearer <chiave> | Ogni richiesta |
Content-Type: application/json | Ogni richiesta con corpo |
X-Request-Id | Ogni risposta, errori compresi; citalo quando ci scrivi |
Cache-Control: no-store, Vary: Authorization | Ogni risposta |
WWW-Authenticate: Bearer realm="alpinepal-api" | Risposte 401 |
Allow | Risposte 405 |
Retry-After (secondi) | Risposte 429; il corpo lo ripete come details.retryAfterSeconds |
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}.
| Stato | Codici | Quando |
|---|---|---|
400 | validation_error, invalid_cursor, invalid_json | La richiesta in sé è sbagliata |
401 | unauthenticated, invalid_api_key | Nessuna chiave, o una malformata, sconosciuta, revocata o dell'altro ambiente |
403 | forbidden, school_not_active, not_managed_by_school, not_sandbox | La chiave vale ma questa azione non spetta a te; not_sandbox è una chiave reale su un endpoint dell'ambiente di prova |
404 | not_found | Non esiste, o non è uno dei tuoi maestri. Non confermiamo mai che qualcosa esista fuori dalla tua scuola |
405 | method_not_allowed | Metodo sbagliato per il percorso; Allow elenca quelli validi |
409 | booking_state, slot_conflict, booking_conflict, external_ref_exists, concurrent_update, not_unassigned, not_on_sale, cancel_window_closed, lesson_started, lesson_ended | Un vero conflitto di stato; leggi lo stato attuale e decidi di nuovo |
413, 415 | payload_too_large, unsupported_media_type | I corpi devono essere JSON e sotto i 256 KB |
422 | outside_availability, in_the_past, too_far_ahead, outside_ski_day, invalid_availability, api_blocks_read_only, limit_reached, invalid_webhook_url, unknown_event, range_too_long, unknown_resort, price_below_minimum, invalid_price, no_resorts, invalid_max_students, currency_mismatch, invalid_duration, too_many_students, instructor_not_verified, wrong_discipline | Ben formata, ma infrange una regola del calendario, della lezione o della scuola |
429 | rate_limited | Vedi Retry-After |
500 | internal | Nostro; l'id della richiesta ci aiuta a trovarlo |
{
"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ù.
| Campo | Valori |
|---|---|
bookingStatus | Vedi la tabella in Prenotazioni |
paymentStatus | pending, processing, succeeded, failed, authentication_required, refund_pending, refunded; all'API arrivano solo succeeded e i due stati di rimborso |
acceptedVia | instructor, school, auto, customer, o null finché non è accettata |
studentLevels | Una voce per allievo: { level: beginner | intermediate | advanced, age: adult | child }; vuoto nell'ambiente di prova |
discipline | ski o snowboard, quella della lezione; le disciplines di un maestro possono dire anche both |
language / customer.language | La lingua della lezione come codice di due lettere, e la lingua in cui il cliente usa il sito (en, es, it) |
meetingPoint | status 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
| Endpoint | Restituisce |
|---|---|
GET / | Il descrittore: nome, versione, link a questa pagina e allo spec (senza chiave) |
GET /me | La tua scuola: id, slug, nome, valuta, paese e sandbox (true con una chiave di prova) |
GET /resorts | Ogni stazione in cui insegna uno dei tuoi maestri o il tuo posto per lezioni non assegnate, con fuso orario e valuta |
GET /instructors | Il 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.
{
"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
{
"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.
| Endpoint | Note |
|---|---|
GET /instructors/{id}/blocks | Filtri facoltativi from, to, externalRef |
POST /instructors/{id}/blocks | date, 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 |
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.
| Endpoint | Come |
|---|---|
GET, PUT /unassigned/availability | GET, PUT /instructors/{id}/availability |
GET /unassigned/availability/expanded | La 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}/assign | instructorId nel corpo: sposta una lezione non assegnata a qualcuno del tuo organico; vedi Rispondere a una richiesta |
{
"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.
| bookingStatus | Significato |
|---|---|
pending_instructor_approval | Pagata, 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 |
confirmed | Accettata (acceptedVia dice da chi: instructor, school, auto, customer) |
upcoming, in_progress | Confermata e vicina alla lezione o durante |
completed | Fatta; la tua parte del denaro è liberata |
rejected_by_instructor, expired | Rimborsata per intero |
cancelled_by_customer, cancelled_by_instructor | Annullata; rimborso secondo la politica di cancellazione |
no_show_customer, no_show_instructor, disputed, refunded | Impostati dal pannello o da noi |
{
"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.
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}.
| Endpoint | Regola |
|---|---|
POST /bookings/{id}/accept | Da 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}/reject | Da in attesa o change_proposed; rimborsa il cliente per intero |
POST /bookings/{id}/propose-time | date, 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}/cancel | Una lezione confermata, in arrivo o in corso non ancora finita; rimborsa il cliente per intero |
POST /bookings/{id}/assign | instructorId 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 |
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.
| Endpoint | Fa |
|---|---|
GET /webhooks | I tuoi endpoint, segreti compresi |
POST /webhooks | url, 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-secret | Un segreto nuovo; il vecchio muore all'istante |
POST /webhooks/{id}/test | Un ping, consegnato ora; riferisce com'è andata |
GET /webhooks/{id}/deliveries | Le ultime cento consegne, senza corpi |
POST /webhooks/{id}/deliveries/{deliveryId}/replay | Lo stesso evento di nuovo come consegna nuova, inviata ora |
curl -X POST https://alpinepal.com/api/v1/webhooks \
-H "Authorization: Bearer ap_live_…" \
-H "Content-Type: application/json" \
-d '{ "url": "https://crm.example.com/alpinepal", "events": ["*"] }'
# → 201 { "id": "wh_…", "secret": "whsec_…", … } — keep the secret{
"id": "evt_4b2c9e17a0d3",
"type": "booking.confirmed",
"apiVersion": "v1",
"createdAt": "2027-01-04T10:02:17.000Z",
"schoolId": "sch_2a71",
"sandbox": false,
"data": {
"booking": { "id": "b_3e8c1f", "bookingStatus": "confirmed", "acceptedVia": "school", "…": "…" },
"previousStatus": "pending_instructor_approval"
}
}POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: AlpinePal-Webhooks/1.0
X-AlpinePal-Event: booking.confirmed
X-AlpinePal-Event-Id: evt_4b2c9e17a0d3
X-AlpinePal-Delivery: whd_00dd9d2f0cdd
X-AlpinePal-Signature: t=1799999999,v1=5f1c…e2a9Eventi. 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.
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.
| Endpoint | Note |
|---|---|
POST /sandbox/bookings | instructorId (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-proposal | La risposta del cliente a un orario proposto |
POST /sandbox/bookings/{id}/cancel | Il cliente annulla, con le stesse regole del sito |
POST /sandbox/bookings/{id}/complete | Ciò che fa la conferma del cliente, o la scansione 48 ore dopo la lezione: completed e la tua parte liberata |
POST /sandbox/bookings/{id}/expire | Ciò che fa la scansione oltre il termine: expired e rimborsata |
POST /sandbox/reset | Elimina prenotazioni, chat e registrazioni contabili della gemella; chiavi, webhook, calendari e registro delle consegne restano |
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.