Commencer gratuitement

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.

OpenID ConnectPKCE obligatoireWebhooks signés
Découverte OpenID
$ 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"]
}
200 OK
application/json · 84 ms
PKCE

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.

01
Ajoutez le bouton

Envoyez le membre vers la page de connexion de sa salle. PKCE est obligatoire pour toute application, confidentielle ou publique.

Demande d'autorisation
<!-- "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>
02
Échanger le code sur votre serveur

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.

Exemple de code
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();
03
Lire les informations du jeton

Uniquement ce que la salle a vérifié et que le membre a approuvé. Rattachez votre fiche à « sub », jamais à l'adresse e-mail.

Exemple de code
{
  "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.

API 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 guide
API request
GET /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 guide
API request
const 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 guide
API request
const 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 guide
API request
await 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.

Signature du webhook
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.

MoyenCheminCe qu'elle faitAuthentification
GET/.well-known/openid-configurationDocument de découvertenone
GET/oauth/jwksClés de signature publiquesnone
GET/oauth/authorizeDémarrer une connexion ou une inscriptionnone
POST/oauth/tokenÉchanger un code, rafraîchir, ou obtenir un jeton d'applicationidentifiants de l'application
GET/oauth/userinfoLes informations du membre connectémembre, Bearer
POST/oauth/revokeRévoquer un jetonidentifiants de l'application
GET/api/v1/meLe membre connectémembre, Bearer
GET/api/v1/me/bookingsSes réservations et ses offres (cours restants, dates, montant dû)membre, Bearer
POST/api/v1/bookingsRéserver un cours en son nommembre, Bearer
DELETE/api/v1/bookings/{id}Annuler une réservationmembre, Bearer
GET/api/v1/members/{member_number}Vérifier un membre par son numéroapplication, scope=lookup
POST/api/v1/members/verifyVérifier ensemble un numéro de membre et une adresse e-mailapplication, scope=lookup
GET/api/v1/schedulePlanning public des coursapplication, scope=schedule
GET/api/v1/offersOffres et produits en vente publiqueapplication, scope=schedule
GET/api/v1/groupsGroupes que cette application peut attribuerapplication, scope=groups
PUT/api/v1/members/{member_number}/groupAttribuer (ou retirer) un groupe à un membreapplication, 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.

300appels de lecture par application / min/me, /me/bookings, /schedule, /offers, /groups
120appels d'écriture par application / minréservations et groupes
120vérifications de membre par application / minun budget partagé entre lookup et verify
600appels par minute sur /api/v1partagés par toutes les applications d'une même salle

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.

CodeSignification
invalid_requestLa requête est mal formée : un champ inconnu ou mal orthographié, un paramètre manquant, un corps trop volumineux.
invalid_tokenLe jeton Bearer est absent, expiré ou révoqué.
insufficient_scopeLe jeton ne porte pas le scope dont ce point d'accès a besoin.
capability_offLa salle n'a pas activé cette capacité pour votre application.
plan_requiredL'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 personneCette ressource n'existe pas (ou n'appartient pas à cette salle).
rate_limitedTrop de requêtes. Attendez le nombre de secondes indiqué dans l'en-tête Retry-After, puis réessayez.
booking_refusedLes 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_requiredCette réservation doit d'abord être payée. L'erreur porte payment_address, la page de paiement du membre.
no_consentCe membre ne s'est pas encore connecté à votre application.
group_not_allowedCe 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.

OpenID Connect standard avec PKCE, obligatoire pour toute application, confidentielle ou publique. Toute bibliothèque cliente OIDC certifiée convient, et le document de découverte se trouve à /.well-known/openid-configuration.
Non. L'écran de connexion est sauté tant qu'ils restent connectés à la salle sur cet appareil, et l'approbation n'est demandée qu'une fois par membre et par application. Évitez prompt=login, prompt=consent et max_age sauf nécessité réelle.
À « sub ». Il ne change jamais pour ce membre et cette application, même s'il change d'e-mail. Traitez « email_verified »: false comme non vérifié.
Non. Cela prouve que quelqu'un les connaît, pas que c'est le membre. Pour être sûr qu'il s'agit du titulaire du compte, utilisez le bouton de connexion et lisez son « sub ».
La réservation répond booking_refused avec un message qui commence par membership_, et aucune réservation n'est créée. Lisez GET /me/bookings et envoyez le membre acheter une offre.
member.connected, member.disconnected, member.status_changed, booking.created, booking.cancelled, payment.completed et member.group_changed, livrés sous forme de POST signés.
Enregistrez http://localhost (n'importe quel port) comme adresse de retour : c'est la seule adresse acceptée en http simple. Ajoutez votre vraie adresse de production avant la mise en ligne.

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.

14 days of Max freeSans carte bancaireAucun appel commercial