API per gestionali esterni

Versione v1 — documentazione aggiornata al 29 luglio 2026

Questa pagina documenta le API HTTP che VolleyFriends mette a disposizione dei gestori di strutture per tenere il calendario dei campi in sincrono con un software gestionale esterno o con un sito con prenotazione online. Sono le stesse regole di dominio del pannello gestore (conflitti, chiusure, listino): cambia solo il modo di autenticarsi.

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.
Ambito delle chiavi

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

  1. Un account gestore campi VolleyFriends con almeno una struttura e un campo.
  2. Un abbonamento in corso con un piano che include la funzione «API per gestionali esterni». L'intera v1anche le sole letture — è riservata a chi ha questa funzione: è una funzione venduta, non un accessorio del pannello.
  3. Una chiave API generata dal gestionale: admin.volleyfriends.comImpostazioniChiavi APINuova 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)."
}
Custodia della chiave
  • 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."
}
Come stare dentro il limite
  • Per l'allineamento periodico usa una sola chiamata a /bookings con un intervallo fromto ampio, invece di una chiamata al giorno.
  • Metti in cache l'elenco di strutture e campi: cambia raramente.
  • In caso di 429 attendi e ritenta con backoff esponenziale (per esempio 2, 4, 8 secondi), rispettando retry-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

GET/api/manager-api/v1/venues

Elenca 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

CampoTipoDescrizione
venues[].iduuidIdentificativo della struttura (venueId negli altri endpoint).
venues[].namestringNome della struttura.
venues[].citystring | nullCittà.
venues[].addressstring | nullIndirizzo.
venues[].verifiedbooleantrue se la struttura ha il badge VERIFICATO.
venues[].courts[]arrayCampi della struttura. Array vuoto se non ne ha.
courts[].iduuidIdentificativo del campo (courtId negli altri endpoint).
courts[].venueIduuidStruttura a cui appartiene il campo.
courts[].namestringNome del campo.
courts[].surfacestring | nullSuperficie, testo libero inserito dal gestore.
courts[].indoorbooleanCampo al coperto.
courts[].lightingbooleanIlluminazione disponibile.

Se l'account non ha ancora strutture la risposta è {"venues": []}.

Errori specifici

Nessuno oltre a quelli generali.

6.2 Elenco prenotazioni

GET/api/manager-api/v1/bookings

Restituisce 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)

NomeTipoObbl.Descrizione
venueIduuidnoLimita a una struttura. Se non è tua: 404 not_found.
courtIduuidnoLimita a un campo. Se non è tuo: 404 not_found. Combinato con un venueId di un'altra tua struttura il risultato è un elenco vuoto.
fromdata o istante ISOnoEstremo inferiore incluso. Una data secca AAAA-MM-GG vale la mezzanotte locale italiana di quel giorno.
todata o istante ISOnoEstremo superiore escluso. Una data secca vale la mezzanotte locale del giorno successivo: to=2026-08-31 include tutto il 31 agosto.
limitinteger 1–1000noNumero massimo di righe (default 500).
offsetinteger ≥ 0noRighe da saltare (default 0): offset=500 è la seconda pagina con il limit di default.
Su cosa agisce il filtro

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).

CampoTipoDescrizione
iduuidIdentificativo della prenotazione.
venueIduuidStruttura.
courtIduuidCampo prenotato.
courtNamestringNome del campo, per comodità di visualizzazione.
startsAtistante ISOInizio, sempre in UTC (suffisso Z).
endsAtistante ISOFine, estremo escluso.
customerNamestringNome del cliente.
customerPhonestring | nullTelefono.
customerEmailstring | nullEmail.
priceCentsinteger | nullPrezzo concordato in centesimi. null = da definire (mai 0 per «non definito»).
playersCountinteger | nullNumero di persone dichiarato (solo prenotazioni su fasce con prezzo a persona). null = non applicabile o non indicato.
statusstringpending · confirmed · paid · cancelled — vedi stati.
paymentMethodstring | nullcash · bank_transfer · paypal · stripe · other. null = non incassato.
paymentLinkstring | nullLink di pagamento con carta generato dal pannello, se presente. In sola lettura.
notesstring | nullNote libere.
sourcestringOrigine: manual (pannello) · api (queste API) · app (richiesta di un giocatore dall'app) · tournament (torneo).
recurrenceIduuid | nullPresente e uguale su tutte le occorrenze di una serie ricorrente creata dal pannello.
createdAtistante ISOCreazione.
updatedAtistante ISOUltima modifica.

Errori specifici

CodiceerrorQuando
400invalid_inputvenueId/courtId non sono UUID, oppure from/to non sono date interpretabili.
404not_foundLa struttura ("Struttura non trovata") o il campo ("Campo non trovato") non esiste o non è tuo.

6.3 Nuova prenotazione

POST/api/manager-api/v1/bookings

Crea 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:

  1. chiusura straordinaria della struttura nella data di inizio → 409 venue_closed;
  2. sovrapposizione con un'altra prenotazione non annullata dello stesso campo → 409 overlap;
  3. prezzo assente → calcolato dal listino del campo (null se l'orario di inizio non ricade in nessuna fascia). Se la fascia ha il prezzo a persona, serve anche playersCount: senza, la richiesta è rifiutata con 400 players_required (a meno di indicare un priceCents esplicito).

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)

NomeTipoObbl.Descrizione
courtIduuidCampo da prenotare. Deve appartenere a una tua struttura.
startsAtistante ISO 8601 con offsetInizio. Sono accettate sia la forma 2026-08-12T18:00:00Z sia 2026-08-12T18:00:00+02:00. Senza offset: 400.
endsAtistante ISO 8601 con offsetFine, deve essere successiva a startsAt.
customerNamestring (1–160)Nome del cliente.
customerPhonestring (max 40)noTelefono.
customerEmailemail (max 200)noEmail, validata come indirizzo.
priceCentsinteger ≥ 0noPrezzo in centesimi. Se omesso viene calcolato dal listino del campo. Indica 0 solo per una prenotazione davvero gratuita.
playersCountinteger 1–500noQuante 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.
notesstring (max 2000)noNote libere, visibili al gestore nel pannello.
statuspending | confirmed | paid | cancellednoDefault confirmed. Da un gestionale esterno usa confirmed o paid: pending è riservato alle richieste che arrivano dall'app dei giocatori (vedi stati).
paymentMethodcash | bank_transfer | paypal | stripe | othernoCome è stato (o sarà) incassato. Omesso = non incassato.
recurrenceobjectnoNon 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

CodiceerrorQuando
400invalid_inputCorpo non valido: campo obbligatorio mancante, istante senza offset, endsAt non successiva a startsAt, email non valida, lunghezze fuori limite.
400players_requiredPrezzo da calcolare dal listino su una fascia a persona, ma playersCount assente.
400recurrence_unsupportedIl corpo contiene recurrence: le ricorrenze non sono disponibili da queste API.
403subscription_requiredNessun abbonamento gestore in corso.
404not_found"Campo non trovato" — il courtId non esiste o non è tuo.
409venue_closedLa struttura ha una chiusura straordinaria nella data di inizio.
409overlapIl 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)." }
Idempotenza

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

PATCH/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

NomeTipoObbl.Descrizione
iduuidId 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.

NomeTipoNote
courtIduuidSposta la prenotazione su un altro tuo campo, anche di un'altra tua struttura (venueId viene aggiornato di conseguenza).
startsAtistante ISO con offsetNuovo inizio.
endsAtistante ISO con offsetNuova fine. Il risultato finale deve restare successivo all'inizio, anche combinando un solo estremo con il valore già salvato.
customerNamestring (1–160)
customerPhonestring (max 40)Annullabile.
customerEmailemail (max 200)Annullabile.
priceCentsinteger ≥ 0Annullabile (null = prezzo da definire). In PATCH il prezzo non viene ricalcolato dal listino: se sposti l'orario e vuoi il nuovo prezzo, indicalo.
playersCountinteger 1–500Annullabile. Aggiorna solo il dato: il prezzo non viene ricalcolato d'ufficio — se il numero di persone cambia il totale, manda anche il nuovo priceCents.
notesstring (max 2000)Annullabile.
statuspending | confirmed | paid | cancelledQualsiasi 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.
paymentMethodcash | bank_transfer | paypal | stripe | otherAnnullabile.
Quando vengono ricontrollati i conflitti

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

CodiceerrorQuando
400invalid_inputCorpo 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").
403subscription_requiredNessun abbonamento gestore in corso.
404not_found"Prenotazione non trovata" oppure "Campo non trovato" se il nuovo courtId non è tuo.
409venue_closedSpostamento in una data di chiusura straordinaria.
409overlapIl nuovo orario si sovrappone a un'altra prenotazione non annullata dello stesso campo.

6.5 Disponibilità di un campo in un giorno

GET/api/manager-api/v1/availability

Restituisce, 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)

NomeTipoObbl.Descrizione
courtIduuidCampo di cui leggere la disponibilità. Deve essere tuo.
dateAAAA-MM-GGGiorno 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

CampoTipoDescrizione
courtIduuidCampo richiesto.
venueIduuidStruttura del campo.
dateAAAA-MM-GGGiorno richiesto, ripetuto.
timezonestringSempre "Europe/Rome": il fuso in cui vanno interpretati date e i minuti degli orari.
hours.weekdayinteger 0–6Giorno della settimana: 0 = lunedì … 6 = domenica.
hours.openMinuteinteger | nullApertura in minuti dalla mezzanotte locale (540 = 09:00). null se il giorno è dichiarato chiuso.
hours.closeMinuteinteger | nullChiusura in minuti (1380 = 23:00). null se il giorno è dichiarato chiuso.
hours.closedbooleantrue se il gestore ha dichiarato quel giorno della settimana come chiuso.
hours.configuredbooleanfalse 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.
closureobject | nullChiusura straordinaria attiva nel giorno richiesto, oppure null.
closure.fromAAAA-MM-GGPrimo giorno di chiusura.
closure.toAAAA-MM-GGUltimo giorno di chiusura, incluso.
closure.reasonstring | nullMotivo, se indicato dal gestore.
bookings[]arraySlot 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.
Con una chiusura in corso

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

CodiceerrorQuando
400invalid_inputcourtId mancante o non UUID, date mancante o non nel formato AAAA-MM-GG.
404not_found"Campo non trovato" — il campo non esiste o non è tuo.

6.6 Listino di un campo

GET/api/manager-api/v1/rates

Il 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)

NomeTipoObbl.Descrizione
courtIduuidCampo 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

CampoTipoDescrizione
courtIduuidCampo richiesto.
rates[].iduuidIdentificativo della fascia.
rates[].labelstringEtichetta scelta dal gestore (es. «Serale»).
rates[].daysMaskinteger 1–127Bitmask 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[].fromMinuteinteger 0–1440Inizio della fascia in minuti dalla mezzanotte locale.
rates[].toMinuteinteger 0–1440Fine della fascia, sempre maggiore di fromMinute.
rates[].pricePerHourCentsinteger > 0Prezzo orario in centesimi (2000 = 20,00 €). Con pricingMode: "person" è il prezzo orario a persona (600 = 6,00 €/persona all'ora).
rates[].pricingModecourt | personcourt = prezzo del campo (default). person = prezzo a persona: il totale scala col numero di persone, mai al ribasso.
rates[].minPlayersinteger | nullMinimo garantito della fascia a persona: sotto quel numero si paga comunque per il minimo. null = nessun minimo.
Come viene calcolato il prezzo di una prenotazione

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

CodiceerrorQuando
400invalid_inputcourtId mancante o non UUID.
404not_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:00 oppure 2026-08-12T18:00:00Z. Una stringa senza offset (2026-08-12T18:00:00) viene rifiutata con 400: 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 Z e millisecondi: "2026-08-12T16:00:00.000Z". Convertili nel fuso locale prima di mostrarli.
  • Le date secche (date, closure.from, closure.to, e from/to quando li usi in forma di giorno) sono AAAA-MM-GG e 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.

MinutiOra localeMinutiOra locale
000:00108018:00
48008:00132022:00
54009:00138023:00
84014:00144024: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

StatoSignificatoOccupa lo slot?
pendingRichiesta 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). — lo slot è già bloccato, per evitare doppie richieste sullo stesso orario
confirmedPrenotazione valida, non ancora incassata. È lo stato iniziale di ogni prenotazione creata da queste API e dal pannello.
paidPrenotazione valida e incassata. Di norma accompagnata da paymentMethod.
cancelledPrenotazione annullata (o richiesta rifiutata). Resta nello storico ma libera il campo: non genera overlap e non compare in /availability.No
Come trattare le prenotazioni «pending»
  • 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), quindi customerPhone può essere null.
  • Non creare prenotazioni pending da 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 creala confirmed e portala a cancelled se il pagamento non arriva.

7.6 Origine e metodo di pagamento

CampoValoriSignificato
sourcemanualInserita dal gestore nel pannello.
apiCreata da un gestionale esterno con queste API. Valore sempre impostato dalla POST, non modificabile.
appRichiesta di prenotazione online inviata da un giocatore dall'app VolleyFriends: nasce pending e attende la tua decisione.
tournamentGenerata dall'organizzazione di un torneo sui campi della struttura.
paymentMethodcash, bank_transfer, paypal, stripe, otherCome è 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"
    }
  ]
}
HTTPerrorSignificatoCosa fare
400invalid_inputParametri o corpo non validi. Il dettaglio è in issues.Correggi la richiesta. Non ritentare identica: l'esito non cambia.
401invalid_keyHeader 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.
403subscription_requiredNessun 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.
403feature_not_includedAbbonamento 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.
404not_foundRisorsa 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.
409overlapIl campo è già occupato nell'intervallo richiesto da una prenotazione non annullata.Proponi un altro slot: rileggi /availability. Non ritentare senza cambiare orario.
409venue_closedChiusura straordinaria della struttura nella data richiesta.Proponi un'altra data. Le chiusure si leggono da /availability (closure).
429rate_limitedSuperate le 120 richieste al minuto per la chiave.Attendi e ritenta con backoff esponenziale, rispettando retry-after.
4xxerrorCodice generico per gli errori di richiesta che non ricadono nei casi sopra (raro).Leggi message e correggi la richiesta.
500internalErrore 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 message degli errori, mantenendo invariati i codici error.

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 error e 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.

Come scrivere un client che non si rompe
  • 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 null e campo assente come equivalenti in lettura.

Changelog

VersioneDataNote
v1lug 2026Aggiunte 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).
v12026Prima 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 error e messaggio della risposta;
  • prefisso della chiave usata (es. vfk_9f2c).
Mai nel messaggio

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.