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_..."
| Scope | Allows |
|---|---|
| read | Every GET endpoint. |
| write | Read, 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."
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A parameter is missing or malformed. The message says which. |
| 401 | invalid_key | The key is missing, wrong or revoked. |
| 403 | missing_scope | The key cannot do this. Writing needs the write scope. |
| 404 | not_found | No such endpoint. |
| 422 | refused | The request was understood and refused, for example inviting someone already on staff. |
| 429 | rate_limited | Wait for the number of seconds in Retry-After. |
| 500 | server_error | Our 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
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).
| Field | Notes |
|---|---|
| Required. The email they will sign in with. | |
| name | Optional. |
| role | trainer (default), manager, regional or admin. |
| club_id | Required for trainers and managers when the company has clubs. |
| region_id | Required 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
| Event | Sent when | data |
|---|---|---|
| client.joined | A client starts with a trainer at the company | client_id, club_id, trainer_id |
| client.left | A client leaves the company | client_id, club_id |
| client.moved | A client changes trainer or club | client_id, from, to |
| session.completed | A client finishes a session | client_id, club_id, trainer_id, session_id, scheduled_date, kind |
| checkin.submitted | A client sends their weekly check-in | client_id, club_id, trainer_id, checkin_id, week_start |
| staff.joined | Someone joins the staff | staff_id, role, club_id, region_id, coaches |
| staff.removed | Someone leaves the staff | staff_id, role, club_id |
| staff.changed | A staff member's role or club changes | staff_id, role, club_id, previous |
| webhook.test | You press Send a test | message |
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 URL | https://usesnapmind.com/scim/v2 |
| Authentication | Bearer token: the SCIM key |
| Unique identifier | userName, the staff member's work email |
| Supported | Create, 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.
SnapMind