L'API partenaire
https://api.sporty.plus/api/partner/v1
C'est le seul endroit où un jeton d'accès OAuth fonctionne. Du JSON simple — pas du JSON:API — avec une surface volontairement réduite : chaque champ est un champ que quelqu'un a choisi d'exposer.
curl https://api.sporty.plus/api/partner/v1/courts \
-H "Authorization: Bearer $ACCESS_TOKEN"
Points de terminaison
| Méthode | Chemin | Scope |
|---|---|---|
GET | /me | me.read |
GET | /me/reservations | me.reservations.read |
POST | /me/reservations | me.reservations.write |
DELETE | /me/reservations/{id} | me.reservations.write |
GET | /me/results | me.results.read |
GET | /me/memberships | me.memberships.read |
GET | /clubs/me | club.read |
GET | /courts | club.courts.read |
GET | /availability | club.courts.read |
GET | /reservations | club.reservations.read |
GET | /reservations/{id} | club.reservations.read |
GET | /reservation-categories | club.reservations.read |
POST | /reservations | club.reservations.write |
PATCH | /reservations/{id} | club.reservations.write |
DELETE | /reservations/{id} | club.reservations.write |
GET PUT DELETE | /webhook | club.reservations.read |
GET | /members | club.members.read |
POST | /members | club.members.write |
GET /me
Le membre pour lequel le jeton a été émis : id, first_name, last_name, name, picture
et locale. Son adresse e-mail n'en fait pas partie ; demandez le scope email et lisez-la
depuis /oauth2/userinfo.
Ce point de terminaison et /me/reservations exigent un jeton émis pour un membre (code
d'autorisation), et ils portent sur les données propres du membre plutôt que sur celles d'un
club : aucun club n'a donc à accorder quoi que ce soit à votre application. Un jeton obtenu par
identifiants client reçoit 401. Si le membre a autorisé votre application pour un seul club,
vous voyez la partie de ses données qui concerne ce club.
GET /me/reservations
Les réservations dans lesquelles le membre joue ou entraîne, à venir sauf indication
contraire. from et to (ISO 8601) définissent la période ; per_page va jusqu'à 100.
{
"data": [
{
"id": 918301,
"club": { "id": 57, "name": "TK Test" },
"court_id": 412,
"court_name": "Teren 3",
"from": "2026-10-07T16:00:00+00:00",
"to": "2026-10-07T17:00:00+00:00",
"approved": true
}
]
}
Les autres joueurs ne sont pas inclus : avec qui un membre joue, c'est aux autres joueurs de le partager.
POST /me/reservations
Réserve un court en tant que membre. Toutes les règles que le club lui applique dans l'application Sporty s'appliquent ici aussi :
- les horaires d'ouverture et le tarif de son adhésion ;
- combien de temps à l'avance il peut réserver, et à quel point il peut s'approcher du début ;
- combien de réservations il peut détenir ;
- s'il doit payer d'abord.
{
"court_id": 412,
"from": "2026-10-07T18:00:00+02:00",
"to": "2026-10-07T19:30:00+02:00",
"external_id": "agent-7f3a"
}
fromettodoivent tomber sur les créneaux du court, tels queGET /availabilityles liste. La réservation doit couvrir un nombre entier de créneauxreservation_duration, et chacun d'eux doit être ouvert à ce membre.external_idest facultatif : votre identifiant pour la requête, jusqu'à 191 caractères. Le renvoyer répond200avec la réservation déjà créée, si bien qu'une nouvelle tentative ne réserve jamais deux fois.
La réponse est 201 :
{
"data": {
"id": 918402,
"club": { "id": 57, "name": "TK Test" },
"court_id": 412,
"court_name": "Teren 3",
"from": "2026-10-07T16:00:00+00:00",
"to": "2026-10-07T17:30:00+00:00",
"approved": false,
"price": 20,
"needs_payment": true,
"pay_by": "2026-10-01T10:20:00+00:00"
}
}
Le paiement n'est jamais encaissé via l'API. Lorsque needs_payment vaut true, le membre
doit payer dans l'application Sporty avant pay_by, sans quoi le club récupère le créneau.
Dites-le-lui. Un club qui n'exige pas de paiement à l'avance répond needs_payment: false et
pay_by: null. pay_by n'est fourni que dans la réponse qui a créé la réservation, et vaut
null lors d'une répétition.
Le membre est notifié comme pour toute réservation qu'il fait lui-même, et le club la voit dans son planning comme étant la sienne.
Lorsque le membre ne peut pas réserver, la réponse est 409 avec un code, plus le message
propre à la règle, traduit pour le membre et destiné à lui être affiché :
error | Signification |
|---|---|
slot_taken | Quelqu'un d'autre occupe une partie de ce créneau. |
outside_opening_hours | Le court n'est pas ouvert à ce membre sur toute la durée. |
court_closed | Le court est fermé ce jour-là. |
in_past | Cet horaire est passé. |
too_soon | Trop proche du début pour son adhésion. |
too_far_ahead | Plus éloigné que ce que son adhésion permet. |
leaves_a_gap | Cela laisserait un trou d'une demi-heure que le club n'autorise pas. |
duration_not_offered | Le club ne vend pas cette durée à cet horaire. |
limit_reached | Il détient déjà autant de réservations que son adhésion le permet. |
not_a_member | Le club ne l'a pas encore accepté. |
not_bookable | Toute autre règle ; le message précise laquelle. |
invalid_times (422) signifie que les horaires ne correspondent pas à des créneaux entiers,
ou qu'ils dépassent minuit. 403 access_denied signifie que le membre a autorisé votre
application pour un autre club.
DELETE /me/reservations/{id}
Annule l'une des réservations du membre en tant que membre, si bien que son délai
d'annulation s'applique. Tout ce qu'il a payé retourne dans le portefeuille de son club, et les
autres joueurs sont prévenus. La réponse est 204. Elle est 409 not_cancelable, avec la
raison, lorsqu'il est trop tard ou que la réservation n'est pas la sienne à annuler (quelqu'un
d'autre l'y a inscrit). Elle est 404 pour une réservation dont il ne fait pas partie.
GET /me/results
Les résultats de matchs du membre, du plus récent au plus ancien. from et to (ISO 8601)
restreignent la période ; per_page va jusqu'à 100.
{
"data": [
{
"id": 22875,
"club": { "id": 57, "name": "TK Test" },
"played_at": "2026-09-27T18:00:00+00:00",
"sport": "tennis",
"kind": "league",
"sets": [
{ "games": [2, 6], "tie_break": null },
{ "games": [6, 3], "tie_break": null },
{ "games": [7, 6], "tie_break": [10, 8] }
],
"won": true,
"official": true,
"verified": true
}
]
}
- Les scores sont vus du côté du membre. Chaque paire est
[celui du membre, celui des adversaires], vous n'avez donc jamais besoin de savoir de quel côté du match il se trouvait. - Sets : seuls les sets effectivement joués sont listés.
won:nulllorsqu'il n'y a pas eu de vainqueur.kind:league,tournamentoumatch.verified: les adversaires ont confirmé le score.- Les adversaires ne sont pas inclus : contre qui un membre a joué, c'est aux adversaires de le partager.
GET /me/memberships
Les clubs dont le membre fait partie, et avec quelle formule : toutes les adhésions qui n'ont pas pris fin.
{
"data": [
{
"club": { "id": 57, "name": "TK Test" },
"membership": { "id": 12, "name": "Seniori", "basic": false },
"start": "2026-01-01T00:00:00+00:00",
"end": "2026-12-31T00:00:00+00:00",
"renews": false
}
]
}
end vaut null pour une adhésion sans date de fin. renews indique si elle se renouvelle
automatiquement. basic désigne la formule dont dispose chaque membre du club avant d'en
acheter une autre.
GET /clubs/me
Le club pour lequel votre jeton agit. À appeler en premier, surtout si vous disposez d'autorisations pour plusieurs clubs.
GET /courts
per_page jusqu'à 100. Chaque court a id, name, active, indoor, lights,
working_from, working_to, reservation_duration (minutes par créneau) et sports, ce qui
s'y pratique :
"sports": [ { "id": 2, "slug": "padel", "name": "Padel" } ]
La plupart des courts ont un seul sport ; quelques-uns en ont deux, et un court pour lequel personne n'en a défini n'en a aucun.
GET /availability
Les créneaux libres. date (YYYY-MM-DD, le jour local du club) est obligatoire ; days
couvre jusqu'à 7 jours à partir de cette date ; court_id restreint à un seul court.
{
"data": [
{
"court_id": 412,
"date": "2026-10-07",
"timezone": "Europe/Zagreb",
"closed": null,
"slots": [
{ "from": "2026-10-07T06:00:00+00:00", "to": "2026-10-07T07:00:00+00:00", "available": false },
{ "from": "2026-10-07T07:00:00+00:00", "to": "2026-10-07T08:00:00+00:00", "available": true }
]
}
]
}
- Une entrée par court et par jour. Les créneaux suivent l'horaire du club pour ce jour,
chacun d'une durée
reservation_duration. Les heures sont en UTC ;timezoneest celui du club. - Occupé ou libre. Un créneau est occupé (
"available": false) dès qu'une réservation le chevauche, même partiellement. - Courts fermés.
closedvautinactiveoucourt_breaklorsque le court ne peut pas du tout être réservé ce jour-là, et sesslotssont alors vides. - Quel horaire. Il s'agit de l'horaire standard du club, celui que voit un visiteur non connecté. L'adhésion propre à un membre peut ouvrir ou fermer certaines heures, et fixer le prix. Ni l'un ni l'autre n'apparaît ici.
GET /reservations
from et to (ISO 8601, UTC) restreignent la période ; per_page jusqu'à 100. Les
réservations sont renvoyées sans les personnes qui y participent. Les lire exige
club.members.read et le point de terminaison des membres, afin que le club prenne les deux
décisions séparément.
updated_since renvoie à la place toutes les réservations modifiées à cet instant ou après,
de la modification la plus ancienne à la plus récente, annulations comprises. Il sert à
maintenir une copie synchronisée.
Chaque réservation comporte :
| Champ | |
|---|---|
id, court_id, court_name | |
from, to | UTC |
category_id | La catégorie de réservation du club, le cas échéant |
approved | Si la réservation est confirmée |
cancelled | Ne vaut true qu'avec updated_since ou sur GET /reservations/{id} |
source | partner pour une réservation envoyée par votre application, sporty pour toute autre |
external_id, name | Votre identifiant et votre nom pour une réservation que vous avez envoyée ; null pour toutes les autres |
updated_at | La date de dernière modification de la réservation |
Écrire des réservations et webhooks
POST, PATCH et DELETE /reservations ainsi que /webhook sont destinés à un système qui
garde son propre planning synchronisé avec celui du club. Ils sont décrits, avec des exemples,
dans Synchroniser les réservations.
GET /members
search recherche dans le nom ou l'e-mail. per_page jusqu'à 100.
POST /members
{
"email": "new@example.com",
"firstName": "New",
"lastName": "Member",
"language": "hr",
"countryCode": "HR"
}
language et countryCode sont facultatifs et prennent par défaut les valeurs du club.
Idempotent sur l'adresse e-mail. Un membre que nous connaissons déjà est rattaché au club et
renvoyé avec 200 ; un nouveau est créé et renvoyé avec 201. Renvoyer la même personne est
sans risque, ce qui compte lorsque deux systèmes synchronisent leurs personnes.
Ce qui n'est pas ici
L'encaissement des paiements. Une réservation de membre effectuée via l'API se paie dans l'application Sporty, jamais via l'API.
Erreurs
| Statut | Corps | Signification |
|---|---|---|
401 | invalid_token | Absent, expiré, révoqué, ou ne venant pas de nous. |
403 | insufficient_scope | Jeton valide, scope non accordé. WWW-Authenticate indique ce qui est nécessaire. |
403 | access_denied | Aucun club n'a accordé l'accès à votre application, ou pas le club que vous avez désigné. |
400 | club_required | Vous agissez pour plusieurs clubs ; désignez-en un avec X-Club. |
422 | Erreurs de validation Laravel | Un corps de requête mal formé. |
429 | — | 120 requêtes par minute, par client. |
Versionnement
v1 figure dans le chemin et sa structure ne changera pas sous vos pieds. De nouveaux champs et
de nouveaux points de terminaison peuvent être ajoutés ; les champs existants ne seront ni
supprimés ni modifiés dans leur signification. Un changement incompatible signifie v2, et nous
vous préviendrons avant son arrivée.