Ownware
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.

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, cancelled, 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"
}

Own Your AI reads a list of servers in this shape:

{
  "mcpServers": [
    { "id": "gymora", "name": "Gymora", "url": "https://your-install/mcp",
      "token": "apk_xxxx", "enabled": true }
  ]
}

**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"* — not an mcpServers array. Pasted as-is, the block above 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}}}'

← Back to Gymora · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →