API pour logiciels de gestion externes

Version v1 — documentation mise à jour le 29 juillet 2026

Cette page documente les API HTTP que VolleyFriends met à la disposition des gérants de structures pour synchroniser le calendrier des terrains avec un logiciel de gestion externe ou un site de réservation en ligne. Ce sont les mêmes règles métier que le panneau de gestion (conflits, fermetures, tarifs) : seule la méthode d'authentification change.

1. Ce que permettent les API

Avec une clé API, tu peux, depuis ton logiciel, lire et écrire sur le calendrier de tes infrastructures :

  • lire les infrastructures et les terrains avec leurs identifiants, nécessaires pour tous les autres appels ;
  • lire les réservations d'une période, d'une infrastructure ou d'un seul terrain, pour les aligner avec tes archives ;
  • créer des réservations depuis l'extérieur (par exemple lorsqu'un client réserve depuis ton site), avec les mêmes contrôles de chevauchement et de fermeture du panneau ;
  • modifier ou annuler une réservation existante (changement d'horaire ou de terrain, encaissement, notes, annulation) ;
  • lire la disponibilité d'un terrain pour un jour donné : créneaux déjà occupés, horaires d'ouverture et fermetures exceptionnelles, afin de proposer les créneaux libres sans avoir à répliquer les règles ;
  • lire la grille tarifaire d'un terrain pour afficher les prix au client final.
Portée des clés

Chaque clé est liée au compte gestionnaire qui l'a créée et ne voit que les structures, les terrains et les réservations de ce compte. Une ressource appartenant à un autre gestionnaire ne renvoie pas 403 mais 404 not_found : son existence n'est pas révélée.

Ne font pas partie de ces API les demandes des organisateurs pour l'utilisation des terrains lors des tournois (elles sont approuvées depuis l'application), la création de structures et de terrains, l'enregistrement des horaires, des fermetures et de la grille tarifaire : ce sont des opérations du panneau de gestion.

2. Prérequis et activation

  1. Un compte gestionnaire de terrains VolleyFriends avec au moins une structure et un terrain.
  2. Un abonnement en cours avec une offre incluant la fonctionnalité « API pour logiciels de gestion externes ». L'intégralité de la v1même les simples lectures — est réservée à ceux qui disposent de cette fonctionnalité : il s'agit d'une option payante, pas d'un accessoire du panneau.
  3. Une clé API générée depuis le panneau de gestion : admin.volleyfriends.comParamètresClés APINouvelle clé.

La clé a la forme vfk_ suivie de 40 caractères hexadécimaux (44 caractères en tout) et n'est affichée qu'une seule fois, immédiatement après sa création : seule son empreinte sha256 reste dans la base de données, elle n'est donc récupérable d'aucune façon. Si tu la perds, révoque-la et génères-en une nouvelle. Dans la liste du panneau, tu ne vois que le préfixe (ex. vfk_9f2c), la date de création et celle de la dernière utilisation.

3. Authentification

Chaque requête doit être authentifiée avec l'en-tête X-Api-Key. Pas besoin de jeton, de connexion, d'expiration ou de renouvellement : la clé reste valide tant que tu ne la révoques pas.

# Les clés de cette page sont des exemples : remplace-les par la tienne.
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues   -H "X-Api-Key: vfk_9f2c41a7b83d05e6c19af4728b60d35e1c9a7f42"

Dans les exemples qui suivent, la clé est lue à partir de la variable d'environnement VF_API_KEY, comme il convient de le faire également en production.

En-tête absent, plus court que 12 caractères, inconnu ou appartenant à une clé révoquée : la réponse est 401 avec ce corps.

{
  "error": "invalid_key",
  "message": "Chiave API mancante o non valida (header X-Api-Key)."
}
Conservation de la clé
  • La clé équivaut à un mot de passe avec les pleins pouvoirs sur le calendrier de tes structures : ne l'intègre pas dans des pages, des applications ou du JavaScript côté navigateur, où elle serait visible par n'importe qui. Utilise-la uniquement de serveur à serveur.
  • Ne la mets pas dans le code source ni dans un dépôt : utilise une variable d'environnement ou un gestionnaire de secrets.
  • Utilise une clé pour chaque système connecté (site, logiciel de gestion, script de synchronisation) : ainsi, tu peux en révoquer une sans couper les autres.
  • Si tu soupçonnes qu'elle a été compromise, révoque-la immédiatement depuis le panneau : la révocation est immédiate et n'est jamais bloquée par l'état de l'abonnement. À partir de ce moment, chaque appel avec cette clé reçoit 401 invalid_key.
  • Trafic exclusivement sur HTTPS ; le champ « dernière utilisation » dans le panneau t'aide à repérer des utilisations inattendues.

4. URL de base

https://www.volleyfriends.com/api

Les chemins documentés ci-dessous doivent être ajoutés à la base. Le préfixe /api appartient au proxy inverse, l'adresse complète d'un point de terminaison est donc par exemple :

GET https://www.volleyfriends.com/api/manager-api/v1/availability

Toutes les réponses sont au format application/json avec un encodage UTF-8. Les requêtes avec corps (POST, PATCH) doivent avoir Content-Type: application/json.

5. Limitation de débit

La limite est de 120 requêtes par minute par clé API (fenêtre glissante de 1 minute). Le décompte se fait par clé, non par adresse IP : les logiciels de gestion sont souvent derrière la même adresse de fournisseur et une limite par IP pénaliserait différents gestionnaires hébergés sur la même infrastructure. Si l'en-tête X-Api-Key est totalement absent, le décompte se base sur l'adresse IP.

Chaque réponse indique l'état du compteur dans les en-têtes x-ratelimit-limit, x-ratelimit-remaining et x-ratelimit-reset (secondes avant réinitialisation). En cas de dépassement, retry-after est également ajouté.

Réponse en cas de dépassement — 429

{
  "error": "rate_limited",
  "message": "Limite di 120 richieste al minuto superato per questa chiave API."
}
Comment rester dans la limite
  • Pour l'alignement périodique, utilise un seul appel à /bookings avec un large intervalle fromto, au lieu d'un appel par jour.
  • Mets en cache la liste des structures et des terrains : elle change rarement.
  • En cas de 429, attends et réessaie avec un backoff exponentiel (par exemple 2, 4, 8 secondes), en respectant retry-after.

6. Endpoints

Six endpoints, tous sous /manager-api/v1. Chaque appel passe par la même chaîne de contrôles, dans l'ordre : clé API valide → fonctionnalité « API pour logiciels de gestion externes » incluse dans le forfait → rate limit → validation des paramètres → vérification que la ressource t'appartient.

6.1 Structures et terrains

GET/api/manager-api/v1/venues

Liste tes structures, chacune avec ses propres terrains imbriqués. C'est l'appel de départ : les id des terrains (courtId) sont nécessaires pour tous les autres endpoints. Les structures sont triées par nom, les terrains par nom.

Paramètres

Aucun.

Exemple de requête

curl -s https://www.volleyfriends.com/api/manager-api/v1/venues   -H "X-Api-Key: $VF_API_KEY"

Exemple de réponse — 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
        }
      ]
    }
  ]
}

Champs de la réponse

ChampTypeDescription
venues[].iduuidIdentifiant de la structure (venueId dans les autres endpoints).
venues[].namestringNom de la structure.
venues[].citystring | nullVille.
venues[].addressstring | nullAdresse.
venues[].verifiedbooleantrue si la structure a le badge VÉRIFIÉ.
venues[].courts[]arrayTerrains de la structure. Array vide s'il n'y en a pas.
courts[].iduuidIdentifiant du terrain (courtId dans les autres endpoints).
courts[].venueIduuidStructure à laquelle appartient le terrain.
courts[].namestringNom du terrain.
courts[].surfacestring | nullSurface, texte libre saisi par le gérant.
courts[].indoorbooleanTerrain couvert.
courts[].lightingbooleanÉclairage disponible.

Si le compte n'a pas encore de structures, la réponse est {"venues": []}.

Erreurs spécifiques

Aucune autre que les générales.

6.2 Liste des réservations

GET/api/manager-api/v1/bookings

Retourne les réservations de tes structures, triées par heure de début croissante. La liste est paginée : au maximum limit lignes par appel (par défaut 500, plafond 1000) — si tu reçois exactement limit lignes, effectue un nouvel appel avec un offset augmenté pour la page suivante. En production, filtre toujours au moins par plage de dates.

Paramètres (query string)

NomTypeObl.Description
venueIduuidnonLimite à une structure. Si elle ne t'appartient pas : 404 not_found.
courtIduuidnonLimite à un terrain. S'il n'est pas à toi : 404 not_found. Combiné avec un venueId d'une autre de tes structures, le résultat est une liste vide.
fromdate ou instant ISOnonBorne inférieure incluse. Une date seule AAAA-MM-GG correspond à minuit heure locale italienne de ce jour-là.
todate ou instant ISOnonBorne supérieure exclue. Une date seule correspond à minuit heure locale du jour suivant : to=2026-08-31 inclut tout le 31 août.
limitinteger 1–1000nonNombre maximum de lignes (par défaut 500).
offsetinteger ≥ 0nonLignes à sauter (par défaut 0) : offset=500 correspond à la deuxième page avec le limit par défaut.
Sur quoi agit le filtre

from et to s'appliquent à l'instant de début de la réservation. Une réservation commencée avant from et toujours en cours après n'apparaît pas : si tu en as besoin, élargis la borne inférieure de quelques heures. Le filtre n'exclut pas les réservations annulées : la liste contient également celles avec status: "cancelled".

Exemple de requête

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"

Exemple de réponse — 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
}

Champs d'une réservation

La même structure est retournée par POST et PATCH (dans la clé booking).

ChampTypeDescription
iduuidIdentifiant de la réservation.
venueIduuidStructure.
courtIduuidTerrain réservé.
courtNamestringNom du terrain, pour faciliter l'affichage.
startsAtinstant ISODébut, toujours en UTC (suffixe Z).
endsAtinstant ISOFin, borne exclue.
customerNamestringNom du client.
customerPhonestring | nullTéléphone.
customerEmailstring | nullE-mail.
priceCentsinteger | nullPrix convenu en centimes. null = à définir (jamais 0 pour « non défini »).
playersCountinteger | nullNombre de personnes déclaré (uniquement pour les réservations sur des créneaux avec tarif par personne). null = non applicable ou non indiqué.
statusstringpending · confirmed · paid · cancelled — voir états.
paymentMethodstring | nullcash · bank_transfer · paypal · stripe · other. null = non encaissé.
paymentLinkstring | nullLien de paiement par carte généré par le panneau, si présent. En lecture seule.
notesstring | nullNotes libres.
sourcestringOrigine : manual (panneau) · api (ces API) · app (demande d'un joueur depuis l'app) · tournament (tournoi).
recurrenceIduuid | nullPrésent et identique sur toutes les occurrences d'une série récurrente créée depuis le panneau.
createdAtinstant ISOCréation.
updatedAtinstant ISODernière modification.

Erreurs spécifiques

CodeerrorQuand
400invalid_inputvenueId/courtId ne sont pas des UUID, ou bien from/to ne sont pas des dates interprétables.
404not_foundLa structure ("Struttura non trovata") ou le terrain ("Campo non trovato") n'existe pas ou ne t'appartient pas.

6.3 Nouvelle réservation

POST/api/manager-api/v1/bookings

Crée une réservation sur l'un de tes terrains. La réservation est créée avec source: "api", ce qui permet de la distinguer dans le panneau de celles saisies manuellement, et le gérant reçoit une notification push « Nouvelle réservation » sur l'app.

Les mêmes règles que celles du panneau sont appliquées, dans l'ordre :

  1. fermeture exceptionnelle de la structure à la date de début → 409 venue_closed;
  2. chevauchement avec une autre réservation non annulée du même terrain → 409 overlap;
  3. tarif absent → calculé à partir de la grille tarifaire du terrain (null si l'heure de début ne correspond à aucun créneau). Si le créneau a un tarif par personne, playersCount est également requis : sans cela, la demande est rejetée avec 400 players_required (à moins d'indiquer un priceCents explicite).

Les horaires d'ouverture ne bloquent pas la création : ils sont informatifs, le gérant reste maître de son terrain. Si tu veux empêcher les réservations en dehors des horaires, vérifie toi-même hours renvoyé par /availability avant d'appeler la POST.

Corps de la requête (JSON)

NomTypeObl.Description
courtIduuidouiTerrain à réserver. Il doit appartenir à l'une de tes structures.
startsAtinstant ISO 8601 avec offsetouiDébut. Sont acceptés aussi bien le format 2026-08-12T18:00:00Z que 2026-08-12T18:00:00+02:00. Sans offset : 400.
endsAtinstant ISO 8601 avec offsetouiFin, elle doit être postérieure à startsAt.
customerNamestring (1–160)ouiNom du client.
customerPhonestring (max 40)nonTéléphone.
customerEmailemail (max 200)nonE-mail, validée comme adresse.
priceCentsinteger ≥ 0nonPrix en centimes. Si omis, il est calculé à partir de la grille tarifaire du terrain. Indique 0 uniquement pour une réservation réellement gratuite.
playersCountinteger 1–500nonNombre de personnes. Requis si le prix doit être calculé à partir de la grille tarifaire et que le créneau de l'heure de début est par personne ; le total applique l'éventuel minimum garanti du créneau.
notesstring (max 2000)nonNotes libres, visibles par le gérant dans le panneau.
statuspending | confirmed | paid | cancellednonPar défaut confirmed. Depuis un logiciel de gestion externe, utilise confirmed ou paid : pending est réservé aux demandes provenant de l'application des joueurs (voir statuts).
paymentMethodcash | bank_transfer | paypal | stripe | othernonComment il a été (ou sera) encaissé. Omis = non encaissé.
recurrenceobjectnonNon pris en charge par ces API : si présente, la demande est rejetée avec 400 recurrence_unsupported (aucune réservation créée). Les séries récurrentes restent une fonctionnalité du panneau : depuis un logiciel de gestion externe, répète la POST pour chaque occurrence.

Exemple de requête

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"
  }'

Exemple de réponse — 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"
  }
}

Dans l'exemple, priceCents n'avait pas été indiqué : 90 minutes sur un créneau à 20,00 €/heure donnent 3000 centimes (le calcul est proportionnel aux minutes réelles, avec le tarif du créneau dans lequel commence la réservation).

Erreurs spécifiques

CodeerrorQuand
400invalid_inputCorps non valide : champ obligatoire manquant, instant sans offset, endsAt non postérieure à startsAt, e-mail non valide, longueurs hors limites.
400players_requiredPrix à calculer à partir de la grille tarifaire sur un créneau par personne, mais playersCount absent.
400recurrence_unsupportedLe corps contient recurrence : les récurrences ne sont pas disponibles depuis ces API.
403subscription_requiredAucun abonnement gérant en cours.
404not_found"Campo non trovato" — le courtId n'existe pas ou ne t'appartient pas.
409venue_closedLa structure a une fermeture exceptionnelle à la date de début.
409overlapLe terrain a déjà une réservation non annulée qui chevauche l'intervalle.
// 409 — chevauchement
{ "error": "overlap", "message": "Il campo è già prenotato in quell'orario." }

// 409 — fermeture exceptionnelle (le motif, s'il est indiqué par le gérant, figure dans le message)
{ "error": "venue_closed", "message": "Struttura chiusa in quella data (manutenzione)." }
Idempotence

Il n'existe pas de clé d'idempotence : répéter le même POST après un timeout peut créer un doublon ou, plus souvent, renvoyer 409 overlap car la première tentative avait réussi. En cas de réponse incertaine, avant de réessayer, vérifie avec GET /bookings sur l'intervalle concerné.

6.4 Modification de réservation

PATCH/api/manager-api/v1/bookings/{id}

Met à jour une réservation existante. C'est une mise à jour partielle : envoie uniquement les champs qui changent, les autres restent inchangés. Le corps doit contenir au moins un champ.

Il n'existe pas de point de terminaison d'annulation : pour annuler une réservation, on définit status: "cancelled". L'historique reste dans le panneau et le créneau redevient libre (les réservations annulées n'occupent pas le terrain et ne génèrent pas d'overlap).

Paramètre de chemin

NomTypeObl.Description
iduuidouiID de la réservation, depuis GET /bookings ou depuis la réponse du POST.

Corps de la requête (JSON)

Tous les champs sont facultatifs. Ceux marqués « annulable » acceptent null pour vider la valeur.

NomTypeNotes
courtIduuidDéplace la réservation sur un autre de tes terrains, même d'une autre de tes structures (venueId est mis à jour en conséquence).
startsAtinstant ISO avec décalageNouveau début.
endsAtinstant ISO avec décalageNouvelle fin. Le résultat final doit rester postérieur au début, même en combinant une seule limite avec la valeur déjà enregistrée.
customerNamestring (1–160)
customerPhonestring (max 40)Annulable.
customerEmailemail (max 200)Annulable.
priceCentsinteger ≥ 0Annulable (null = prix à définir). Dans PATCH, le prix n'est pas recalculé à partir de la grille tarifaire : si tu déplaces l'horaire et que tu veux le nouveau prix, indique-le.
playersCountinteger 1–500Annulable. Met à jour uniquement la donnée : le prix n'est pas recalculé d'office — si le nombre de personnes modifie le total, envoie également le nouveau priceCents.
notesstring (max 2000)Annulable.
statuspending | confirmed | paid | cancelledToute transition est autorisée. C'est également le moyen de répondre à une demande provenant de l'application (status: "pending" avec source: "app") : passe-la à confirmed pour l'accepter ou à cancelled pour la refuser — le joueur reçoit la notification.
paymentMethodcash | bank_transfer | paypal | stripe | otherAnnulable.
Quand les conflits sont-ils revérifiés

Les contrôles de fermeture et de chevauchement sont refaits dans deux cas : lorsque la demande déplace le créneau (startsAt, endsAt ou courtId) et lorsqu'elle réactive une réservation annulée (status de cancelled à tout autre état) — lorsqu'elle était annulée, elle n'occupait pas le terrain, et entre-temps le créneau a pu être pris : dans ce cas, la PATCH répond 409 overlap ou 409 venue_closed. Une PATCH qui modifie uniquement d'autres champs (tarif, notes, encaissement) ne les répète pas.

Exemple de requête — déplacement et encaissement

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

Exemple de requête — annulation

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" }'

Exemple de réponse — 200

La réservation mise à jour, dans le même format que la 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"
  }
}

Erreurs spécifiques

CodeerrorQuand
400invalid_inputCorps vide ("Nessun campo da aggiornare"), valeurs non valides, ou heure de fin non postérieure à celle de début ("L'orario di fine deve essere successivo a quello di inizio").
403subscription_requiredAucun abonnement gérant en cours.
404not_found"Prenotazione non trovata" ou "Campo non trovato" si le nouveau courtId n'est pas le tien.
409venue_closedDéplacement vers une date de fermeture exceptionnelle.
409overlapLe nouvel horaire chevauche une autre réservation non annulée du même terrain.

6.5 Disponibilité d'un terrain sur une journée

GET/api/manager-api/v1/availability

Renvoie, pour un terrain et un jour, les créneaux occupés, les horaires d'ouverture de ce jour de la semaine et l'éventuelle fermeture exceptionnelle. C'est l'endpoint à utiliser pour calculer les créneaux libres sur un site externe sans dupliquer les règles du logiciel de gestion : soustrais les réservations de la fenêtre d'ouverture.

Sont listés tous les créneaux qui occupent le terrain, dans n'importe quel état sauf cancelled : donc également les demandes de réservation en ligne encore pending. Le jour est le jour local italien et les intervalles sont semi-ouverts : une réservation qui se termine exactement à minuit appartient au jour précédent. Une réservation à cheval sur minuit apparaît dans les deux jours qu'elle intersecte, avec ses instants réels (qui peuvent tomber en dehors du jour demandé).

Paramètres (query string)

NomTypeObl.Description
courtIduuidouiTerrain dont il faut lire la disponibilité. Il doit être le tien.
dateAAAA-MM-GGouiJour local italien. Format différent : 400 avec le message "Data non valida (AAAA-MM-GG)".

Exemple de requête

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"

Exemple de réponse — 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"
    }
  ]
}

Champs de la réponse

ChampTypeDescription
courtIduuidTerrain demandé.
venueIduuidStructure du terrain.
dateAAAA-MM-GGJour demandé, répété.
timezonestringToujours "Europe/Rome" : le fuseau horaire dans lequel doivent être interprétés date et les minutes des horaires.
hours.weekdayinteger 0–6Jour de la semaine : 0 = lundi … 6 = dimanche.
hours.openMinuteinteger | nullOuverture en minutes à partir de minuit local (540 = 09:00). null si le jour est déclaré fermé.
hours.closeMinuteinteger | nullFermeture en minutes (1380 = 23:00). null si le jour est déclaré fermé.
hours.closedbooleantrue si le gérant a déclaré ce jour de la semaine comme fermé.
hours.configuredbooleanfalse lorsque le gérant n'a pas configuré les horaires pour ce jour : dans ce cas, openMinute/closeMinute prennent la valeur par défaut 480 (08:00) – 1380 (23:00), ce qui est une solution de secours pour l'affichage, pas un horaire déclaré.
closureobject | nullFermeture exceptionnelle active le jour demandé, ou null.
closure.fromAAAA-MM-GGPremier jour de fermeture.
closure.toAAAA-MM-GGDernier jour de fermeture, inclus.
closure.reasonstring | nullMotif, si indiqué par le gérant.
bookings[]arrayCréneaux occupés, triés par début. Uniquement startsAt, endsAt et status (pending, confirmed ou paid) : aucune donnée client, ainsi la réponse peut être utilisée pour construire un calendrier public.
Avec une fermeture en cours

Lorsque closure n'est pas null, le jour est à considérer comme non réservable, indépendamment de hours : un POST à cette date recevrait 409 venue_closed.

Erreurs spécifiques

CodeerrorQuand
400invalid_inputcourtId manquant ou non UUID, date manquante ou non au format AAAA-MM-GG.
404not_found"Campo non trovato" — le terrain n'existe pas ou n'est pas le tien.

6.6 Grille tarifaire d'un terrain

GET/api/manager-api/v1/rates

La grille tarifaire du terrain en lecture seule : elle sert à afficher les prix au client avant la réservation, avec les mêmes tranches que le logiciel de gestion utilise pour calculer le prix automatique. Les tranches se modifient uniquement depuis le panneau. Elles sont triées par fromMinute croissant.

Paramètres (query string)

NomTypeObl.Description
courtIduuidouiTerrain dont lire la grille tarifaire. Il doit être le tien.

Exemple de requête

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"

Exemple de réponse — 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
    }
  ]
}

Champs de la réponse

ChampTypeDescription
courtIduuidTerrain demandé.
rates[].iduuidIdentifiant de la tranche.
rates[].labelstringÉtiquette choisie par le gérant (ex. « Soirée »).
rates[].daysMaskinteger 1–127Bitmask des jours : lun 1, mar 2, mer 4, jeu 8, ven 16, sam 32, dim 64. 127 = tous les jours, 31 = du lundi au vendredi.
rates[].fromMinuteinteger 0–1440Début de la tranche en minutes depuis minuit heure locale.
rates[].toMinuteinteger 0–1440Fin de la tranche, toujours supérieure à fromMinute.
rates[].pricePerHourCentsinteger > 0Prix horaire en centimes (2000 = 20,00 €). Avec pricingMode: "person", c'est le prix horaire par personne (600 = 6,00 €/personne par heure).
rates[].pricingModecourt | personcourt = prix du terrain (par défaut). person = prix par personne : le total s'ajuste selon le nombre de personnes, jamais à la baisse.
rates[].minPlayersinteger | nullMinimum garanti de la tranche par personne : en dessous de ce nombre, on paie quand même pour le minimum. null = aucun minimum.
Comment est calculé le prix d'une réservation

On regarde l'instant de début : parmi les tranches du terrain, on prend la première (par ordre de fromMinute) dont le jour est actif dans la bitmask et qui contient la minute de début (fromMinute ≤ inizio < toMinute). Le prix est proportionnel aux minutes réelles : arrotonda(pricePerHourCents × minuti / 60) — 90 minutes à 20,00 €/heure font 30,00 €. Une réservation à cheval sur deux tranches reste valorisée au tarif de la tranche de début. Aucune tranche correspondante : priceCents vaut null (prix à définir), jamais 0.

Avec une tranche par personne (pricingMode: "person"), le résultat horaire est multiplié par les personnes facturées : max(playersCount, minPlayers). Exemple avec la tranche de soirée ci-dessus (6,00 €/personne par heure, minimum 10) : 60 minutes pour 12 personnes → 72,00 € ; pour 16 → 96,00 € (le prix par personne ne baisse pas) ; pour 8 → 60,00 € (le minimum garanti s'applique). Sans playersCount, le prix n'est pas calculable : null en lecture, 400 players_required dans la POST qui s'appuie sur la grille tarifaire.

Erreurs spécifiques

CodeerrorQuand
400invalid_inputcourtId manquant ou non UUID.
404not_found"Campo non trovato" — le terrain n'existe pas ou n'est pas le tien.

Un terrain sans grille tarifaire répond 200 avec "rates": [].

7. Conventions des données

7.1 Dates et horaires

  • En entrée les instants sont des chaînes ISO 8601 avec offset obligatoire : 2026-08-12T18:00:00+02:00 ou 2026-08-12T18:00:00Z. Une chaîne sans offset (2026-08-12T18:00:00) est rejetée avec 400 : elle serait ambiguë, et pour un calendrier, c'est une ambiguïté qui se paie par des réservations erronées d'une heure.
  • En sortie, les instants sont toujours normalisés en UTC avec le suffixe Z et millisecondes : "2026-08-12T16:00:00.000Z". Convertis-les dans le fuseau horaire local avant de les afficher.
  • Les dates simples (date, closure.from, closure.to et from/to quand tu les utilises sous forme de jour) sont AAAA-MM-GG et se réfèrent au jour local italien.
  • Les intervalles sont semi-ouverts [inizio, fine) : 18:00–19:00 et 19:00–20:00 ne sont pas en conflit.

7.2 Fuseau horaire

Les réservations sont conservées comme des instants absolus, mais toutes les règles de calendrier — jour de la semaine, tranches de tarifs, horaires d'ouverture, dates de fermeture, limites de la journée — sont évaluées dans le fuseau Europe/Rome, avec l'heure d'été gérée automatiquement. Le champ timezone de la réponse de /availability le déclare explicitement. Si ton serveur tourne en UTC ou dans un autre fuseau, ne convertis pas les minutes à la main : elles sont déjà exprimées en heure locale italienne.

7.3 Minutes depuis minuit

Les horaires « de calendrier » (openMinute, closeMinute, fromMinute, toMinute) sont des entiers de 0 à 1440 qui comptent les minutes depuis minuit heure locale. Conversions : ore = ⌊m / 60⌋, minuti = m mod 60.

MinutesMinutesMinutesMinutes
000:00108018:00
48008:00132022:00
54009:00138023:00
84014:00144024:00 (fin de journée)

7.4 Montants

Tous les montants sont des entiers en centimes d'euro (priceCents, pricePerHourCents) : aucune valeur à virgule flottante, aucun symbole de devise. 2000 = 20,00 €. Sur une réservation, priceCents: null signifie « prix à définir » et est différent de 0, qui signifie « gratuit ».

7.5 États d'une réservation

7.5 Statuts d'une réservationStatut23. StatoOccupe le créneau ?
pendingDemande de réservation en ligne en attente de la décision du gérant. Elle provient uniquement des demandes envoyées par les joueurs depuis l'app VolleyFriends (source: "app") : le gérant l'accepte (confirmed) ou la refuse (cancelled).Oui — le créneau est déjà bloqué, pour éviter les doubles demandes sur le même horaire
confirmedRéservation valide, pas encore encaissée. C'est l'état initial de chaque réservation créée par ces API et depuis le panneau.Oui
paidRéservation valide et encaissée. Généralement accompagnée de paymentMethod.Oui
cancelledRéservation annulée (ou demande refusée). Elle reste dans l'historique mais libère le terrain : elle ne génère pas d'overlap et n'apparaît pas dans /availability.Non
Comment traiter les réservations « pending »
  • Tu les trouves dans GET /bookings et dans les créneaux occupés de /availability : traite le créneau comme non disponible, exactement comme une réservation confirmed.
  • Tu peux répondre à la demande depuis ton logiciel de gestion avec une PATCH : {"status": "confirmed"} pour l'accepter, {"status": "cancelled"} pour la refuser. Le joueur reçoit une notification avec le résultat.
  • Reconnais-les grâce au couple status: "pending" + source: "app". Les données du client proviennent du profil VolleyFriends du joueur (nom et e-mail), donc customerPhone peut être null.
  • Ne crée pas de réservations pending depuis ces API : la valeur est acceptée par la validation, mais cet état décrit une demande en attente de réponse de la part du gérant et personne ne prendrait de décision de ton côté. Si ton flux comprend un panier ou un paiement à finaliser, conserve l'attente dans ton système et appelle la POST lorsque la réservation est définitive — ou crée-la en confirmed et passe-la en cancelled si le paiement n'aboutit pas.

7.6 Origine et mode de paiement

ChampValeursStatut23. Stato
sourcemanualSaisie par le gérant dans le panneau.
apiCréée par un logiciel de gestion externe avec ces API. Valeur toujours définie par la POST, non modifiable.
appDemande de réservation en ligne envoyée par un joueur depuis l'app VolleyFriends : elle est créée en pending et attend ta décision.
tournamentGénérée par l'organisation d'un tournoi sur les terrains de la structure.
paymentMethodcash, bank_transfer, paypal, stripe, otherComment elle a été encaissée. null = non encaissée.

Les deux sont des domaines textuels : de nouvelles valeurs peuvent être ajoutées à l'avenir sans changement de version, ton code ne doit donc pas générer d'erreur sur une valeur qu'il ne connaît pas.

7.7 Données personnelles des clients

Le nom, le téléphone et l'e-mail que tu envoies dans les réservations sont des données personnelles de tes clients : tu es le responsable du traitement, VolleyFriends les conserve pour ton compte en tant que sous-traitant au sens de l'art. 28 GDPR. Envoie uniquement ce qui est nécessaire à la gestion du terrain et informe tes clients. Les détails figurent dans la privacy policy.

8. Erreurs générales

Chaque erreur a le même corps : un code stable dans error, conçu pour ton code, et un message en italien, conçu pour les personnes. Prends tes décisions sur error et sur le statut HTTP, non sur le texte du message, qui peut changer.

{
  "error": "overlap",
  "message": "Il campo è già prenotato in quell'orario."
}

Les erreurs de validation ajoutent issues, la liste détaillée des problèmes champ par champ :

{
  "error": "invalid_input",
  "message": "Dati non validi",
  "issues": [
    {
      "code": "invalid_string",
      "validation": "datetime",
      "path": ["startsAt"],
      "message": "Invalid datetime"
    }
  ]
}
HTTPerrorStatut23. StatoQue faire
400invalid_inputParamètres ou corps non valides. Le détail est dans issues.Corrige la requête. Ne réessaie pas à l'identique : le résultat ne changera pas.
401invalid_keyEn-tête X-Api-Key absent, malformé, inconnu ou révoqué.Vérifie que tu envoies l'en-tête et que la clé est active dans le panneau. Si elle a été révoquée, génères-en une nouvelle.
403subscription_requiredAucun abonnement « Gestore campi » en cours sur le compte propriétaire de la clé.Réactive l'abonnement depuis l'application (Profil → espace gérant → Abonnement). Aucun appel ne fonctionne d'ici là.
403feature_not_includedAbonnement actif, mais la formule n'inclut pas « API pour logiciels de gestion externes ». Le message mentionne la formule en cours.Passe à une formule qui inclut cette fonctionnalité. Valable pour toute la v1, lectures incluses.
404not_foundRessource inexistante ou appartenant à un autre gérant : les deux situations ne sont pas distinguées, afin de ne pas révéler les données d'autrui.Relis les ID depuis /venues et depuis /bookings : il s'agit presque toujours d'un ancien ID ou d'un ID copié depuis un autre compte.
409overlapLe terrain est déjà occupé sur le créneau demandé par une réservation non annulée.Propose un autre créneau : relis /availability. Ne réessaie pas sans modifier l'heure.
409venue_closedFermeture exceptionnelle de l'établissement à la date demandée.Propose une autre date. Les fermetures peuvent être consultées depuis /availability (closure).
429rate_limitedLimite de 120 requêtes par minute dépassée pour cette clé.Patiente et réessaie avec un backoff exponentiel, en respectant retry-after.
4xxerrorCode générique pour les erreurs de requête qui ne rentrent pas dans les cas ci-dessus (rare).Lis message et corrige la requête.
500internalErreur imprévue de notre côté.Réessaye plus tard. Si cela persiste, écris-nous en indiquant l'heure, l'endpoint et le préfixe de la clé (non la clé).

Un chemin inexistant sous la base renvoie 404 ; il en va de même pour une méthode non prévue sur un chemin existant :

{
  "error": "not_found",
  "message": "Risorsa non trovata"
}

Les réponses 502, 503 et 504 proviennent du reverse proxy lorsque l'API n'est pas joignable (redémarrage ou maintenance) : dans ces cas, le corps n'est pas garanti au format JSON. Traite-les comme des erreurs temporaires et réessaye avec un backoff, sans tenter d'en lire le contenu.

9. Versions et compatibilité

La version est dans le path : /manager-api/v1. Il n'existe aucun header de version ni date de release à spécifier : la seule version actuellement publiée est v1.

Ce que nous pouvons modifier sans te prévenir

  • Ajouter des champs aux réponses existantes.
  • Ajouter des paramètres optionnels aux requêtes, avec une valeur par défaut qui conserve le comportement actuel.
  • Ajouter de nouveaux endpoints sous /v1.
  • Ajouter des valeurs aux champs textuels ouverts (source, paymentMethod, surface).
  • Reformuler les message des erreurs, en maintenant inchangés les codes error.

Ce qui ne change pas au sein de la v1

  • Aucun champ ne sera supprimé ou renommé dans les réponses.
  • Aucun champ ne changera de type ou de signification.
  • Aucun paramètre actuellement optionnel ne deviendra obligatoire.
  • Les codes error et les statuts HTTP documentés ici restent les mêmes.

Toute modification rompant ces garanties arrivera sur un nouveau préfixe (/v2), la v1 étant maintenue en service avec un préavis pour les gestionnaires qui l'utilisent.

Comment écrire un client qui ne casse pas
  • Ignore les champs que tu ne connais pas au lieu de générer une erreur (pas de parsing « strict » qui rejette les propriétés inattendues).
  • Gère les valeurs inconnues des champs textuels ouverts avec une branche de repli.
  • Ne te fie pas à l'ordre des clés JSON ni au texte des messages.
  • Traite null et un champ absent comme équivalents en lecture.

Changelog

VersionDateNotes
v1juil. 2026Ajouts compatibles : tarifs par personne dans la grille tarifaire (pricingMode, minPlayers) et playersCount sur les réservations (avec l'erreur players_required) ; pagination limit/offset sur la liste des réservations ; revérification des conflits lors de la réactivation d'une réservation annulée ; recurrence désormais explicitement refusé avec 400 recurrence_unsupported (auparavant ignoré en silence).
v12026Première version publique : structures et terrains, réservations (liste, création, modification), disponibilité quotidienne, grille tarifaire. Authentification avec X-Api-Key, 120 requêtes/minute par clé.

10. Support à l'intégration

Pour des questions techniques, des comportements inattendus ou des demandes d'endpoints que tu ne trouves pas ici, écris-nous depuis la page Contacts. Pour aller droit au but, indique dans le message :

  • l'endpoint et la méthode que tu appelles ;
  • la date et l'heure de l'appel (avec le fuseau horaire) et le code HTTP reçu ;
  • le code error et le message de la réponse ;
  • le préfixe de la clé utilisée (ex. vfk_9f2c).
Jamais dans le message

Ne nous envoie pas la clé API complète par quelque canal que ce soit, pas même pour nous faire vérifier un problème : le préfixe nous suffit pour l'identifier. Si tu l'as déjà partagée avec quelqu'un, révoque-la et génères-en une nouvelle.

Si tu as besoin de tester l'intégration sans toucher au calendrier réel, crée une structure d'essai avec un terrain dédié et utilise-la : tous les appels sont limités aux structures du propriétaire de la clé, le reste de ton compte n'est donc pas concerné.