API v1 · OpenID Connect
Connectez votre site à votre salle.
Laissez vos membres se connecter avec leur compte de la salle, vérifier un abonnement, réserver des cours et afficher votre planning sur votre propre site. OpenID Connect standard, une petite API REST et des webhooks signés.
$ curl https://movemembers.com/.well-known/openid-configuration { "issuer": "https://movemembers.com", "authorization_endpoint": "…/oauth/authorize", "token_endpoint": "…/oauth/token", "code_challenge_methods_supported": ["S256"] }
01 · Démarrage rapide
Démarrage rapide de l'API MoveMembers : connecter, échanger, lire.
Les membres se connectent avec leur compte de la salle. Votre site reçoit une réponse signée avec leur numéro de membre : toute bibliothèque OIDC convient.
Envoyez le membre vers la page de connexion de sa salle. PKCE est obligatoire pour toute application, confidentielle ou publique.
<!-- "Join Your Gym" --> <a href="https://movemembers.com/oauth/authorize?client_id=app_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&response_type=code&scope=openid%20member_number%20profile%20email%20phone%20membership%20bookings%20field%3Avisitor_type&redirect_uri=https://yoursite.com/callback&state=…&nonce=…&code_challenge=…&code_challenge_method=S256&prompt=create" style="display:inline-flex;align-items:center;gap:10px;padding:12px 20px; border-radius:10px;background:#ffffff;color:#14161b;border:1px solid #e4e6ef; font-family:inherit; font-size:15px;font-weight:600;text-decoration:none"> Join Your Gym </a>
Vous recevez un jeton d'identité signé par la salle (ES256, clés sur /oauth/jwks). Ne faites jamais cet échange depuis une page : il exige votre secret client.
const { Issuer } = require("openid-client"); // any certified OIDC client works const issuer = await Issuer.discover("https://movemembers.com"); const client = new issuer.Client({ client_id: "app_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", client_secret: process.env.APP_SECRET, // never hard-code this redirect_uris: ["https://yoursite.com/callback"], response_types: ["code"], }); // codeVerifier is the one you generated before redirecting the member const tokenSet = await client.callback( "https://yoursite.com/callback", params, // req.query, from the redirect { code_verifier: codeVerifier, state, nonce }, ); // tokenSet.claims() is verified against the gym's public keys at /oauth/jwks const member = tokenSet.claims();
Uniquement ce que la salle a vérifié et que le membre a approuvé. Rattachez votre fiche à « sub », jamais à l'adresse e-mail.
{
"sub": "mbr_4Hq9kR2vLpXsT8nZ",
"member_number": "1042",
"name": "Nok Suwan",
"email": "nok@example.com",
"email_verified": true,
"phone_number": "+66812345678",
"membership": { "status": "active", "plan": "Monthly", "ends": "2026-11-01" },
"fields": { "visitor_type": "…" }
}04 · Vérifier un membre
Vérifier ensemble un numéro de membre et une adresse e-mail.
De serveur à serveur, avec un jeton d'application (client_credentials, scope=lookup). Envoyez-les dans le corps de la requête, jamais dans l'adresse : une adresse finit dans les journaux du serveur.
La réponse est toujours 200 avec un seul champ, « active », pour que personne ne puisse se servir de votre formulaire afin de savoir qui est membre. Seul un corps mal formé répond 400 invalid_request.
const res = await fetch("https://movemembers.com/api/v1/members/verify", { method: "POST", headers: { Authorization: `Bearer ${tokens.access_token}`, "Content-Type": "application/json", }, body: JSON.stringify({ member_number: 1042, email: form.email }), }); // { "active": true }
Recettes
Des recettes pour l'API de la salle.
Chaque point d'écriture appelle exactement les mêmes règles que les applications de la salle : fenêtres de réservation, listes d'attente, contrôles d'offre.
Relier une fiche de votre site à un membre
Un chien, un casier, une voiture : stockez « sub » sur votre propre fiche comme clé étrangère, jamais l'e-mail. Revérifiez le statut plus tard avec un jeton d'application.
Voir le guideGET /api/v1/members/1042
Authorization: Bearer <app token, scope=lookup>Réserver ou annuler au nom du membre
Listez le planning, puis réservez avec son jeton d'accès. Un « booking_refused » porte toujours une raison que le membre comprend : affichez error.message.
Voir le guideconst res = await fetch("https://movemembers.com/api/v1/bookings", { method: "POST", headers: { Authorization: `Bearer ${memberAccessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ class_id: classId, date: "2026-10-04" }), }); if (!res.ok) { const { error } = await res.json(); if (error.code === "booking_refused") showToMember(error.message); // e.g. "class full" }
Afficher votre planning et vos prix
Information publique, sans connexion du membre. Un jeton d'application avec scope=schedule lit /schedule et /offers.
Voir le guideconst res = await fetch("https://movemembers.com/api/v1/offers", { headers: { Authorization: `Bearer ${appToken}` }, // scope=schedule }); const offers = await res.json();
Vérifier l'identité auprès d'un tiers, puis attribuer un groupe
Le fournisseur appelle votre serveur avec le résultat, jamais une page de navigateur. N'envoyez que le groupe que le membre a gagné, jamais de données d'identité.
Voir le guideawait fetch("https://movemembers.com/api/v1/members/1042/group", { method: "PUT", headers: { Authorization: `Bearer ${appToken}`, // scope=groups "Content-Type": "application/json", }, body: JSON.stringify({ group: "thai", reason: "ID verified" }), });
05 · Webhooks
Webhooks signés, vérifiés en quelques lignes.
member.connected, member.disconnected, member.status_changed, booking.created, booking.cancelled, payment.completed, member.group_changed. Réessayés jusqu'à 5 fois sur environ 3 heures sans réponse 2xx.
Vérifiez sur le corps BRUT, refusez tout ce qui est signé depuis plus de 5 minutes, puis dédoublonnez sur X-Webhook-Id. Le secret de signature n'est affiché qu'une fois, dans l'onglet Webhooks de l'application.
const crypto = require("node:crypto"); const TOLERANCE_SECONDS = 300; // every retry is signed again, so a real delivery is always fresh function verifyWebhook(rawBody, header, secret) { const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header); if (!m) return false; const [, timestamp, signature] = m; // Replay protection: refuse anything signed more than 5 minutes ago (or in the future). if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false; const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) // the EXACT bytes received, never re-serialized .digest("hex"); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); } // Then deduplicate on the X-Webhook-Id header: keep the id of every delivery you have processed // (for a day, retries span about 3 hours) and ignore an id you have already seen.
10 · Référence
Tous les points d'accès de l'API MoveMembers.
Du JSON partout. Rien ne se comporte différemment parce que l'appel vient de votre site.
| Moyen | Chemin | Ce qu'elle fait | Authentification |
|---|---|---|---|
| GET | /.well-known/openid-configuration | Document de découverte | none |
| GET | /oauth/jwks | Clés de signature publiques | none |
| GET | /oauth/authorize | Démarrer une connexion ou une inscription | none |
| POST | /oauth/token | Échanger un code, rafraîchir, ou obtenir un jeton d'application | identifiants de l'application |
| GET | /oauth/userinfo | Les informations du membre connecté | membre, Bearer |
| POST | /oauth/revoke | Révoquer un jeton | identifiants de l'application |
| GET | /api/v1/me | Le membre connecté | membre, Bearer |
| GET | /api/v1/me/bookings | Ses réservations et ses offres (cours restants, dates, montant dû) | membre, Bearer |
| POST | /api/v1/bookings | Réserver un cours en son nom | membre, Bearer |
| DELETE | /api/v1/bookings/{id} | Annuler une réservation | membre, Bearer |
| GET | /api/v1/members/{member_number} | Vérifier un membre par son numéro | application, scope=lookup |
| POST | /api/v1/members/verify | Vérifier ensemble un numéro de membre et une adresse e-mail | application, scope=lookup |
| GET | /api/v1/schedule | Planning public des cours | application, scope=schedule |
| GET | /api/v1/offers | Offres et produits en vente publique | application, scope=schedule |
| GET | /api/v1/groups | Groupes que cette application peut attribuer | application, scope=groups |
| PUT | /api/v1/members/{member_number}/group | Attribuer (ou retirer) un groupe à un membre | application, scope=groups |
Seuls GET /members/{member_number} et POST /members/verify fonctionnent sans le consentement du membre, et ils ne renvoient rien de plus que l'état actif ou non de l'abonnement.
12 · Limites de débit
Limites de débit, comptées par minute.
Au-delà d'une limite, l'API répond 429 rate_limited avec un en-tête Retry-After. Chaque catégorie a son propre budget : une grosse lecture n'épuise donc jamais vos réservations.
11 · Erreurs
Des erreurs, une seule forme partout.
Toute erreur /api/v1 ressemble à { "error": { "code", "message", "request_id" } }. Les points /oauth gardent la forme OAuth standard, pour que les bibliothèques OAuth fonctionnent telles quelles.
| Code | Signification |
|---|---|
| invalid_request | La requête est mal formée : un champ inconnu ou mal orthographié, un paramètre manquant, un corps trop volumineux. |
| invalid_token | Le jeton Bearer est absent, expiré ou révoqué. |
| insufficient_scope | Le jeton ne porte pas le scope dont ce point d'accès a besoin. |
| capability_off | La salle n'a pas activé cette capacité pour votre application. |
| plan_required | L'offre de la salle n'inclut pas les applications connectées pour le moment. Rien n'est supprimé ; tout revient dès qu'elle passe à une offre supérieure. |
| aucune conversation ouverte avec cette personne | Cette ressource n'existe pas (ou n'appartient pas à cette salle). |
| rate_limited | Trop de requêtes. Attendez le nombre de secondes indiqué dans l'en-tête Retry-After, puis réessayez. |
| booking_refused | Les règles de réservation ont refusé cette demande ; message explique pourquoi. Un message qui commence par membership_ signifie qu'aucune réservation n'a été créée. |
| payment_required | Cette réservation doit d'abord être payée. L'erreur porte payment_address, la page de paiement du membre. |
| no_consent | Ce membre ne s'est pas encore connecté à votre application. |
| group_not_allowed | Ce groupe n'est pas l'un de ceux que la salle a activés pour votre application. |
request_id est aussi envoyé dans l'en-tête X-Request-Id : citez-le quand vous demandez de l'aide, il retrouve votre appel dans le journal.
Questions
Questions sur l'API MoveMembers.
Construisez sur votre salle. Commencez avec votre propre compte.
Créez votre studio, enregistrez une application et une adresse de retour localhost, puis testez tout le parcours.