Home›Gymora›API
Gymora · API

Gymora API, as shipped in the download

Gymora REST API & Webhooks (v2.0)

Gymora ships a JSON REST API and signed webhooks so door systems, kiosks, class-booking sites and your own scripts can check members in, read arrears and record payments — using exactly the same rules the front desk uses.

Authentication

Create a key in Settings → API & Webhooks. A key belongs to a user and can do exactly what that user can do in the app. Send it on every call:

Authorization: Bearer apk_...

The key is shown once at creation and stored as a SHA-256 hash. Revoke any time.

A key acts as the user it belongs to: every write checks that user's role, as the screens do, and answers 403 forbidden when it does not allow the action (3.2; before, any valid key could write). Adding a member, checking someone in and recording a payment need a front-desk or admin key.

Endpoints

Method & pathWhat it does
GET /api/pingVerify a key
GET /api/membersMembers with their current membership; ?q= searches name/email/phone
POST /api/membersCreate a member (name required; email, phone, join_date, notes)
GET /api/members/{id}One member + membership, the check-in guard, recent payments and check-ins
POST /api/members/{id}/paymentsRecord a payment (amount or amount_cents; optional due_date, method, paid)
POST /api/checkinsCheck a member in (member_id, optional class_name)
GET /api/arrearsOverdue payments with the total, as of today
GET /api/openapi.jsonOpenAPI 3 spec (no auth)

Check a member in

curl -X POST https://your-install/api/checkins \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"member_id": 12, "class_name": "Vinyasa 18:00"}'

201 → {"checkin": {...}, "credits_remaining": 7}
403 → {"error": "forbidden", "detail": "Membership is frozen. Unfreeze it before checking in."}

The API is not a second implementation. Every check-in runs Membership::checkInGuard() — the same function behind the desk screen — so a frozen, canceled, expired or credit-less membership is refused identically, and detail carries the same sentence a receptionist sees.

Class credits are burned atomically. A credit-bearing pass decrements with a guarded UPDATE ... WHERE credits_remaining > 0; if two requests race for the last credit, exactly one wins and the other gets a 403. Two turnstiles cannot both spend it.

Webhooks

Add receiver URLs in Settings → API & Webhooks. Events:

  • member.checked_in — includes the member, the check-in and credits_remaining
  • checkin.denied — includes the member and the reason the guard refused
  • payment.recorded — any payment lands

Each delivery is an HTTP POST with a JSON body and:

X-Gymora-Event: member.checked_in
X-Gymora-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret>

Verify the signature before trusting a payload. Delivery is one attempt with a 5-second timeout — deliberately simple; the Recent deliveries log on the settings page shows every attempt and response code. Design receivers to be idempotent.

Both front doors fire the same events: a check-in at the desk and a check-in over the API are indistinguishable to your receiver.

Kiosk and turnstile integrations

checkin.denied carrying a human-readable reason is the point: a door controller can show "Membership expired on 2026-07-31 — renew to continue" on its own screen without re-implementing any membership rules.

Limits, honestly

  • The API acts at user level; there is no separate scope system beyond the app's roles.
  • One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
  • List endpoints cap at 200 rows; this is a gym tool, not a data warehouse.
  • Membership expiry is computed on view — there is no scheduler, so there is no "membership expired" webhook. GET /api/members reports effective_status live.

MCP — the agent endpoint (new in 3.0)

Gymora speaks MCP (Model Context Protocol) on one route, so an assistant can read the register and work the desk without anyone building a bridge first.

POST /mcp
Authorization: Bearer apk_...          ← the SAME revocable key the REST API uses
Content-Type: application/json

Stateless streamable-HTTP: one JSON-RPC message per request, no session, no SSE. A GET gets 405 with Allow: POST, as the transport requires.

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://your-install/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http gymora https://your-install/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "gymora",
  "server_url": "https://your-install/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Gymora implements the protocol, not an integration with a particular vendor.

The tools

ToolWrites?What it does
list_membersnoThe register with each member's effective membership status and whether they may check in right now — the desk's own verdict, not a second opinion.
member_detailnoOne member: membership, credits, recent visits, payments, arrears.
check_inYESRecords attendance and, on a class pack, spends one credit.
memberships_expiringnoMemberships ending within N days.
revenue_reportnoCollected in a month plus outstanding arrears, in integer cents.

What the endpoint refuses, and why

  • check_in cannot overspend a pass. It calls the same perform_checkin() the front desk, the REST API and the kiosk call — one atomic credit burn, product-wide. Twelve concurrent agent check-ins against six credits yield exactly six visits and six refusals; credits never go negative.
  • Arguments must be single values. Anything nested is rejected — the same guard Api::body() applies to the REST API.
  • Unknown arguments are refused by name. An agent that typos stauts is told so, rather than silently getting unfiltered results.
  • Out-of-enum values are refused, with the valid options listed.
  • Roles apply exactly as they do in the browser. A key belonging to a viewer is refused check_in — a different answer for the same role would make the agent path a privilege hole.

Every MCP write lands in the audit trail beside the browser and API writes, naming the key's user as the actor and recording via: mcp.

Example

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"check_in","arguments":{"member_id":42}}}'

Connect it to Zapier, Make or n8n

Gymora talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.

Before you start

  • In Gymora, open Settings → API & Webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
  • Use your install's full address, for example https://gymora.yourcompany.com.

When something happens in Gymora: send it to another tool

  1. In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
  2. Copy that URL into Gymora under Settings → API & Webhooks, and choose the event or events below.
  3. Make the event happen once in Gymora, so the other tool receives a sample and you can map its fields.
EventFires when
member.checked_inA member checks in; includes the member and the check-in
checkin.deniedIncludes the member and the reason the guard refused
payment.recordedAny payment lands

Every delivery carries X-Gymora-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.

When something happens elsewhere: act in Gymora

  • Zapier: "Webhooks by Zapier", action POST (or Custom Request).
  • Make: module HTTP → Make a request.
  • n8n: the HTTP Request node.

Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.

The endpoints that change something:

  • POST /api/members: Create a member (name required; email, phone, join_date, notes)
  • POST /api/members/{id}/payments: Record a payment (amount or amount_cents; optional due_date, method, paid)
  • POST /api/checkins: Check a member in (member_id, optional class_name)

Example recipes

  • member.checked_in → a row in Google Sheets: webhook trigger on member.checked_in (Includes the member, the check-in and), then the Google Sheets "Create row" action with the fields you want.
  • checkin.denied → a message in Slack or Teams: webhook trigger on checkin.denied (Includes the member and the reason the guard refused), then the Slack or Microsoft Teams "Send message" action.
  • A new form entry or row elsewhere → Gymora: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling POST /api/members (Create a member (name required; email, phone, join_date, notes)).

These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Gymora's own automated tests check its API.

← Back to Gymora · Manual · Quickstart · Test run