Connect your site to your gym
Members sign in with their your gym account. Your site gets a signed answer with their member number. Standard OpenID Connect, so any OIDC library works.
Add the button
Send the member to their gym's sign-in page. PKCE is required for every app, confidential or public.
Exchange the code on your server
You get an ID token signed by the gym, and the member's details. Never do this exchange from a page: it needs your client secret.
Node.js (openid-client, a certified OIDC client library):
PHP:
The ID token is signed with ES256 and verified against the gym's public keys, published at:
Read the claims
The ID token (and GET /oauth/userinfo with the access token) carry only what the gym has checked and the member has approved — never more.
Link your own record to "sub", never to the email address: "sub" never changes for this member and this app, even if they change their email. Check the status later with GET /api/v1/members/1042.
"email_verified" is true only once the member has signed in with a code at least once; treat "false" as unverified and never trust the email alone until then.
Check a member by number
For a server-to-server check that doesn't need the member present: get an app token with client_credentials, then look up a member by number. This never requires the member's consent, and returns exactly two fields.
Get an app token
Look up the member
Nothing else comes back here, whatever the app's other capabilities: this is the one endpoint that works without the member's consent.
Webhooks
member.connected, member.disconnected, member.status_changed, booking.created, booking.cancelled, payment.completed, member.group_changed. Delivered as a signed POST, retried up to 5 times over about 3 hours if your endpoint doesn't answer with a 2xx.
Verify the signature
Every delivery carries three headers: X-Webhook-Signature (t=<timestamp>,v1=<hmac>), X-Webhook-Id (unique per attempt, for deduplication) and X-Webhook-Timestamp. Verify on the RAW body you received, never a re-serialized one: reordering keys changes the signature.
The signing secret is shown once, in the app's Webhooks tab, right when you add the address. Store it then: it can't be shown again, only rotated.
Read the event
Recipe: link a record on your site to a member
Any site that owns its own records — a dog, a locker, a car, a booking of its own — can attach them to a gym member without ever storing their password.
The member signs in
They tap "Join Your Gym", sign in (or sign up) at the gym, and approve what your app asks for.
Your server gets sub
You exchange the code for an ID token, and read "sub" (a stable id for this member and this app, e.g. "mbr_4Hq9kR2vLpXsT8nZ").
Store sub as a foreign key
Save "sub" on your own record — the dog, the locker, the car — never the email address, which can change.
Re-check later
Book a class
List what's on the schedule, then book or cancel on the member's behalf, with their access token.
List the schedule
Book a class
A refused booking is not an error to log and hide: "booking_refused" always carries the reason a member would recognize (a full class, a membership issue, a closed window). Show error.message to them, don't swallow it.
Cancel a booking
Show your schedule and prices
Public information, no member sign-in needed: perfect for a site's own class timetable or pricing page.
The public schedule
Plans on public sale
Recipe: verify identity with a third party, then set a group
A common shape when a gym needs proof of something (a national ID, an age, a certification) before selling certain plans: the check happens entirely outside this API.
The member signs in
You get "sub" the usual way (quick start, steps 1–3).
Verify with a third party
Send the member to a verification provider of your choice, joined with your own "sub" for this member so you know whose result comes back.
Receive the decision on your server
The provider calls YOUR server's webhook with the result. Never read a verification decision sent from a browser page: a page can be edited by whoever is looking at it, so a client-side "verified: true" proves nothing.
Set the group
Never send identity data (name, ID number, photo, date of birth) to this API. Send only the group the member has earned; the verification, and everything it was based on, stays entirely on your side.
All endpoints
JSON everywhere. Every write endpoint calls the exact same rules as the gym's own apps (booking windows, waitlists, plan checks): nothing behaves differently because it came from your site.
| Method | Path | What it does | Auth |
|---|---|---|---|
| GET | /.well-known/openid-configuration | Discovery document | — |
| GET | /oauth/jwks | Public signing keys | — |
| GET | /oauth/authorize | Start a sign-in or sign-up | — |
| POST | /oauth/token | Exchange a code, refresh, or get an app token | app credentials |
| GET | /oauth/userinfo | The signed-in member's claims | member, Bearer |
| POST | /oauth/revoke | Revoke a token | app credentials |
| GET | /api/v1/me | The signed-in member | member, Bearer |
| GET | /api/v1/me/bookings | Their bookings | member, Bearer |
| POST | /api/v1/bookings | Book a class on their behalf | member, Bearer |
| DELETE | /api/v1/bookings/{id} | Cancel a booking | member, Bearer |
| GET | /api/v1/members/{member_number} | Check a member by number | app, scope=lookup |
| GET | /api/v1/schedule | Public class schedule | app, scope=schedule |
| GET | /api/v1/offers | Plans and products on public sale | app, scope=schedule |
| GET | /api/v1/groups | Groups this app can set | app, scope=groups |
| PUT | /api/v1/members/{member_number}/group | Set (or clear) a member's group | app, scope=groups |
Errors
Every error looks the same: { "error": { "code", "message" } }.
| Code | Meaning |
|---|---|
| invalid_token | The bearer token is missing, expired, or revoked. |
| insufficient_scope | The token doesn't carry the scope this endpoint needs. |
| capability_off | The gym hasn't enabled this capability for your app. |
| plan_required | The gym's plan doesn't include connected apps right now. Nothing is deleted; it comes back the moment they upgrade. |
| not_found | That resource doesn't exist (or doesn't belong to this gym). |
| rate_limited | Too many requests. Back off and retry. |
| booking_refused | The booking rules refused this request; message explains why (a membership check, a booking window, a full class…). |
| payment_required | This booking needs to be paid before the seat is confirmed. payment_address points to the member's payment page. |
| no_consent | That member hasn't connected to your app yet. |
| group_not_allowed | That group isn't one the gym enabled for your app. |
Test with your own account
Register http://localhost (with any port) as one of your return addresses while you build: it's the one address the gym accepts over plain http, exactly like every other return address in this system. Add your real production address before going live; the localhost one can stay for the next time you need to test.