Synchroniser les réservations
Si vous vendez les courts d'un club via votre propre système — une place de marché, un logiciel de gestion de site ou le site web du club —, deux plannings doivent concorder. Cette page couvre les deux sens :
- Vous nous informez de chaque réservation que vous prenez, et nous bloquons ce court pour que personne ne le réserve dans Sporty.
- Nous vous informons de chaque réservation prise, déplacée ou annulée dans Sporty, par webhook signé.
Votre intégration est un compte de service. Elle détient un jeton obtenu par identifiants client et agit pour le club, pas pour un membre.
Ce qu'est une réservation venant de vous
Lorsque vous envoyez une réservation, le court est bloqué exactement comme lorsqu'un administrateur du club bloque un créneau dans Sporty :
- Elle a un nom (affiché dans le planning du club) et, éventuellement, une catégorie.
- Elle n'a ni joueurs ni prix. Sporty n'encaisse aucun paiement pour elle, n'applique aucune limite d'adhésion et ne notifie aucun membre à son sujet.
- Le court doit être libre. C'est la seule règle que nous vérifions.
Qui a joué et qui a payé reste dans votre système. Vous indiquez au club que le court est pris, vous ne vendez pas la réservation via Sporty.
Avant de commencer
Le club doit accorder à votre application les scopes dont elle a besoin. Pour une synchronisation bidirectionnelle, ce sont :
| Scope | Pour |
|---|---|
club.courts.read | Lister les courts, pour les faire correspondre aux vôtres |
club.reservations.read | Lire le planning et ses catégories, et recevoir les webhooks |
club.reservations.write | Bloquer, déplacer et annuler des réservations |
club.reservations.write permet à votre application d'annuler n'importe quelle réservation
du club, y compris celles des membres, comme peut le faire le personnel du club. Le club
l'accorde en connaissance de cause. Utilisez-le pour les réservations que vous devez reprendre,
pas pour faire le ménage dans le planning.
Obtenir un jeton :
curl -X POST https://api.sporty.plus/oauth2/token \
-d grant_type=client_credentials \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET \
-d scope="club.courts.read club.reservations.read club.reservations.write"
Si votre application agit pour plusieurs clubs, envoyez X-Club: <club id> avec chacune des
requêtes ci-dessous. Les webhooks se configurent club par club.
Catégories
Une réservation peut porter l'une des catégories de réservation du club, les étiquettes que ses administrateurs donnent aux créneaux bloqués (« Ligue », « École de tennis »). Le planning du club l'affiche dans la couleur de la catégorie.
curl https://api.sporty.plus/api/partner/v1/reservation-categories \
-H "Authorization: Bearer $ACCESS_TOKEN"
{ "data": [ { "id": 3, "name": "Liga", "color": "#ea47bc" } ] }
Cela nécessite club.reservations.read. Un club peut n'en avoir aucune ; category_id est
facultatif.
Bloquer un court
Pour trouver d'abord un créneau libre, GET /availability?date=… liste les créneaux de chaque
court, libres ou pris ; voir l'API partenaire. Bloquer
un court revérifie sa disponibilité, si bien qu'un créneau pris entre-temps renvoie quand même
409 slot_taken.
curl -X POST https://api.sporty.plus/api/partner/v1/reservations \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_id": "bk_8f3a21",
"court_id": 412,
"from": "2026-10-06T17:00:00+02:00",
"to": "2026-10-06T18:30:00+02:00",
"name": "Online: Horvat"
}'
| Champ | ||
|---|---|---|
external_id | obligatoire | Votre identifiant pour la réservation, jusqu'à 191 caractères. |
court_id | obligatoire | Un court issu de GET /courts. |
from, to | obligatoire | ISO 8601 avec un décalage horaire. to doit être postérieur à from. |
name | facultatif | Ce que le club voit dans son planning, jusqu'à 255 caractères. |
category_id | facultatif | L'une des catégories de réservation du club, issue de GET /reservation-categories. |
La réponse est 201 avec la réservation :
{
"data": {
"id": 918273,
"court_id": 412,
"court_name": "Teren 3",
"from": "2026-10-06T15:00:00+00:00",
"to": "2026-10-06T16:30:00+00:00",
"category_id": null,
"approved": false,
"cancelled": false,
"source": "partner",
"external_id": "bk_8f3a21",
"name": "Online: Horvat",
"updated_at": "2026-09-29T11:20:04+00:00"
}
}
Conservez id. C'est ce qui vous permet de déplacer ou d'annuler la réservation. Les heures
sont toujours renvoyées en UTC.
L'envoyer deux fois est sans risque
external_id rend la requête idempotente. Renvoyer le même external_id tant que cette
réservation existe répond 200 avec la réservation existante, inchangée. Une nouvelle
tentative après un délai dépassé ne bloque donc jamais un second court. Un nouvel envoi avec
d'autres horaires ne déplace pas la réservation ; utilisez PATCH pour la déplacer.
Si la réservation a été annulée entre-temps, par vous ou par le club, renvoyer son
external_id la crée à nouveau.
Lorsque le court est pris
{
"error": "slot_taken",
"error_description": "The court is already booked for some of that time.",
"conflicting_reservation_id": 918102
}
Statut 409. Une réservation peut commencer exactement quand une autre se termine ; tout
chevauchement supérieur à zéro est un conflit.
Déplacer une réservation
curl -X PATCH https://api.sporty.plus/api/partner/v1/reservations/918273 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"court_id": 413, "from": "2026-10-06T18:00:00+02:00", "to": "2026-10-06T19:30:00+02:00"}'
N'envoyez que ce qui change : court_id, name, category_id, from, to. Chaque extrémité
de l'horaire peut changer indépendamment, donc {"to": "…"} prolonge ou raccourcit une
réservation. Le nouveau créneau est vérifié par rapport à toutes les autres réservations, et la
réponse est la réservation mise à jour. Une modification qui ferait finir la réservation avant
son début est refusée avec 422 invalid_times.
Vous ne pouvez déplacer que les réservations envoyées par votre application. Les
réservations faites dans Sporty appartiennent aux membres et au club. Pour celles-ci, ainsi que
pour les réservations annulées, la réponse est 404.
Annuler une réservation
curl -X DELETE https://api.sporty.plus/api/partner/v1/reservations/918273 \
-H "Authorization: Bearer $ACCESS_TOKEN"
La réponse est 204. Cela fonctionne pour n'importe quelle réservation du club, et se déroule
comme l'annulation par un administrateur du club. S'il s'agissait de la réservation d'un membre,
il est remboursé sur le portefeuille de son club et averti de l'annulation.
Être informé des réservations faites dans Sporty
Configurer le webhook
curl -X PUT https://api.sporty.plus/api/partner/v1/webhook \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://bookings.example.com/hooks/sporty"}'
{
"data": {
"url": "https://bookings.example.com/hooks/sporty",
"delivered_up_to": "2026-09-29T11:25:00+00:00",
"last_delivered_at": null,
"last_failed_at": null,
"last_error": null,
"secret": "whsec_…"
}
}
secret n'est affiché qu'une fois, dans cette réponse. Conservez-le. Pour le remplacer,
envoyez "rotate_secret": true avec le PUT suivant, et la réponse contiendra le nouveau.- Renvoyer un
PUTchange l'URL.GET /webhookaffiche les paramètres actuels et l'état des livraisons ;DELETE /webhookarrête les livraisons. - Il y a un webhook par application et par club.
- Un nouveau webhook démarre au moment où vous le configurez. Pour tout ce qui précède, voir rattraper le retard.
- L'URL doit être en
https, ne contenir aucun identifiant et se résoudre vers une adresse publique. Tout le reste est refusé avec422.
Ce qui arrive
Environ une fois par minute, tant que quelque chose a changé, vous recevez un POST comme
celui-ci :
{
"id": "5d1c4e0a-4a59-4b8e-9a0e-2a7b8f6c9d12",
"club_id": 57,
"sent_at": "2026-09-29T11:31:07+00:00",
"events": [
{
"type": "reservation.created",
"reservation": {
"id": 918301,
"court_id": 412,
"court_name": "Teren 3",
"from": "2026-10-07T16:00:00+00:00",
"to": "2026-10-07T17:00:00+00:00",
"category_id": null,
"approved": true,
"cancelled": false,
"source": "sporty",
"external_id": null,
"name": null,
"updated_at": "2026-09-29T11:30:58+00:00"
}
}
]
}
typeprend l'une des valeurs suivantes :reservation.createdreservation.updated, lorsque la réservation a été déplacée, renommée ou modifiée d'une autre manière ;reservation.cancelled.
eventscontient jusqu'à environ 100 modifications, de la plus ancienne à la plus récente.- Les réservations des membres arrivent sans nom (
namevautnull) et sans les personnes qui y participent. Vous voyez que le court est pris, et quand.
Ne comptez pas sur type pour être complet. Les modifications sont regroupées entre deux
livraisons, si bien qu'une réservation créée puis annulée dans la même minute n'arrive que sous
la forme reservation.cancelled. Traitez reservation comme l'état actuel de la réservation et
appliquez-le : si cancelled vaut true, libérez le court ; sinon, bloquez-le sur court_id de
from à to.
Vous n'êtes pas informé de vos propres modifications. Une réservation que vous envoyez ou
déplacez ne vous est pas renvoyée. Lorsque vous annulez la réservation d'un membre, elle revient
bien sous la forme reservation.cancelled. Cet événement est sans conséquence, puisque le court
est déjà libre dans votre copie.
Répondre
Répondez avec n'importe quel 2xx dans les 10 secondes. Tout le reste compte comme un échec :
un autre statut, un délai dépassé ou une redirection (nous ne suivons pas les redirections). Les
mêmes modifications, plus tout ce qui est plus récent, sont alors renvoyées environ une minute
plus tard, et ainsi de suite jusqu'à ce que vous les acceptiez. Rien n'est perdu, et l'ordre est
conservé.
Cela signifie que vous recevrez parfois une modification deux fois. Rendez le traitement
idempotent : utilisez reservation.id comme clé, et ignorez une copie dont l'updated_at n'est
pas plus récent que ce que vous avez déjà.
La livraison s'arrête dans chacun de ces cas :
- le club retire son autorisation ;
- votre application est révoquée ;
- vous supprimez le webhook ;
- le club arrête le webhook.
GET /webhook affiche last_error si les livraisons échouent.
Vérifier la signature
Chaque livraison comporte :
Sporty-Signature: t=1790680267,v1=6c4b1e9f…
v1 est le HMAC-SHA256 en hexadécimal de {t}.{raw body}, avec votre secret de webhook comme
clé. Vérifiez-le sur le corps brut de la requête avant de l'analyser, et rejetez les
horodatages anciens pour qu'une livraison interceptée ne puisse pas être rejouée.
import crypto from 'node:crypto'
export function verifySporty(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')))
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds
return fresh && parts.v1?.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
}
function verifySporty(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'].'.'.$rawBody, $secret);
return abs(time() - (int) $parts['t']) <= $tolerance
&& hash_equals($expected, $parts['v1'] ?? '');
}
Prévoyez qu'une livraison arrive avec plusieurs minutes de retard : elle est renvoyée jusqu'à ce que vous l'acceptiez, mais elle est signée à nouveau à chaque fois, donc la tolérance n'a qu'à couvrir le décalage d'horloge.
Rattraper le retard
Les mêmes modifications sont disponibles sur demande. Utilisez-les après une panne de votre côté, lors de votre première connexion, ou comme vérification nocturne que les deux plannings concordent :
curl "https://api.sporty.plus/api/partner/v1/reservations?updated_since=2026-09-28T00:00:00Z" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Avec updated_since, la liste contient toutes les réservations modifiées à cet instant ou
après, de la modification la plus ancienne à la plus récente, annulations comprises.
Parcourez les pages avec per_page (jusqu'à 100) et page. Pour reprendre plus tard, repartez
du dernier updated_at que vous avez vu ; vous reverrez peut-être cette réservation, ce qui est
sans conséquence.
GET /reservations/{id} lit une seule réservation, annulée ou non.
Erreurs
| Statut | error | Signification |
|---|---|---|
409 | slot_taken | Le court est réservé sur une partie de cet horaire. conflicting_reservation_id indique par quelle réservation. |
422 | unknown_court | Le club n'a pas de court de ce type. |
422 | unknown_category | Le club n'a pas de catégorie de réservation de ce type. |
422 | invalid_times | La réservation finirait avant de commencer. |
404 | not_found | Aucune réservation de ce type dans ce club ; pour PATCH, aucune envoyée par votre application. |
409 | not_cancelable | La réservation n'a pas pu être annulée ; error_description indique pourquoi. |
409 | no_acting_admin | Aucun administrateur du club n'est enregistré comme ayant accordé l'accès à votre application. Demandez au club de l'accorder à nouveau. |
409 | no_courts | Le club n'a aucun court à réserver. |
Les erreurs générales (401, 403 insufficient_scope, 429, etc.) sont listées sur la page de
l'API partenaire.
Ce que voit le club
Chaque réservation que vous bloquez, déplacez ou annulez est enregistrée au nom de l'administrateur du club qui a accordé l'accès à votre application. Si le club a enregistré l'application lui-même, elle est enregistrée au nom de l'administrateur qui l'a enregistrée. Lorsque vous annulez la réservation d'un membre, c'est le nom de cet administrateur qui figure dans la notification du membre. Prévenez le club, pour que personne ne soit surpris.
Le club voit aussi votre webhook. Sous Paramètres → Clients API, un administrateur du club voit :
- son URL ;
- s'il livre, s'il échoue ou s'il n'a encore rien envoyé ;
- la date de sa dernière livraison, et la dernière erreur tant qu'il échoue.
Le secret n'y est jamais affiché. Faites en sorte que votre point de terminaison continue de répondre. Un webhook qui échoue pendant des jours, c'est ce qu'un club remarque, et ce qu'il arrête.
L'administrateur peut arrêter votre webhook sans retirer votre accès. Ensuite :
GET /webhookrépond404;- votre jeton fonctionne toujours ;
- un
PUTconfigure un nouveau webhook, avec un nouveau secret, qui est informé des modifications à partir de ce moment.
Appelez GET /reservations?updated_since=… à partir de votre dernière livraison reçue, pour
récupérer ce qui s'est passé pendant que le webhook était arrêté.