SnapMindDevelopers
Get an API key

SnapMind API

Connect your gym's own systems to SnapMind Training. Pull clubs, staff and weekly totals into a data warehouse, add staff from an HR system, or react the moment a client finishes a session.

Overview

The API is REST over HTTPS and speaks JSON. Every request is scoped to the company that owns the key, so one key never sees another company's data.

https://usesnapmind.com/api/v1

Keys and auth

Owners and admins create keys in the company console under Developers, API keys. A key is shown once, in full, when it is created. SnapMind keeps only a one-way hash of it.

Send the key as a bearer token:

curl https://usesnapmind.com/api/v1/company \
  -H "Authorization: Bearer smk_live_..."
ScopeAllows
readEvery GET endpoint.
writeRead, plus inviting staff.

Create one key per system that connects. You can then see each one's usage in the console and revoke it on its own. Revoking takes effect on the next request.

Limits and errors

Each key can make 120 requests a minute. Every response carries X-RateLimit-Remaining and X-Request-Id; include the request id when you contact support.

Errors return a status code and a body you can show to a person:

{
  "error": {
    "code": "invalid_request",
    "message": "limit must be between 1 and 500."
  }
}
StatusCodeMeaning
400invalid_requestA parameter is missing or malformed. The message says which.
401invalid_keyThe key is missing, wrong or revoked.
403missing_scopeThe key cannot do this. Writing needs the write scope.
404not_foundNo such endpoint.
422refusedThe request was understood and refused, for example inviting someone already on staff.
429rate_limitedWait for the number of seconds in Retry-After.
500server_errorOur fault. Retry with backoff.

Paging

List endpoints return up to limit rows (default 100, at most 500), sorted by id. When has_more is true, pass next_cursor as after to get the next page.

{
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "8f2c1e9a-..."
}

What the API never sends

Clients are ids. The API, webhooks and exports carry a client's id, club, trainer, status and session counts. They never carry a client's name, email, messages, check-in answers or notes. Those stay between the client and their trainer, the same rule owners see in the dashboard.

Company

GET/company

Your company and its headline counts.

{
  "id": "3ea82c7e-...",
  "name": "Northside Performance",
  "logo_url": "https://...",
  "accent_color": "#f97316",
  "clubs": 3,
  "staff": 4,
  "clients": 14,
  "created_at": "2026-10-27T18:31:02Z"
}

Clubs

GET/clubs

Every club, open and closed. Parameters: limit, after.

{
  "data": [{
    "id": "b5287970-...",
    "name": "Downtown",
    "city": "Austin",
    "timezone": null,
    "region": { "id": "fcc9e224-...", "name": "Central Austin" },
    "status": "open",
    "created_at": "2026-10-27T18:40:11Z"
  }],
  "has_more": false,
  "next_cursor": null
}

Staff

GET/staff

Everyone on staff. Parameters: club_id, limit, after. role is one of owner, admin, regional, manager or trainer; status is active or removed.

{
  "data": [{
    "id": "330dca66-...",
    "name": "Coach Priya Shah",
    "email": "priya@northside.example",
    "role": "manager",
    "club_id": "b5287970-...",
    "region_id": null,
    "coaches": true,
    "status": "active",
    "joined_at": "2026-10-27T18:41:55Z"
  }],
  "has_more": false,
  "next_cursor": null
}

Clients

GET/clients

Every client at the company, as ids. Parameters: club_id, limit, after. status is onboarding, active, paused or complete.

{
  "data": [{
    "id": "7a1d...",
    "club_id": "b5287970-...",
    "trainer_id": "330dca66-...",
    "status": "active",
    "with_trainer_since": "2026-08-13T15:02:44Z"
  }],
  "has_more": false,
  "next_cursor": null
}

Weekly totals

GET/totals

The week's numbers for the whole company, one club or one trainer. Parameters: week (any date in the week, YYYY-MM-DD, default this week), club_id, trainer_id. Weeks start on Monday.

curl "https://usesnapmind.com/api/v1/totals?week=2026-10-26&club_id=b5287970-..." \
  -H "Authorization: Bearer smk_live_..."

{
  "week_start": "2026-10-26",
  "club_id": "b5287970-...",
  "trainer_id": null,
  "clients": 8,
  "clients_active": 7,
  "sessions_planned": 30,
  "sessions_done": 13,
  "checkins": 3
}

Invite staff

POST/invites

Invites someone to your staff, with the same checks as the console. Needs a key with the write scope. They join the next time they sign in with that email; if they already have a SnapMind account and coach nobody yet, they join at once (joined: true).

FieldNotes
emailRequired. The email they will sign in with.
nameOptional.
roletrainer (default), manager, regional or admin.
club_idRequired for trainers and managers when the company has clubs.
region_idRequired for regional managers.
curl -X POST https://usesnapmind.com/api/v1/invites \
  -H "Authorization: Bearer smk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"jordan@northside.example","name":"Jordan Lee","role":"trainer","club_id":"b5287970-..."}'

201
{
  "id": "e41c...",
  "email": "jordan@northside.example",
  "role": "trainer",
  "joined": false,
  "has_account": false
}

Webhooks: how delivery works

Add a webhook in the company console under Developers, Webhooks: an https address and the events you want. SnapMind sends a POST with a JSON body the moment the event happens, usually within a minute.

Answer with any 2xx status within 10 seconds. Anything else is retried after 1, 2, 4, 8 minutes and onward, up to 6 hours apart and 8 tries in all. The console shows every delivery, lets you send one again, and sends test events. A webhook that fails 50 deliveries in a row is switched off, and the console says why.

Deliveries can arrive more than once and out of order. Use the event id to ignore repeats.

POST /your/address
Content-Type: application/json
SnapMind-Event: session.completed
SnapMind-Delivery: 9b0d...
SnapMind-Signature: t=1761591000,v1=5f2a...

{
  "id": "evt_4c1f...",
  "type": "session.completed",
  "created_at": "2026-10-27T19:30:00Z",
  "company_id": "3ea82c7e-...",
  "data": {
    "client_id": "7a1d...",
    "club_id": "b5287970-...",
    "trainer_id": "330dca66-...",
    "session_id": "c90e...",
    "scheduled_date": "2026-10-27",
    "kind": "movement"
  }
}

Events

EventSent whendata
client.joinedA client starts with a trainer at the companyclient_id, club_id, trainer_id
client.leftA client leaves the companyclient_id, club_id
client.movedA client changes trainer or clubclient_id, from, to
session.completedA client finishes a sessionclient_id, club_id, trainer_id, session_id, scheduled_date, kind
checkin.submittedA client sends their weekly check-inclient_id, club_id, trainer_id, checkin_id, week_start
staff.joinedSomeone joins the staffstaff_id, role, club_id, region_id, coaches
staff.removedSomeone leaves the staffstaff_id, role, club_id
staff.changedA staff member's role or club changesstaff_id, role, club_id, previous
webhook.testYou press Send a testmessage

Verifying signatures

Each webhook has its own secret, starting whsec_, shown in the console. The SnapMind-Signature header holds a timestamp t and v1, the hex HMAC-SHA256 of t + "." + raw body using that secret. Compare in constant time, and refuse timestamps more than five minutes old.

Node.js

const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ''));
}

Python

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Use the raw body exactly as received. Parsing and re-serializing the JSON first changes the bytes and the signature will not match.

SCIM provisioning

Okta, Microsoft Entra ID and other identity providers can add and remove staff automatically. Create a key with Automatic staff accounts (SCIM) in the company console, then in your provider's SnapMind app:

Base URLhttps://usesnapmind.com/scim/v2
AuthenticationBearer token: the SCIM key
Unique identifieruserName, the staff member's work email
SupportedCreate, read, filter by userName eq, update (PUT and PATCH), deactivate, delete. Groups are not used.

A new user gets an invite as a trainer, at the club whose name matches the enterprise department attribute (or the only club). Setting active to false or deleting the user removes them from your staff; their clients stay with your company and move to a coach at their club. Roles beyond trainer are set in SnapMind.

Single sign-on

Staff can sign in through your SAML provider at usesnapmind.com/sso. Owners and admins set it up in the company console under Security, which lists the Entity ID and ACS URL to enter in Okta, Microsoft Entra ID or Google Workspace. Anyone your provider lets in with one of your email domains joins your company as a trainer the first time they sign in.

Questions or a missing endpoint: michael.donovan@usesnapmind.com.