1. Cosa permettono le API
Con una chiave API puoi, dal tuo software, leggere e scrivere sul calendario delle tue strutture:
- leggere strutture e campi con i loro identificativi, necessari a tutte le altre chiamate;
- leggere le prenotazioni di un periodo, di una struttura o di un singolo campo, per allinearle al tuo archivio;
- creare prenotazioni dall'esterno (per esempio quando un cliente prenota dal tuo sito), con gli stessi controlli di sovrapposizione e chiusura del pannello;
- modificare o annullare una prenotazione esistente (spostamento di orario o campo, incasso, note, annullamento);
- leggere la disponibilità di un campo in un giorno: slot già occupati, orari di apertura e chiusure straordinarie, così da proporre gli slot liberi senza replicare le regole;
- leggere il listino di un campo per mostrare i prezzi al cliente finale.
Ogni chiave è legata all'account gestore che l'ha creata e vede solo le strutture, i campi e le prenotazioni di quell'account. Una risorsa che appartiene a un altro gestore non risponde 403 ma 404 not_found: non ne viene rivelata l'esistenza.
Non fanno parte di queste API le richieste degli organizzatori per l'uso dei campi nei tornei (si approvano dall'app), la creazione di strutture e campi, il salvataggio di orari, chiusure e listino: sono operazioni del pannello gestore.
2. Requisiti e attivazione
- Un account gestore campi VolleyFriends con almeno una struttura e un campo.
- Un abbonamento in corso con un piano che include la funzione «API per gestionali esterni». L'intera
v1— anche le sole letture — è riservata a chi ha questa funzione: è una funzione venduta, non un accessorio del pannello. - Una chiave API generata dal gestionale: admin.volleyfriends.com → Impostazioni → Chiavi API → Nuova chiave.
La chiave ha la forma vfk_ seguito da 40 caratteri esadecimali (44 caratteri in tutto) e viene mostrata una sola volta, subito dopo la creazione: nel database resta soltanto la sua impronta sha256, quindi non è recuperabile in nessun modo. Se la perdi, revocala e generane una nuova. Nell'elenco del pannello vedi solo il prefisso (es. vfk_9f2c), la data di creazione e quella dell'ultimo utilizzo.
3. Autenticazione
Ogni richiesta va autenticata con l'header X-Api-Key. Non servono token, login, scadenze o rinnovi: la chiave resta valida finché non la revochi.
# Le chiavi di questa pagina sono di esempio: sostituisci con la tua.
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues -H "X-Api-Key: vfk_9f2c41a7b83d05e6c19af4728b60d35e1c9a7f42"
Negli esempi che seguono la chiave è letta dalla variabile d'ambiente VF_API_KEY, come conviene fare anche in produzione.
Header assente, più corto di 12 caratteri, sconosciuto o appartenente a una chiave revocata: la risposta è 401 con questo corpo.
{
"error": "invalid_key",
"message": "Chiave API mancante o non valida (header X-Api-Key)."
}
- La chiave vale come una password con pieni poteri sul calendario delle tue strutture: non inserirla in pagine, app o JavaScript lato browser, dove sarebbe leggibile da chiunque. Usala solo da server a server.
- Non metterla nel codice sorgente né in un repository: usa una variabile d'ambiente o un archivio di segreti.
- Usa una chiave per ogni sistema collegato (sito, gestionale, script di allineamento): così puoi revocarne una senza spegnere gli altri.
- Se sospetti che sia stata esposta, revocala subito dal pannello: la revoca è immediata e non è mai bloccata dallo stato dell'abbonamento. Da quel momento ogni chiamata con quella chiave riceve
401 invalid_key. - Traffico esclusivamente su HTTPS; il campo «ultimo utilizzo» nel pannello ti aiuta ad accorgerti di usi inattesi.
4. Base URL
https://www.volleyfriends.com/api
I path documentati qui sotto vanno accodati alla base. Il prefisso /api appartiene al reverse proxy, quindi l'indirizzo completo di un endpoint è per esempio:
GET https://www.volleyfriends.com/api/manager-api/v1/availability
Tutte le risposte sono application/json con codifica UTF-8. Le richieste con corpo (POST, PATCH) devono avere Content-Type: application/json.
5. Rate limiting
Il limite è di 120 richieste al minuto per chiave API (finestra scorrevole di 1 minuto). Il conteggio è per chiave, non per indirizzo IP: i gestionali stanno spesso dietro lo stesso indirizzo del provider e un limite per IP penalizzerebbe gestori diversi ospitati sulla stessa infrastruttura. Se l'header X-Api-Key manca del tutto, il conteggio ricade sull'indirizzo IP.
Ogni risposta riporta lo stato del contatore negli header x-ratelimit-limit, x-ratelimit-remaining e x-ratelimit-reset (secondi al reset). Al superamento viene aggiunto anche retry-after.
Risposta al superamento — 429
{
"error": "rate_limited",
"message": "Limite di 120 richieste al minuto superato per questa chiave API."
}
- Per l'allineamento periodico usa una sola chiamata a
/bookingscon un intervallofrom–toampio, invece di una chiamata al giorno. - Metti in cache l'elenco di strutture e campi: cambia raramente.
- In caso di
429attendi e ritenta con backoff esponenziale (per esempio 2, 4, 8 secondi), rispettandoretry-after.
6. Endpoint
Sei endpoint, tutti sotto /manager-api/v1. Ogni chiamata passa dalla stessa catena di controlli, nell'ordine: chiave API valida → funzione «API per gestionali esterni» inclusa nel piano → rate limit → validazione dei parametri → verifica che la risorsa sia tua.
6.1 Strutture e campi
/api/manager-api/v1/venuesElenca le tue strutture, ciascuna con i propri campi annidati. È la chiamata da cui partire: gli id dei campi (courtId) servono a tutti gli altri endpoint. Le strutture sono ordinate per nome, i campi per nome.
Parametri
Nessuno.
Esempio di richiesta
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues -H "X-Api-Key: $VF_API_KEY"
Esempio di risposta — 200
{
"venues": [
{
"id": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"name": "Centro Sportivo Aurora",
"city": "Verona",
"address": "Via delle Palestre 12",
"verified": true,
"courts": [
{
"id": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"name": "Campo 1",
"surface": "parquet",
"indoor": true,
"lighting": true
},
{
"id": "e2a71f64-9c03-4bd8-b512-38f6cd90a7e1",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"name": "Campo 2 beach",
"surface": "sabbia",
"indoor": false,
"lighting": true
}
]
}
]
}
Campi della risposta
| Campo | Tipo | Descrizione |
|---|---|---|
venues[].id | uuid | Identificativo della struttura (venueId negli altri endpoint). |
venues[].name | string | Nome della struttura. |
venues[].city | string | null | Città. |
venues[].address | string | null | Indirizzo. |
venues[].verified | boolean | true se la struttura ha il badge VERIFICATO. |
venues[].courts[] | array | Campi della struttura. Array vuoto se non ne ha. |
courts[].id | uuid | Identificativo del campo (courtId negli altri endpoint). |
courts[].venueId | uuid | Struttura a cui appartiene il campo. |
courts[].name | string | Nome del campo. |
courts[].surface | string | null | Superficie, testo libero inserito dal gestore. |
courts[].indoor | boolean | Campo al coperto. |
courts[].lighting | boolean | Illuminazione disponibile. |
Se l'account non ha ancora strutture la risposta è {"venues": []}.
Errori specifici
Nessuno oltre a quelli generali.
6.2 Elenco prenotazioni
/api/manager-api/v1/bookingsRestituisce le prenotazioni delle tue strutture, ordinate per orario di inizio crescente. L'elenco è paginato: al massimo limit righe per chiamata (default 500, tetto 1000) — se ricevi esattamente limit righe, richiama con offset aumentato per la pagina successiva. In produzione filtra comunque sempre almeno per intervallo di date.
Parametri (query string)
| Nome | Tipo | Obbl. | Descrizione |
|---|---|---|---|
venueId | uuid | no | Limita a una struttura. Se non è tua: 404 not_found. |
courtId | uuid | no | Limita a un campo. Se non è tuo: 404 not_found. Combinato con un venueId di un'altra tua struttura il risultato è un elenco vuoto. |
from | data o istante ISO | no | Estremo inferiore incluso. Una data secca AAAA-MM-GG vale la mezzanotte locale italiana di quel giorno. |
to | data o istante ISO | no | Estremo superiore escluso. Una data secca vale la mezzanotte locale del giorno successivo: to=2026-08-31 include tutto il 31 agosto. |
limit | integer 1–1000 | no | Numero massimo di righe (default 500). |
offset | integer ≥ 0 | no | Righe da saltare (default 0): offset=500 è la seconda pagina con il limit di default. |
from e to si applicano all'istante di inizio della prenotazione. Una prenotazione iniziata prima di from e ancora in corso dopo non compare: se ti serve, allarga l'estremo inferiore di qualche ora. Il filtro non esclude le prenotazioni annullate: l'elenco contiene anche quelle con status: "cancelled".
Esempio di richiesta
curl -s -G https://www.volleyfriends.com/api/manager-api/v1/bookings -H "X-Api-Key: $VF_API_KEY" --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05" --data-urlencode "from=2026-08-01" --data-urlencode "to=2026-08-31"
Esempio di risposta — 200
{
"bookings": [
{
"id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"courtName": "Campo 1",
"startsAt": "2026-08-12T16:00:00.000Z",
"endsAt": "2026-08-12T17:30:00.000Z",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"priceCents": 3000,
"playersCount": null,
"status": "confirmed",
"paymentMethod": null,
"paymentLink": null,
"notes": "Prenotato dal sito della struttura",
"source": "api",
"recurrenceId": null,
"createdAt": "2026-07-28T09:14:02.517Z",
"updatedAt": "2026-07-28T09:14:02.517Z"
}
],
"limit": 500,
"offset": 0
}
Campi di una prenotazione
La stessa struttura è restituita da POST e PATCH (dentro la chiave booking).
| Campo | Tipo | Descrizione |
|---|---|---|
id | uuid | Identificativo della prenotazione. |
venueId | uuid | Struttura. |
courtId | uuid | Campo prenotato. |
courtName | string | Nome del campo, per comodità di visualizzazione. |
startsAt | istante ISO | Inizio, sempre in UTC (suffisso Z). |
endsAt | istante ISO | Fine, estremo escluso. |
customerName | string | Nome del cliente. |
customerPhone | string | null | Telefono. |
customerEmail | string | null | Email. |
priceCents | integer | null | Prezzo concordato in centesimi. null = da definire (mai 0 per «non definito»). |
playersCount | integer | null | Numero di persone dichiarato (solo prenotazioni su fasce con prezzo a persona). null = non applicabile o non indicato. |
status | string | pending · confirmed · paid · cancelled — vedi stati. |
paymentMethod | string | null | cash · bank_transfer · paypal · stripe · other. null = non incassato. |
paymentLink | string | null | Link di pagamento con carta generato dal pannello, se presente. In sola lettura. |
notes | string | null | Note libere. |
source | string | Origine: manual (pannello) · api (queste API) · app (richiesta di un giocatore dall'app) · tournament (torneo). |
recurrenceId | uuid | null | Presente e uguale su tutte le occorrenze di una serie ricorrente creata dal pannello. |
createdAt | istante ISO | Creazione. |
updatedAt | istante ISO | Ultima modifica. |
Errori specifici
| Codice | error | Quando |
|---|---|---|
| 400 | invalid_input | venueId/courtId non sono UUID, oppure from/to non sono date interpretabili. |
| 404 | not_found | La struttura ("Struttura non trovata") o il campo ("Campo non trovato") non esiste o non è tuo. |
6.3 Nuova prenotazione
/api/manager-api/v1/bookingsCrea una prenotazione su un tuo campo. La prenotazione nasce con source: "api", così nel pannello si distingue da quelle inserite a mano, e il gestore riceve una notifica push «Nuova prenotazione» sull'app.
Vengono applicate le stesse regole del pannello, nell'ordine:
- chiusura straordinaria della struttura nella data di inizio →
409 venue_closed; - sovrapposizione con un'altra prenotazione non annullata dello stesso campo →
409 overlap; - prezzo assente → calcolato dal listino del campo (
nullse l'orario di inizio non ricade in nessuna fascia). Se la fascia ha il prezzo a persona, serve ancheplayersCount: senza, la richiesta è rifiutata con400 players_required(a meno di indicare unpriceCentsesplicito).
Gli orari di apertura non bloccano la creazione: sono informativi, il gestore resta sovrano sul proprio campo. Se vuoi impedire prenotazioni fuori orario, controlla tu hours restituito da /availability prima di chiamare la POST.
Corpo della richiesta (JSON)
| Nome | Tipo | Obbl. | Descrizione |
|---|---|---|---|
courtId | uuid | sì | Campo da prenotare. Deve appartenere a una tua struttura. |
startsAt | istante ISO 8601 con offset | sì | Inizio. Sono accettate sia la forma 2026-08-12T18:00:00Z sia 2026-08-12T18:00:00+02:00. Senza offset: 400. |
endsAt | istante ISO 8601 con offset | sì | Fine, deve essere successiva a startsAt. |
customerName | string (1–160) | sì | Nome del cliente. |
customerPhone | string (max 40) | no | Telefono. |
customerEmail | email (max 200) | no | Email, validata come indirizzo. |
priceCents | integer ≥ 0 | no | Prezzo in centesimi. Se omesso viene calcolato dal listino del campo. Indica 0 solo per una prenotazione davvero gratuita. |
playersCount | integer 1–500 | no | Quante persone. Richiesto se il prezzo va calcolato dal listino e la fascia dell'orario di inizio è a persona; il totale applica l'eventuale minimo garantito della fascia. |
notes | string (max 2000) | no | Note libere, visibili al gestore nel pannello. |
status | pending | confirmed | paid | cancelled | no | Default confirmed. Da un gestionale esterno usa confirmed o paid: pending è riservato alle richieste che arrivano dall'app dei giocatori (vedi stati). |
paymentMethod | cash | bank_transfer | paypal | stripe | other | no | Come è stato (o sarà) incassato. Omesso = non incassato. |
recurrence | object | no | Non supportato da queste API: se presente la richiesta è rifiutata con 400 recurrence_unsupported (nessuna prenotazione creata). Le serie ricorrenti restano una funzione del pannello: da gestionale esterno ripeti la POST per ogni occorrenza. |
Esempio di richiesta
curl -s -X POST https://www.volleyfriends.com/api/manager-api/v1/bookings -H "X-Api-Key: $VF_API_KEY" -H "Content-Type: application/json" -d '{
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"startsAt": "2026-08-12T18:00:00+02:00",
"endsAt": "2026-08-12T19:30:00+02:00",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"notes": "Prenotato dal sito della struttura"
}'
Esempio di risposta — 201
{
"booking": {
"id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"courtName": "Campo 1",
"startsAt": "2026-08-12T16:00:00.000Z",
"endsAt": "2026-08-12T17:30:00.000Z",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"priceCents": 3000,
"playersCount": null,
"status": "confirmed",
"paymentMethod": null,
"paymentLink": null,
"notes": "Prenotato dal sito della struttura",
"source": "api",
"recurrenceId": null,
"createdAt": "2026-07-28T09:14:02.517Z",
"updatedAt": "2026-07-28T09:14:02.517Z"
}
}
Nell'esempio priceCents non era stato indicato: 90 minuti su una fascia da 20,00 €/ora danno 3000 centesimi (il calcolo è proporzionale ai minuti effettivi, con la tariffa della fascia in cui inizia la prenotazione).
Errori specifici
| Codice | error | Quando |
|---|---|---|
| 400 | invalid_input | Corpo non valido: campo obbligatorio mancante, istante senza offset, endsAt non successiva a startsAt, email non valida, lunghezze fuori limite. |
| 400 | players_required | Prezzo da calcolare dal listino su una fascia a persona, ma playersCount assente. |
| 400 | recurrence_unsupported | Il corpo contiene recurrence: le ricorrenze non sono disponibili da queste API. |
| 403 | subscription_required | Nessun abbonamento gestore in corso. |
| 404 | not_found | "Campo non trovato" — il courtId non esiste o non è tuo. |
| 409 | venue_closed | La struttura ha una chiusura straordinaria nella data di inizio. |
| 409 | overlap | Il campo ha già una prenotazione non annullata che si sovrappone all'intervallo. |
// 409 — sovrapposizione
{ "error": "overlap", "message": "Il campo è già prenotato in quell'orario." }
// 409 — chiusura straordinaria (il motivo, se indicato dal gestore, è nel messaggio)
{ "error": "venue_closed", "message": "Struttura chiusa in quella data (manutenzione)." }
Non esiste una chiave di idempotenza: ripetere la stessa POST dopo un timeout può creare un duplicato oppure, più spesso, restituire 409 overlap perché il primo tentativo era andato a buon fine. In caso di risposta incerta, prima di ritentare verifica con GET /bookings sull'intervallo interessato.
6.4 Modifica prenotazione
/api/manager-api/v1/bookings/{id}Aggiorna una prenotazione esistente. È un aggiornamento parziale: manda solo i campi che cambiano, gli altri restano invariati. Il corpo deve contenere almeno un campo.
Non esiste un endpoint di cancellazione: per annullare una prenotazione si imposta status: "cancelled". Lo storico resta nel pannello e lo slot torna libero (le prenotazioni annullate non occupano il campo e non generano overlap).
Parametro di percorso
| Nome | Tipo | Obbl. | Descrizione |
|---|---|---|---|
id | uuid | sì | Id della prenotazione, da GET /bookings o dalla risposta della POST. |
Corpo della richiesta (JSON)
Tutti i campi sono opzionali. Quelli marcati «annullabile» accettano null per svuotare il valore.
| Nome | Tipo | Note |
|---|---|---|
courtId | uuid | Sposta la prenotazione su un altro tuo campo, anche di un'altra tua struttura (venueId viene aggiornato di conseguenza). |
startsAt | istante ISO con offset | Nuovo inizio. |
endsAt | istante ISO con offset | Nuova fine. Il risultato finale deve restare successivo all'inizio, anche combinando un solo estremo con il valore già salvato. |
customerName | string (1–160) | — |
customerPhone | string (max 40) | Annullabile. |
customerEmail | email (max 200) | Annullabile. |
priceCents | integer ≥ 0 | Annullabile (null = prezzo da definire). In PATCH il prezzo non viene ricalcolato dal listino: se sposti l'orario e vuoi il nuovo prezzo, indicalo. |
playersCount | integer 1–500 | Annullabile. Aggiorna solo il dato: il prezzo non viene ricalcolato d'ufficio — se il numero di persone cambia il totale, manda anche il nuovo priceCents. |
notes | string (max 2000) | Annullabile. |
status | pending | confirmed | paid | cancelled | Qualsiasi transizione è ammessa. È anche il modo di rispondere a una richiesta arrivata dall'app (status: "pending" con source: "app"): portala a confirmed per accettarla o a cancelled per rifiutarla — il giocatore riceve la notifica. |
paymentMethod | cash | bank_transfer | paypal | stripe | other | Annullabile. |
I controlli di chiusura e sovrapposizione vengono rifatti in due casi: quando la richiesta sposta lo slot (startsAt, endsAt o courtId) e quando riattiva una prenotazione annullata (status da cancelled a qualsiasi altro stato) — da annullata non occupava il campo, e nel frattempo lo slot può essere stato preso: in quel caso la PATCH risponde 409 overlap o 409 venue_closed. Una PATCH che cambia solo altri campi (prezzo, note, incasso) non li ripete.
Esempio di richiesta — spostamento e incasso
curl -s -X PATCH https://www.volleyfriends.com/api/manager-api/v1/bookings/9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f -H "X-Api-Key: $VF_API_KEY" -H "Content-Type: application/json" -d '{
"startsAt": "2026-08-12T19:00:00+02:00",
"endsAt": "2026-08-12T20:30:00+02:00",
"status": "paid",
"paymentMethod": "cash",
"priceCents": 3000
}'
Esempio di richiesta — annullamento
curl -s -X PATCH https://www.volleyfriends.com/api/manager-api/v1/bookings/9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f -H "X-Api-Key: $VF_API_KEY" -H "Content-Type: application/json" -d '{ "status": "cancelled" }'
Esempio di risposta — 200
La prenotazione aggiornata, nello stesso formato della POST:
{
"booking": {
"id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"courtName": "Campo 1",
"startsAt": "2026-08-12T17:00:00.000Z",
"endsAt": "2026-08-12T18:30:00.000Z",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"priceCents": 3000,
"playersCount": null,
"status": "paid",
"paymentMethod": "cash",
"paymentLink": null,
"notes": "Prenotato dal sito della struttura",
"source": "api",
"recurrenceId": null,
"createdAt": "2026-07-28T09:14:02.517Z",
"updatedAt": "2026-07-28T11:02:47.883Z"
}
}
Errori specifici
| Codice | error | Quando |
|---|---|---|
| 400 | invalid_input | Corpo vuoto ("Nessun campo da aggiornare"), valori non validi, oppure orario di fine non successivo a quello di inizio ("L'orario di fine deve essere successivo a quello di inizio"). |
| 403 | subscription_required | Nessun abbonamento gestore in corso. |
| 404 | not_found | "Prenotazione non trovata" oppure "Campo non trovato" se il nuovo courtId non è tuo. |
| 409 | venue_closed | Spostamento in una data di chiusura straordinaria. |
| 409 | overlap | Il nuovo orario si sovrappone a un'altra prenotazione non annullata dello stesso campo. |
6.5 Disponibilità di un campo in un giorno
/api/manager-api/v1/availabilityRestituisce, per un campo e un giorno, gli slot occupati, gli orari di apertura di quel giorno della settimana e l'eventuale chiusura straordinaria. È l'endpoint da usare per calcolare gli slot liberi su un sito esterno senza duplicare le regole del gestionale: sottrai le prenotazioni dalla finestra di apertura.
Sono elencati tutti gli slot che occupano il campo, in qualunque stato tranne cancelled: quindi anche le richieste di prenotazione online ancora pending. Il giorno è quello locale italiano e gli intervalli sono semiaperti: una prenotazione che finisce esattamente a mezzanotte appartiene al giorno precedente. Una prenotazione a cavallo della mezzanotte compare in entrambi i giorni che interseca, con i suoi istanti reali (che possono cadere fuori dal giorno richiesto).
Parametri (query string)
| Nome | Tipo | Obbl. | Descrizione |
|---|---|---|---|
courtId | uuid | sì | Campo di cui leggere la disponibilità. Deve essere tuo. |
date | AAAA-MM-GG | sì | Giorno locale italiano. Formato diverso: 400 con messaggio "Data non valida (AAAA-MM-GG)". |
Esempio di richiesta
curl -s -G https://www.volleyfriends.com/api/manager-api/v1/availability -H "X-Api-Key: $VF_API_KEY" --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05" --data-urlencode "date=2026-08-12"
Esempio di risposta — 200
{
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"date": "2026-08-12",
"timezone": "Europe/Rome",
"hours": {
"weekday": 2,
"openMinute": 540,
"closeMinute": 1380,
"closed": false,
"configured": true
},
"closure": null,
"bookings": [
{
"startsAt": "2026-08-12T16:00:00.000Z",
"endsAt": "2026-08-12T17:30:00.000Z",
"status": "confirmed"
},
{
"startsAt": "2026-08-12T18:00:00.000Z",
"endsAt": "2026-08-12T19:00:00.000Z",
"status": "paid"
},
{
"startsAt": "2026-08-12T19:00:00.000Z",
"endsAt": "2026-08-12T20:30:00.000Z",
"status": "pending"
}
]
}
Campi della risposta
| Campo | Tipo | Descrizione |
|---|---|---|
courtId | uuid | Campo richiesto. |
venueId | uuid | Struttura del campo. |
date | AAAA-MM-GG | Giorno richiesto, ripetuto. |
timezone | string | Sempre "Europe/Rome": il fuso in cui vanno interpretati date e i minuti degli orari. |
hours.weekday | integer 0–6 | Giorno della settimana: 0 = lunedì … 6 = domenica. |
hours.openMinute | integer | null | Apertura in minuti dalla mezzanotte locale (540 = 09:00). null se il giorno è dichiarato chiuso. |
hours.closeMinute | integer | null | Chiusura in minuti (1380 = 23:00). null se il giorno è dichiarato chiuso. |
hours.closed | boolean | true se il gestore ha dichiarato quel giorno della settimana come chiuso. |
hours.configured | boolean | false quando il gestore non ha impostato gli orari per quel giorno: in quel caso openMinute/closeMinute valgono la convenzione predefinita 480 (08:00) – 1380 (23:00), che è un ripiego di visualizzazione, non un orario dichiarato. |
closure | object | null | Chiusura straordinaria attiva nel giorno richiesto, oppure null. |
closure.from | AAAA-MM-GG | Primo giorno di chiusura. |
closure.to | AAAA-MM-GG | Ultimo giorno di chiusura, incluso. |
closure.reason | string | null | Motivo, se indicato dal gestore. |
bookings[] | array | Slot occupati, ordinati per inizio. Solo startsAt, endsAt e status (pending, confirmed o paid): nessun dato del cliente, così la risposta si può usare per costruire un calendario pubblico. |
Quando closure non è null il giorno è da considerare non prenotabile, a prescindere da hours: una POST in quella data riceverebbe 409 venue_closed.
Errori specifici
| Codice | error | Quando |
|---|---|---|
| 400 | invalid_input | courtId mancante o non UUID, date mancante o non nel formato AAAA-MM-GG. |
| 404 | not_found | "Campo non trovato" — il campo non esiste o non è tuo. |
6.6 Listino di un campo
/api/manager-api/v1/ratesIl listino del campo in sola lettura: serve a mostrare i prezzi al cliente prima della prenotazione, con le stesse fasce che il gestionale usa per calcolare il prezzo automatico. Le fasce si modificano solo dal pannello. Sono ordinate per fromMinute crescente.
Parametri (query string)
| Nome | Tipo | Obbl. | Descrizione |
|---|---|---|---|
courtId | uuid | sì | Campo di cui leggere il listino. Deve essere tuo. |
Esempio di richiesta
curl -s -G https://www.volleyfriends.com/api/manager-api/v1/rates -H "X-Api-Key: $VF_API_KEY" --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05"
Esempio di risposta — 200
{
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"rates": [
{
"id": "4a1e0b72-6d35-4c81-93af-0b7e2d5814c6",
"label": "Mattina infrasettimanale",
"daysMask": 31,
"fromMinute": 480,
"toMinute": 840,
"pricePerHourCents": 1500,
"pricingMode": "court",
"minPlayers": null
},
{
"id": "c07d9b31-8f42-4a05-b6e9-15d3c8017a2b",
"label": "Serale",
"daysMask": 127,
"fromMinute": 1080,
"toMinute": 1380,
"pricePerHourCents": 600,
"pricingMode": "person",
"minPlayers": 10
}
]
}
Campi della risposta
| Campo | Tipo | Descrizione |
|---|---|---|
courtId | uuid | Campo richiesto. |
rates[].id | uuid | Identificativo della fascia. |
rates[].label | string | Etichetta scelta dal gestore (es. «Serale»). |
rates[].daysMask | integer 1–127 | Bitmask dei giorni: lun 1, mar 2, mer 4, gio 8, ven 16, sab 32, dom 64. 127 = tutti i giorni, 31 = da lunedì a venerdì. |
rates[].fromMinute | integer 0–1440 | Inizio della fascia in minuti dalla mezzanotte locale. |
rates[].toMinute | integer 0–1440 | Fine della fascia, sempre maggiore di fromMinute. |
rates[].pricePerHourCents | integer > 0 | Prezzo orario in centesimi (2000 = 20,00 €). Con pricingMode: "person" è il prezzo orario a persona (600 = 6,00 €/persona all'ora). |
rates[].pricingMode | court | person | court = prezzo del campo (default). person = prezzo a persona: il totale scala col numero di persone, mai al ribasso. |
rates[].minPlayers | integer | null | Minimo garantito della fascia a persona: sotto quel numero si paga comunque per il minimo. null = nessun minimo. |
Si guarda l'istante di inizio: fra le fasce del campo si prende la prima (in ordine di fromMinute) il cui giorno è attivo nella bitmask e che contiene il minuto di inizio (fromMinute ≤ inizio < toMinute). Il prezzo è proporzionale ai minuti effettivi: arrotonda(pricePerHourCents × minuti / 60) — 90 minuti a 20,00 €/ora fanno 30,00 €. Una prenotazione a cavallo di due fasce resta valorizzata con la tariffa della fascia di inizio. Nessuna fascia corrispondente: priceCents risulta null (prezzo da definire), mai 0.
Con una fascia a persona (pricingMode: "person") il risultato orario si moltiplica per le persone fatturate: max(playersCount, minPlayers). Esempio con la fascia serale qui sopra (6,00 €/persona all'ora, minimo 10): 60 minuti in 12 persone → 72,00 €; in 16 → 96,00 € (il prezzo a persona non cala); in 8 → 60,00 € (scatta il minimo garantito). Senza playersCount il prezzo non è calcolabile: null in lettura, 400 players_required nella POST che si affida al listino.
Errori specifici
| Codice | error | Quando |
|---|---|---|
| 400 | invalid_input | courtId mancante o non UUID. |
| 404 | not_found | "Campo non trovato" — il campo non esiste o non è tuo. |
Un campo senza listino risponde 200 con "rates": [].
7. Convenzioni dei dati
7.1 Date e orari
- In ingresso gli istanti sono stringhe ISO 8601 con offset obbligatorio:
2026-08-12T18:00:00+02:00oppure2026-08-12T18:00:00Z. Una stringa senza offset (2026-08-12T18:00:00) viene rifiutata con400: sarebbe ambigua, e per un calendario è una ambiguità che si paga con prenotazioni sbagliate di un'ora. - In uscita gli istanti sono sempre normalizzati in UTC con suffisso
Ze millisecondi:"2026-08-12T16:00:00.000Z". Convertili nel fuso locale prima di mostrarli. - Le date secche (
date,closure.from,closure.to, efrom/toquando li usi in forma di giorno) sonoAAAA-MM-GGe si riferiscono al giorno locale italiano. - Gli intervalli sono semiaperti
[inizio, fine): 18:00–19:00 e 19:00–20:00 non sono in conflitto.
7.2 Fuso orario
Le prenotazioni sono conservate come istanti assoluti, ma tutte le regole di calendario — giorno della settimana, fasce del listino, orari di apertura, date di chiusura, confini del giorno — sono valutate nel fuso Europe/Rome, con l'ora legale gestita automaticamente. Il campo timezone della risposta di /availability lo dichiara esplicitamente. Se il tuo server gira in UTC o in un altro fuso, non convertire a mano i minuti: sono già espressi in ora locale italiana.
7.3 Minuti dalla mezzanotte
Gli orari «da calendario» (openMinute, closeMinute, fromMinute, toMinute) sono interi da 0 a 1440 che contano i minuti dalla mezzanotte locale. Conversioni: ore = ⌊m / 60⌋, minuti = m mod 60.
| Minuti | Ora locale | Minuti | Ora locale |
|---|---|---|---|
| 0 | 00:00 | 1080 | 18:00 |
| 480 | 08:00 | 1320 | 22:00 |
| 540 | 09:00 | 1380 | 23:00 |
| 840 | 14:00 | 1440 | 24:00 (fine giornata) |
7.4 Importi
Tutti gli importi sono interi in centesimi di euro (priceCents, pricePerHourCents): nessun valore in virgola mobile, nessun simbolo di valuta. 2000 = 20,00 €. Su una prenotazione, priceCents: null significa «prezzo da definire» ed è diverso da 0, che significa «gratuito».
7.5 Stati di una prenotazione
| Stato | Significato | Occupa lo slot? |
|---|---|---|
pending | Richiesta di prenotazione online in attesa della decisione del gestore. Nasce solo dalle richieste inviate dai giocatori dall'app VolleyFriends (source: "app"): il gestore la accetta (confirmed) o la rifiuta (cancelled). | Sì — lo slot è già bloccato, per evitare doppie richieste sullo stesso orario |
confirmed | Prenotazione valida, non ancora incassata. È lo stato iniziale di ogni prenotazione creata da queste API e dal pannello. | Sì |
paid | Prenotazione valida e incassata. Di norma accompagnata da paymentMethod. | Sì |
cancelled | Prenotazione annullata (o richiesta rifiutata). Resta nello storico ma libera il campo: non genera overlap e non compare in /availability. | No |
- Le trovi in GET /bookings e negli slot occupati di /availability: tratta lo slot come non disponibile, esattamente come una
confirmed. - Puoi rispondere alla richiesta dal tuo gestionale con una PATCH:
{"status": "confirmed"}per accettarla,{"status": "cancelled"}per rifiutarla. Il giocatore riceve una notifica con l'esito. - Riconoscile dalla coppia
status: "pending"+source: "app". I dati del cliente arrivano dal profilo VolleyFriends del giocatore (nome ed email), quindicustomerPhonepuò esserenull. - Non creare prenotazioni
pendingda queste API: il valore è accettato dalla validazione, ma quello stato descrive una richiesta in attesa di risposta da parte del gestore e nessuno la deciderebbe dal tuo lato. Se nel tuo flusso c'è un carrello o un pagamento da completare, tieni l'attesa nel tuo sistema e chiama la POST quando la prenotazione è definitiva — oppure crealaconfirmede portala acancelledse il pagamento non arriva.
7.6 Origine e metodo di pagamento
| Campo | Valori | Significato |
|---|---|---|
source | manual | Inserita dal gestore nel pannello. |
api | Creata da un gestionale esterno con queste API. Valore sempre impostato dalla POST, non modificabile. | |
app | Richiesta di prenotazione online inviata da un giocatore dall'app VolleyFriends: nasce pending e attende la tua decisione. | |
tournament | Generata dall'organizzazione di un torneo sui campi della struttura. | |
paymentMethod | cash, bank_transfer, paypal, stripe, other | Come è stato incassato. null = non incassato. |
Entrambi sono domini testuali: nuovi valori possono essere aggiunti in futuro senza cambiare versione, quindi il tuo codice non deve andare in errore su un valore che non conosce.
7.7 Dati personali dei clienti
Nome, telefono ed email che invii nelle prenotazioni sono dati personali dei tuoi clienti: sei tu il titolare del trattamento, VolleyFriends li conserva per tuo conto come responsabile ai sensi dell'art. 28 GDPR. Manda solo ciò che serve alla gestione del campo e informa i tuoi clienti. Il dettaglio è nella privacy policy.
8. Errori generali
Ogni errore ha lo stesso corpo: un codice stabile in error, pensato per il tuo codice, e un message in italiano, pensato per le persone. Fai le tue decisioni su error e sullo stato HTTP, non sul testo del messaggio, che può cambiare.
{
"error": "overlap",
"message": "Il campo è già prenotato in quell'orario."
}
Gli errori di validazione aggiungono issues, l'elenco dettagliato dei problemi campo per campo:
{
"error": "invalid_input",
"message": "Dati non validi",
"issues": [
{
"code": "invalid_string",
"validation": "datetime",
"path": ["startsAt"],
"message": "Invalid datetime"
}
]
}
| HTTP | error | Significato | Cosa fare |
|---|---|---|---|
| 400 | invalid_input | Parametri o corpo non validi. Il dettaglio è in issues. | Correggi la richiesta. Non ritentare identica: l'esito non cambia. |
| 401 | invalid_key | Header X-Api-Key assente, malformato, sconosciuto o revocato. | Verifica di inviare l'header e che la chiave sia attiva nel pannello. Se è stata revocata, generane una nuova. |
| 403 | subscription_required | Nessun abbonamento «Gestore campi» in corso sull'account proprietario della chiave. | Riattiva l'abbonamento dall'app (Profilo → area gestore → Abbonamento). Nessuna chiamata funziona fino a quel momento. |
| 403 | feature_not_included | Abbonamento attivo, ma il piano non include «API per gestionali esterni». Il messaggio nomina il piano in corso. | Passa a un piano che comprende la funzione. Vale per tutta la v1, letture incluse. |
| 404 | not_found | Risorsa inesistente oppure appartenente a un altro gestore: le due situazioni non si distinguono, per non rivelare dati altrui. | Rileggi gli id da /venues e da /bookings: quasi sempre è un id vecchio o copiato da un altro account. |
| 409 | overlap | Il campo è già occupato nell'intervallo richiesto da una prenotazione non annullata. | Proponi un altro slot: rileggi /availability. Non ritentare senza cambiare orario. |
| 409 | venue_closed | Chiusura straordinaria della struttura nella data richiesta. | Proponi un'altra data. Le chiusure si leggono da /availability (closure). |
| 429 | rate_limited | Superate le 120 richieste al minuto per la chiave. | Attendi e ritenta con backoff esponenziale, rispettando retry-after. |
| 4xx | error | Codice generico per gli errori di richiesta che non ricadono nei casi sopra (raro). | Leggi message e correggi la richiesta. |
| 500 | internal | Errore imprevisto sul nostro lato. | Ritenta più tardi. Se persiste, scrivici indicando ora, endpoint e prefisso della chiave (non la chiave). |
Un path inesistente sotto la base risponde 404; lo stesso vale per un metodo non previsto su un path esistente:
{
"error": "not_found",
"message": "Risorsa non trovata"
}
Le risposte 502, 503 e 504 arrivano dal reverse proxy quando l'API non è raggiungibile (riavvio o manutenzione): in quei casi il corpo non è garantito in formato JSON. Trattale come errori temporanei e ritenta con backoff, senza tentare di leggerne il contenuto.
9. Versioni e compatibilità
La versione è nel path: /manager-api/v1. Non esistono header di versione né date di release da specificare: l'unica versione attualmente pubblicata è v1.
Che cosa possiamo cambiare senza avvisarti
- Aggiungere campi alle risposte esistenti.
- Aggiungere parametri opzionali alle richieste, con un valore predefinito che conserva il comportamento attuale.
- Aggiungere nuovi endpoint sotto
/v1. - Aggiungere valori ai campi testuali aperti (
source,paymentMethod,surface). - Riformulare i
messagedegli errori, mantenendo invariati i codicierror.
Che cosa non cambia dentro la v1
- Nessun campo verrà rimosso o rinominato dalle risposte.
- Nessun campo cambierà tipo o significato.
- Nessun parametro oggi opzionale diventerà obbligatorio.
- I codici
errore gli stati HTTP documentati qui restano quelli.
Qualunque modifica che rompa queste garanzie arriverà su un nuovo prefisso (/v2), con la v1 mantenuta in servizio e un preavviso ai gestori che la stanno usando.
- Ignora i campi che non conosci invece di andare in errore su di essi (nessun parsing «strict» che rifiuta le proprietà inattese).
- Gestisci i valori sconosciuti dei campi testuali aperti con un ramo di ripiego.
- Non fare affidamento sull'ordine delle chiavi JSON né sul testo dei messaggi.
- Tratta
nulle campo assente come equivalenti in lettura.
Changelog
| Versione | Data | Note |
|---|---|---|
| v1 | lug 2026 | Aggiunte compatibili: tariffe a persona nel listino (pricingMode, minPlayers) e playersCount sulle prenotazioni (con l'errore players_required); paginazione limit/offset sull'elenco prenotazioni; ricontrollo dei conflitti alla riattivazione di una prenotazione annullata; recurrence ora rifiutato esplicitamente con 400 recurrence_unsupported (prima era ignorato in silenzio). |
| v1 | 2026 | Prima versione pubblica: strutture e campi, prenotazioni (elenco, creazione, modifica), disponibilità giornaliera, listino. Autenticazione con X-Api-Key, 120 richieste/minuto per chiave. |
10. Supporto all'integrazione
Per domande tecniche, comportamenti inattesi o richieste di endpoint che qui non trovi, scrivici dalla pagina Contatti. Per arrivare subito al punto, indica nel messaggio:
- endpoint e metodo che stai chiamando;
- data e ora della chiamata (con il fuso) e il codice HTTP ricevuto;
- codice
errore messaggio della risposta; - prefisso della chiave usata (es.
vfk_9f2c).
Non inviarci la chiave API completa in nessun canale, nemmeno per farci verificare un problema: il prefisso ci basta per identificarla. Se l'hai già condivisa con qualcuno, revocala e generane una nuova.
Se hai bisogno di provare l'integrazione senza toccare il calendario reale, crea una struttura di prova con un campo dedicato e usa quella: tutte le chiamate sono limitate alle strutture del proprietario della chiave, quindi il resto del tuo account non è coinvolto.