OAuth et Se connecter avec Sporty

L'API partenaire

Ce qu'un jeton d'accès peut réellement lire et écrire, et ce qu'il ne peut délibérément pas faire.

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éthodeCheminScope
GET/meme.read
GET/me/reservationsme.reservations.read
POST/me/reservationsme.reservations.write
DELETE/me/reservations/{id}me.reservations.write
GET/me/resultsme.results.read
GET/me/membershipsme.memberships.read
GET/clubs/meclub.read
GET/courtsclub.courts.read
GET/availabilityclub.courts.read
GET/reservationsclub.reservations.read
GET/reservations/{id}club.reservations.read
GET/reservation-categoriesclub.reservations.read
POST/reservationsclub.reservations.write
PATCH/reservations/{id}club.reservations.write
DELETE/reservations/{id}club.reservations.write
GET PUT DELETE/webhookclub.reservations.read
GET/membersclub.members.read
POST/membersclub.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"
}
  • from et to doivent tomber sur les créneaux du court, tels que GET /availability les liste. La réservation doit couvrir un nombre entier de créneaux reservation_duration, et chacun d'eux doit être ouvert à ce membre.
  • external_id est facultatif : votre identifiant pour la requête, jusqu'à 191 caractères. Le renvoyer répond 200 avec 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é :

errorSignification
slot_takenQuelqu'un d'autre occupe une partie de ce créneau.
outside_opening_hoursLe court n'est pas ouvert à ce membre sur toute la durée.
court_closedLe court est fermé ce jour-là.
in_pastCet horaire est passé.
too_soonTrop proche du début pour son adhésion.
too_far_aheadPlus éloigné que ce que son adhésion permet.
leaves_a_gapCela laisserait un trou d'une demi-heure que le club n'autorise pas.
duration_not_offeredLe club ne vend pas cette durée à cet horaire.
limit_reachedIl détient déjà autant de réservations que son adhésion le permet.
not_a_memberLe club ne l'a pas encore accepté.
not_bookableToute 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 : null lorsqu'il n'y a pas eu de vainqueur.
  • kind : league, tournament ou match.
  • 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 ; timezone est 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. closed vaut inactive ou court_break lorsque le court ne peut pas du tout être réservé ce jour-là, et ses slots sont 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, toUTC
category_idLa catégorie de réservation du club, le cas échéant
approvedSi la réservation est confirmée
cancelledNe vaut true qu'avec updated_since ou sur GET /reservations/{id}
sourcepartner pour une réservation envoyée par votre application, sporty pour toute autre
external_id, nameVotre identifiant et votre nom pour une réservation que vous avez envoyée ; null pour toutes les autres
updated_atLa 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

StatutCorpsSignification
401invalid_tokenAbsent, expiré, révoqué, ou ne venant pas de nous.
403insufficient_scopeJeton valide, scope non accordé. WWW-Authenticate indique ce qui est nécessaire.
403access_deniedAucun club n'a accordé l'accès à votre application, ou pas le club que vous avez désigné.
400club_requiredVous agissez pour plusieurs clubs ; désignez-en un avec X-Club.
422Erreurs de validation LaravelUn 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.