API Partenaire · Accès restreint

PTM Partner API

API REST exposée par Padel Tournoi Manager pour les partenaires intégrés. Authentification par clé API. Tous les endpoints retournent du JSON.

Base URL STAGING https://coavlxhpthncsnbnfstw.supabase.co/functions/v1
Format JSON
·
Auth x-api-key header
·
Version v1
·
Mis à jour Juin 2026
01

Authentification

Header requis

Toutes les requêtes doivent inclure la clé API dans le header x-api-key. La clé est fournie par PTM lors de l'onboarding partenaire.

Exemple de requête authentifiée
HTTP
GET /partner-tournaments-list x-api-key: ptm_live_xxxxxxxxxxxxxxxxxxxx Content-Type: application/json
⚠ Sécurité

Ne jamais exposer la clé API côté client. Tous les appels doivent transiter par votre backend.

02

Codes d'erreur

CodeSignificationDescription
200OKSuccès, y compris les cas métier (not_found, multiple…)
201CreatedInscription créée (statut confirmed, pending ou waitlist)
400Bad RequestParamètre requis manquant ou mal formé (email, téléphone)
401UnauthorizedClé API absente, invalide ou désactivée
404Not FoundRessource inexistante ou non publiée sur Pista
409ConflictDoublon d'inscription (paire déjà inscrite)
500Server ErrorErreur interne PTM
03

Liste des tournois

GET /partner-tournaments-list Tournois ouverts aux inscriptions

Retourne la liste des tournois publiés sur Pista avec les inscriptions ouvertes (registration_open = true). Tous les filtres sont optionnels et cumulables.

Paramètres optionnels
ParamètreTypeDescription
date_fromdate (YYYY-MM-DD)Tournois à partir de cette date
date_todate (YYYY-MM-DD)Tournois jusqu'à cette date
genderstringFiltrer par genre : Masculin, Féminin, Mixte
levelstringFiltrer par niveau : P25, P100, P250…
club_iduuidRestreindre aux tournois d'un club spécifique
latnumberLatitude du point de référence (requis avec lng et radius_km)
lngnumberLongitude du point de référence
radius_kmnumberRayon de recherche en km autour du point lat/lng
Requête
HTTP
GET /partner-tournaments-list?date_from=2026-06-01&date_to=2026-07-31&gender=Masculin&level=P25 x-api-key: votre_clé_api
Réponse 200 - Succès
JSON
{ "tournaments": [ { "id": "uuid", "name": "Tournoi P25 ON Padel", "date": "2026-06-20", "end_date": null, "start_time": "09:00", "level": "P25", "gender": "Masculin", "club_name": "ON Padel Escalquens", "city": "Escalquens", "address": "12 Rue du Padel, 31750 Escalquens", "latitude": 43.5421, "longitude": 1.5987, "total_pairs": 16, "registered_pairs": 4, "available_spots": 12, "registration_open": true, "status": "scheduled", "featured": false, "dotation": "500 €" } ] }
Note

available_spots peut être null si le tournoi n'a pas de limite de paires définie. La réponse est limitée à 100 résultats. Une pagination cursor-based (?cursor=) sera disponible prochainement.

end_date (AAAA-MM-JJ) n'est renseignée que pour un tournoi sur plusieurs jours ; elle vaut null pour un tournoi d'une journée. start_time (HH:MM) peut être null.

status, featured et dotation sont destinés aux sites vitrine de club (clé bornée à un compte) : badge « en cours » / « terminé », mise en avant, prize money. status est l'enum partenaire stable, découplé de notre interne : scheduled, in_progress, completed, cancelled — les mêmes valeurs que le webhook TournamentUpdated. Un agrégateur peut les ignorer sans conséquence.

date_from
date_to
gender
level
club_id
radius_km
lat
lng
04

Détail d'un tournoi

GET /partner-tournaments-detail?id={uuid} Détail complet
Paramètres
ParamètreTypeRequisDescription
iduuidrequisIdentifiant du tournoi
Requête
HTTP
GET /partner-tournaments-detail?id=550e8400-e29b-41d4-a716-446655440000 x-api-key: votre_clé_api
Réponse 200 - Succès
JSON
{ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Tournoi P25 ON Padel", "date": "2026-06-20", "end_date": "2026-06-21", "start_time": "09:00", "level": "P25", "gender": "Masculin", "format": "TMC", "club_name": "ON Padel Escalquens", "city": "Escalquens", "address": "12 Rue du Padel, 31750 Escalquens", "total_pairs": 16, "registered_pairs": 4, "available_spots": 12, "registration_open": true, "entry_fee": 30, "status": "scheduled", "featured": false, "dotation": "500 €" }
Réponse 400 - Paramètre manquant
JSON
{ "error": "id is required" }
Réponse 404 - Tournoi introuvable
JSON
{ "error": "Tournament not found" }
id *
05

Inscription d'une paire

Vérification & logique automatique

À l'inscription, PTM vérifie chaque joueur auprès de la FFT. Trois issues possibles :

  • status: confirmed — joueurs vérifiés et places disponibles : la paire est ajoutée au tableau.
  • status: waitlist — tournoi complet : la paire est placée en liste d'attente (avec sa position).
  • status: pending — un joueur n'a pas pu être vérifié (introuvable FFT, licence incorrecte, homonymie, ou service FFT temporairement indisponible) : la place est réservée mais l'inscription attend une validation manuelle du juge-arbitre.

Les champs requis manquants ou mal formés (email, téléphone) sont rejetés en 400 avant toute création.

POST /partner-register Inscription paire au tournoi
Body (JSON)
JSON
{ "tournament_id": "550e8400-e29b-41d4-a716-446655440000", "player1": { "tenup_id": "2834285838", "classement": 9013, "gender": "H", "first_name": "Jean", "last_name": "Dupont", "phone": "0612345678", "email": "jean.dupont@email.com" }, "player2": { "tenup_id": "2834285839", "classement": 4210, "first_name": "Marie", "last_name": "Martin" } }
Champs requis
ChampTypeDescription
tournament_iduuidrequis ID du tournoi cible
player1.first_namestringrequis Prénom joueur 1
player1.last_namestringrequis Nom joueur 1
player1.emailstringrequis Email joueur 1 (format validé) — le joueur 1 est le contact de la paire
player1.phonestringrequis Téléphone joueur 1 (≥ 6 chiffres)
player2.first_namestringrequis Prénom joueur 2
player2.last_namestringrequis Nom joueur 2
playerN.tenup_idstringIdentifiant fédéral (idCrm). Recommandé : c'est l'ancre d'identité la plus fiable
playerN.classementnumberClassement padel, transmis avec tenup_id
playerN.genderstring"H" ou "F", transmis avec tenup_id
playerN.licence_numberstringLicence FFT. Facultative depuis le 29/08/2026
playerN.no_licencebooleanLe joueur déclare ne pas encore avoir de licence
player2.emailstringFacultatif. Validé s'il est fourni
player2.phonestringFacultatif. Validé s'il est fourni

Aucun identifiant fédéral n'est obligatoire. Envoyez tenup_id quand vous l'avez : c'est le chemin recommandé, et le seul qui n'appelle pas la FFT — PTM fait alors confiance au classement et au gender que vous transmettez, qui doivent donc provenir d'une recherche fédérale et non d'une saisie libre du visiteur. Sans tenup_id, PTM recherche le joueur par nom et licence. Sans rien du tout, l'inscription est acceptée et signalée comme incomplète au juge-arbitre.

⚠️ N'exigez pas la licence de vos utilisateurs. La recherche fédérale publique ne la retourne pas : elle rend le nom, le classement, le club et l'idCrm, jamais le numéro de licence, qui n'est accessible que par un appel authentifié. Ce n'est pas un cas particulier réservé aux fiches masquées, c'est le cas nominal — un formulaire qui exige la licence est infranchissable pour l'ensemble de vos joueurs, pas pour quelques-uns. Mesuré le 30/08/2026 sur des recherches réelles.

L'idCrm le remplace intégralement comme ancre d'identité, et il a l'avantage de ne dépendre d'aucune authentification. C'est la raison pour laquelle la licence est devenue facultative le 29/08/2026.

L'email (format) et le téléphone (au moins 6 chiffres) sont validés ; une valeur mal formée renvoie un 400, y compris pour le joueur 2 dont ces champs sont pourtant facultatifs.

Réponse 202 - Demande en attente de confirmation
JSON
{ "registration_id": "uuid", "status": "pending_contact", "message": "Demande enregistree. Un email de confirmation vient de vous etre envoye...", "expires_at": "2026-08-29T22:39:41.372Z" }

C'est la réponse NOMINALE d'une clé de club. La place n'est pas réservée au POST : elle l'est quand le joueur 1 clique le lien reçu par email. Entre les deux, l'inscription existe mais n'occupe aucune place, et le tournoi peut se remplir. N'annoncez donc pas une place acquise sur cet écran.

expires_at est toujours présent sur un 202 (échéance à 60 minutes). Le jeton de confirmation n'est jamais rendu : il ne transite que par l'email, faute de quoi la preuve ne prouverait plus rien. Rejouer le même POST rend la même demande et n'envoie pas de second email — un bouton « je n'ai pas reçu l'email » serait donc sans effet, mieux vaut inviter à vérifier les indésirables. Seul le joueur 1 est destinataire ; le joueur 2 est prévenu après confirmation.

Le clic aboutit sur une page PTM, qui annonce elle-même le résultat : confirmé, ou liste d'attente avec la position si le tournoi s'est rempli entre-temps. Le message ci-dessus est un repli technique, pas un texte d'interface : rédigez le vôtre.

⚠️ Les réponses 201 ci-dessous ne concernent que les clés agrégateur. Une clé de club reçoit toujours un 202.

Réponse 201 - Confirmé
JSON
{ "registration_id": "uuid", "status": "confirmed", "players": [ { "slot": 1, "ptm_player_id": "uuid" }, { "slot": 2, "ptm_player_id": "uuid" } ], "message": "Registration confirmed" }

players porte le ptm_player_id de chaque slot : stockez-le, c'est la clé de jointure du remplacement de joueur. Le webhook change_type: "roster" renvoie le même identifiant pour un slot inchangé et un identifiant différent pour le slot remplacé — vous diffez dessus. Le champ est absent en liste d'attente (la paire n'existe pas encore ; les identifiants apparaissent à la promotion) et présent en confirmed comme en pending.

Réponse 201 - Liste d'attente
JSON
{ "registration_id": "uuid", "status": "waitlist", "waitlist_position": 2, "message": "Tournament is full, you have been added to the waitlist" }
Réponse 201 - En attente de validation (vérification FFT échouée)
JSON
{ "registration_id": "uuid", "status": "pending", "players": [ { "slot": 1, "ptm_player_id": "uuid" }, { "slot": 2, "ptm_player_id": "uuid" } ], "message": "Registration received, pending verification" }
Réponse 400 - Champ manquant ou invalide
JSON
{ "error": "missing_field", "field": "player1.phone_invalid" }

field identifie le problème : nom du champ manquant (ex. player2.last_name) ou suffixe _invalid pour un format incorrect (player1.email_invalid, player1.phone_invalid).

Réponse 404 - Tournoi introuvable
JSON
{ "error": "tournament_not_found" }

Rendu aussi bien pour un tournoi inexistant que pour un tournoi hors du périmètre de votre clé. Les deux cas sont volontairement indistincts : les séparer révélerait l'existence des tournois d'un autre club.

Réponse 409 - Inscriptions closes ou pas encore ouvertes
JSON
{ "error": "registration_not_yet_open", "opens_at": "2026-09-01T09:00:00.000Z" } { "error": "registration_closed" }

Depuis le 29/08/2026, ces deux cas répondent 409 et non plus 404 : le tournoi existe, seul son état s'oppose à la demande. opens_at n'accompagne que registration_not_yet_open — « pas encore ouvert » et « fermé » appellent des réactions opposées du joueur, revenir ou renoncer.

⚠️ Lisez error avant le code HTTP. Le 409 ne signifie plus « paire déjà inscrite » à lui seul.

Réponse 409 - Joueur déjà engagé
JSON
{ "error": "This pair is already registered" }

Déclenché dès qu'un des deux joueurs est déjà engagé sur ce tournoi, par sa licence ou par son tenup_id. Une demande en attente de confirmation (202) ne bloque personne : elle ne réserve rien.

Body JSON
06

Désinscription d'une paire

Restrictions

La désinscription n'est possible que pour les inscriptions créées via Pista (source: pista). Elle est bloquée si le tournoi est en cours ou terminé. La promotion automatique de la liste d'attente n'est pas gérée, le JA continue à promouvoir manuellement.

POST /partner-unregister Annuler une inscription paire
Body (JSON)
JSON
{ "registration_id": "550e8400-e29b-41d4-a716-446655440000" }
Champs requis
ChampTypeDescription
registration_iduuidID de l'inscription à annuler
Réponse 200 - Annulation confirmée
JSON
{ "status": "cancelled", "message": "Registration successfully cancelled" }
Réponse 400 - Paramètre manquant
JSON
{ "error": "registration_id is required" }
Réponse 403 - Inscription non-Pista
JSON
{ "error": "Cannot unregister a non-Pista registration" }
Réponse 404 - Inscription introuvable
JSON
{ "error": "Registration not found" }
Réponse 409 - Tournoi en cours ou inscription déjà annulée
JSON
{ "error": "Tournament already in progress, unregistration not allowed" } { "error": "Registration already cancelled" }
Body JSON
07

Webhooks · disponible (staging)

Implémenté sur staging

PTM notifie des changements sur le cycle de vie des inscriptions et des tournois (et non sur les attributs des joueurs : classement/contact ne sont pas synchronisés — servis par GET tournaments/:id). L'émission est active sur staging ; il reste à échanger l'URL d'abonnement et le secret de signature, et à confirmer le mapping de l'annulation (voir plus bas).

Chaque événement est envoyé en POST vers l'URL d'abonnement, signé en HMAC-SHA256 du corps brut (header X-PTM-Signature, hex). Un header X-PTM-Event-Id (UUID) permet la déduplication idempotente. En cas d'échec, réessais en backoff exponentiel (jusqu'à 8 tentatives, puis abandon). Seuls les changements visibles côté partenaire sont émis (pas de notification si le statut projeté ne change pas) ; un changement initié par le partenaire n'est pas réémis vers lui.

RegistrationStatusUpdated — changement de statut d'une inscription
JSON
{ // change_type: "status" — transition du statut projeté "event": "RegistrationStatusUpdated", "registration_id": "uuid", "tournament_id": "uuid", "change_type": "status", "previous_status": "pending", "new_status": "confirmed", "timestamp": "2026-06-15T10:00:00Z" } // change_type: "roster" — remplacement de joueur, statut inchangé { "event": "RegistrationStatusUpdated", "registration_id": "uuid", "tournament_id": "uuid", "change_type": "roster", "previous_status": "confirmed", "new_status": "confirmed", "roster": [ { "ptm_player_id": "uuid", "first_name": "...", "last_name": "...", "licence": "..." }, { "ptm_player_id": "uuid", "first_name": "...", "last_name": "...", "licence": "..." } ], "timestamp": "2026-06-15T10:00:00Z" }
Statuts de sortie : cancelled ≠ rejected

Une inscription peut sortir pour deux raisons opposées, et PTM les distingue : rejected = le juge-arbitre refuse une inscription qu'il n'a pas pu vérifier (licence douteuse, joueur injoignable) ; cancelled = désistement ou retrait. Le champ reason accompagne ces deux statuts et donne la cause exacte :

reasonSignification
ja_rejectionRefus du juge-arbitre après vérification impossible (accompagne rejected)
ja_cancellationLe juge-arbitre annule une inscription vérifiée — typiquement un joueur qui se désiste
bulk_replaceAnnulation en masse : le juge-arbitre a réimporté sa liste de joueurs en mode remplacement. Ce n'est un jugement sur personne — attendez-vous à recevoir l'événement pour toutes les inscriptions du tournoi d'un coup.
partner_unregisterDésinscription que vous avez vous-même initiée (ne vous est pas réémise — anti-echo)

L'annulation d'un tournoi n'émet pas d'événement par inscription : elle arrive via TournamentUpdated avec status: "cancelled", à vous de retirer les inscriptions associées.

change_type distingue deux causes d'émission. "status" : transition du statut projeté confirmed / pending / waitlist / rejected — ex. le juge-arbitre valide une inscription en anomalie (pending → confirmed), promeut une liste d'attente, ou rejette une inscription. "roster" : remplacement d'un joueur dans la paire, à statut inchangé. Le champ roster n'est présent que dans ce second cas, et il est volontairement minimisé — ptm_player_id (identifiant opaque et stable, pour la réconciliation par slot), nom, prénom et licence, sans email, téléphone ni classement ; ces attributs restent servis par GET tournaments/:id. N'est émis que pour les inscriptions d'origine partenaire ; l'ancre de liaison est registration_id, renvoyé par POST tournaments/:id/register.

TournamentUpdated — changement sur un tournoi
JSON
{ "event": "TournamentUpdated", "tournament_id": "uuid", "status": "cancelled", "previous_status": "scheduled", "max_pairs": 16, "available_spots": 6, "timestamp": "2026-06-15T10:00:00Z" }

Émis sur changement du statut projeté du tournoi — scheduled / in_progress / completed / cancelled — et non du statut interne PTM. Les états amont (brouillon, configuration, prêt) sont tous projetés en scheduled : leurs transitions ne génèrent donc aucun événement, ce qui vous épargne le bruit de préparation. → cancelled signifie que toutes les inscriptions du tournoi sont à retirer côté partenaire. N'est émis que pour les tournois ayant au moins une inscription d'origine partenaire.

Points encore à valider avec Pista

URL d'abonnement + secret de signature à échanger, sans quoi les événements sont enregistrés mais jamais livrés. Deux points sont désormais tranchés : l'identifiant joueur opaque est ptm_player_id (livré à l'inscription et dans le roster), et l'annulation n'est plus rabattue sur rejected — cancelled est un statut à part entière, accompagné de sa reason.