Roll Call API
Connect automation tools to your gym. The Roll Call API serves narrow, read-only projections of gym facts and accepts structured commands— requests for action that Roll Call validates, applies through the same code paths its own dashboard uses, and may reject. It never exposes raw records, payment instruments, biometric data, or another gym's members.
Basics
| Base URL | https://api.rollcallaccess.com/v1 |
| Auth | Authorization: Bearer <token>— a scoped key minted from your gym's Settings page |
| Versioning | Path-versioned (/v1). Additive changes don't bump; breaking changes ship as /v2 alongside. |
| Tenancy | Every key is bound to one gym. Every response is scoped to it. |
Every read response uses one envelope — the payload plus explicit freshness:
{
"data": { /* endpoint-specific */ },
"meta": {
"asOf": "2026-08-04T09:30:00+10:00", // when Roll Call computed this projection
"requestId": "req_01J4X8ZK9M" // echoed in logs on both sides
}
}Authentication & keys
Keys are minted (and revoked) by the gym owner at owner.rollcallaccess.com/settings → API access. A key is shown once at creation and stored hashed — treat it like a password. Each key carries scopes:
| Scope | Grants |
|---|---|
read | The read projections below |
command | POST /commands |
A missing or revoked key is 401; a valid key without the needed scope, or one bound to a different gym, is 403.
Read endpoints
Look up a member by email
/members/lookup?email=<urlencoded>scope: readCase-insensitive exact match on the member's contact email — never a search or list. Returns the member-summary projection, or 404 when no member of your gym has that email.
{
"data": {
"memberId": "0b2f6d6e-…",
"displayName": "Rocco Talevski",
"status": "active", // active | trial | grace_period | overdue |
// suspended | cancelled | staff | guest | pending
"statusReason": null,
"statusChangedAt": "2026-07-01T10:00:00+10:00",
"joinedAt": "2025-11-14T10:00:00+11:00",
"isMinor": false,
"household": null, // or { "householdId": "…", "role": "guardian|junior|spectator" }
"flags": { "hasOpenFollowUp": false, "onHold": false }
},
"meta": { "asOf": "2026-08-04T09:30:00+10:00", "requestId": "req_…" }
}Member summary
/members/:memberIdscope: readThe same projection as the lookup, keyed by member id. flags.onHold reflects an active membership hold; flags.hasOpenFollowUp means staff already have an open task for this member.
Membership status
/members/:memberId/membershipscope: readThe member's current plan and hold state. No price instruments, no processor payloads.
{
"data": {
"plan": {
"name": "Adults Unlimited",
"priceCents": 4500,
"cadence": "weekly", // weekly | fortnightly | monthly | one_off | per_visit
"planType": "recurring" // recurring | one_off | class_pack | casual | prepaid_term | comp
},
"startedAt": "2025-11-14T10:00:00+11:00",
"currentPeriodEnd": "2026-08-11T00:00:00+10:00", // null when open-ended
"billingRail": "stripe_owned",
"hold": { "active": false, "resumesAt": null },
"revenueClass": "recurring"
},
"meta": { "asOf": "2026-08-04T09:30:00+10:00", "requestId": "req_…" }
}GymMaster check (temporary)
/members/:memberId/membership/gymmasterscope: readTemporary endpoint. Some memberships are still billed through GymMaster during the migration off it, and those cannot be paused through this API (the pause command rejects them with rail_not_pausable). This gives you the one-boolean check. It will be removed — with notice in the changelog — once no membership is GymMaster-billed; the same fact is permanently available as billingRail on the membership read, so prefer that where convenient.
{
"data": {
"isGymMasterBilled": true, // true = cannot be paused through this API
"billingRail": "gymmaster"
},
"meta": { "asOf": "2026-08-04T09:30:00+10:00", "requestId": "req_…" }
}Pause options
/members/:memberId/membership/pause-optionsscope: readAnswers "could a pause succeed, and within what limits?" before you send the command — computed from the same rule code the command uses, so it never disagrees with a real attempt. Every gym sets its own hold rules per plan (minimum/maximum length, a per-term day cap, whether a pause extends the contract); check this first and only propose pauses that fit.
{
"data": {
"canPause": true,
"reason": null, // blocking reason code when canPause = false
"rules": {
"minDays": 7, // null = no minimum
"maxDays": 28, // null = no maximum
"capDaysPerTerm": 30, // null = uncapped
"usedDaysThisTerm": 0,
"remainingDaysThisTerm": 30, // null = unlimited
"noticeDays": 0,
"extendsContract": true // a pause pushes the contract end out
},
"currentHold": { "active": false, "resumesAt": null },
"term": { "start": "2026-07-01T00:00:00+10:00", "end": "2027-07-01T00:00:00+10:00" }
},
"meta": { "asOf": "2026-08-04T09:30:00+10:00", "requestId": "req_…" }
}When canPause is false, reason carries the same code the command would reject with (see the table below).
Attendance history & trend
/members/:memberId/attendancescope: readRecent visits plus an 8-week trend. All counts are distinct training days (two classes in one day count once — the same unit the member sees), over confirmed attendance only.
{
"data": {
"recent": [
{ "sessionId": "6f2a…", "className": "No-Gi Fundamentals",
"attendedAt": "2026-08-04T18:00:00+10:00" }
],
"weeklyCounts": [2, 3, 3, 2, 3, 1, 2, 3], // distinct training days/week, most recent first
"trend": "stable", // stable | improving | declining | insufficient_data
"lastAttendedAt": "2026-08-04T18:00:00+10:00",
"lifetimeVisits": 148 // distinct training DAYS
},
"meta": { "asOf": "2026-08-05T09:30:00+10:00", "requestId": "req_…" }
}Payment state (observed)
/members/:memberId/paymentsscope: readObserved state only — enough to reply to a member intelligently, never amounts, invoices, or payment instruments. dishonoured means a debit bounced in the last 14 days; arrearsfollows the member's status ladder.
{
"data": {
"state": "clear", // clear | arrears | dishonoured | unknown
"lastObservedAt": "2026-08-01T10:00:00+10:00",
"rail": "stripe_owned",
"lastDishonourAt": null,
"escalation": null, // or { "memberStatus": "overdue", "graceEntersAt": "…" }
"openFollowUp": null // or { "kind": "dishonour", "createdAt": "…" }
},
"meta": { "asOf": "2026-08-05T09:30:00+10:00", "requestId": "req_…" }
}Lead & trial status
/members/:memberId/trialscope: readWhere this person sits in the funnel: are they a lead, has a trial started, did they show up, did they convert — plus their next booking.
{
"data": {
"memberId": "0b2f6d6e-…",
"isTrialLead": true,
"source": "facebook", // lead channel; "direct" when unknown
"createdAt": "2026-07-28T12:00:00+10:00",
"trial": { // null until a trial plan is attached
"startedAt": "2026-07-29T09:00:00+10:00",
"endsAt": "2026-08-05T09:00:00+10:00",
"attendedCount": 2, // distinct training days since the trial started
"outcome": "in_progress" // in_progress | converted | trial_ended | cancelled
},
"booking": { // next upcoming (or most recent) booking, or null
"bookingId": "…", "sessionId": "…", "className": "No-Gi Fundamentals",
"startsAt": "2026-08-06T18:00:00+10:00", "status": "booked"
}
},
"meta": { "asOf": "2026-08-05T09:30:00+10:00", "requestId": "req_…" }
}Gym-level reads
Timetable
/timetable?days=7scope: readUpcoming sessions with live seat counts (computed from bookings at request time, never a cached number). days is optional: 1–28, default 7.
{
"data": {
"sessions": [
{
"sessionId": "6f2a…",
"name": "No-Gi Fundamentals",
"room": "Mat 1",
"audience": "adult", // kids | adult | any
"startsAt": "2026-08-06T18:00:00+10:00",
"endsAt": "2026-08-06T19:00:00+10:00",
"coach": "Sam",
"capacity": 20, // null = unlimited
"booked": 14, // LIVE booking counts, never a cached mirror
"waitlisted": 0,
"seatsLeft": 6
}
]
},
"meta": { "asOf": "2026-08-05T09:30:00+10:00", "requestId": "req_…" }
}Single class session
/classes/:sessionIdscope: readOne session in the same shape as the timetable entries — use it to re-check seats just before proposing a booking.
Staff
/staffscope: readActive coaches, the class types they're assigned to, and their session load over the next 7 days. No pay or contact data. Availability is honestly unknown until Roll Call tracks it.
{
"data": {
"staff": [
{
"coachId": "…", "memberId": "…",
"name": "Sam", "role": "head_coach", // coach | head_coach
"assignedClassTypes": ["Gi", "No-Gi"],
"upcomingSessions": 6, // next 7 days
"availability": { "state": "unknown", "note": null }
}
]
},
"meta": { "asOf": "2026-08-05T09:30:00+10:00", "requestId": "req_…" }
}Funnel stats
/stats/funnel?days=30scope: readLead → trial → conversion counts over a window (days 1–365, default 30), plus cancellations and a member-count breakdown. Aggregates only — no member identities. leads.created counts trial leads and enquiries created in the window.
{
"data": {
"windowDays": 30,
"leads": { "created": 8 },
"trials": { "started": 8, "attendedAtLeastOnce": 1, "converted": 0,
"trialEnded": 8, "inProgress": 0 },
"trialConversionRate": 0.0,
"cancellations": { "count": 1, "byReason": [] },
"memberCounts": { "active": 191, "grace_period": 74, "overdue": 17 }
},
"meta": { "asOf": "2026-08-05T09:30:00+10:00", "requestId": "req_…" }
}Commands
One write surface. You request; Roll Call authorises against its own rules, applies through its own single-writer appliers, audits, and may reject. A rejection is a normal, expected outcome (200 with status: "rejected"and a machine-readable reason) — surface it to a human, don't retry it.
request_membership_pause
/commandsscope: commandPauses (holds) a membership until resumeAt, via the same hold path a staff member uses in the dashboard. The Idempotency-Key header is required: replaying the same key returns the original receipt and applies nothing twice; the same key with a different body is 409.
POST /v1/commands
Authorization: Bearer <token>
Idempotency-Key: pause:0b2f6d6e:2026-09-07
{
"command": {
"type": "request_membership_pause",
"payload": {
"memberId": "0b2f6d6e-…",
"resumeAt": "2026-09-07T00:00:00+10:00",
"reason": "Member-requested pause (4 weeks), approved in Operator."
}
},
"context": {
"correlationId": "wfr_01J4…",
"approvalRef": "apr_01J4…", // your approval id — audit context only
"requestedBy": "operator"
}
}{
"data": {
"commandId": "rcmd_01J4…",
"status": "applied", // accepted | applied | rejected
"reason": null, // machine-readable code when rejected
"detail": null,
"appliedAt": "2026-08-04T09:30:05+10:00"
},
"meta": { "asOf": "2026-08-04T09:30:05+10:00", "requestId": "req_…" }
}Check pause options first — the gym's own hold rules (length limits, a per-term day cap, billing rail) decide whether a pause is allowed, and the command re-validates every one of them. Rejection reasons you should expect and handle:
| reason | Meaning |
|---|---|
member_not_found | No such member in your gym |
policy_member_not_active | Member's status doesn't allow a pause (e.g. cancelled) |
no_active_membership | Member has no membership (or no plan) to hold |
already_on_hold | The membership is already paused |
invalid_resume_date | resumeAt is in the past or unreasonably far out |
rail_not_pausable | The membership is billed on a rail Roll Call can't pause (pause it at the source) |
hold_limit_exceeded | The plan's hold rules refuse this pause (cap / min / max days) — detail carries the message |
billing_pause_failed | Payment collection couldn't be paused, so nothing was changed |
internal_error | Something unexpected — nothing was changed; safe to retry with a new key after review |
Supported command types: request_membership_pause, request_member_note, request_trial_booking, request_cancellation_review (each documented below). Any other command.type returns 422 unsupported — new types appear here first.
request_member_note
/commandsscope: commandAppends a note to the member's timeline so gym staff see what your software did ("replied to pause request, approved by Sam"). It never edits member fields — the lowest-risk command, and the one to send after most automated actions. Payload: { memberId, note, kind: "operator_observation" }. Receipt detail carries timelineEventId.
request_trial_booking
/commandsscope: commandBooks a member into a class session through Roll Call's own booking engine — capacity locking, waitlist rules and audience gates all apply. Payload: { memberId, sessionId }. A full class comes back rejected / booking_full; a successful waitlist is applied with detail.bookingStatus: "waitlisted"; a repeat request for the same member+session is applied with detail.alreadyBooked. Other rejection codes: bookings_not_enabled, session_not_found, class_already_started, audience_mismatch, already_attended.
request_cancellation_review
/commandsscope: commandA member wants to cancel? This creates an open follow-up task for gym staff — your software never cancels anyone. Payload: { memberId, reason, evidenceSummary }. Receipt detail carries followUpId.
Errors
| Status | Meaning | Retry? |
|---|---|---|
| 401 | Missing, invalid, or revoked key | No |
| 403 | Valid key, wrong scope or wrong gym | No |
| 404 | Unknown member / resource | No |
| 409 | Idempotency-Key reused with a different body | No |
| 422 | Unsupported command type or invalid payload | No |
| 429 | Rate limited — honour Retry-After | After delay |
| 503 | API not configured / dependency down | Yes |
GETs are safe to retry. Commands are only safe to retry by re-sending the same Idempotency-Key.
Changelog
| Date | Change |
|---|---|
| 2026-08-05 | Big expansion: timetable, single class, staff, funnel stats, member attendance, payment state, and lead/trial status reads; plus three new commands — request_member_note, request_trial_booking, request_cancellation_review. |
| 2026-08-04 | New base URL: https://api.rollcallaccess.com/v1. The previous Railway address keeps working indefinitely — no action needed for existing integrations. |
| 2026-08-04 | Added GET /members/:id/membership/gymmaster — a TEMPORARY check for GymMaster-billed (unpausable) memberships; will be removed with changelog notice after the GymMaster migration completes. |
| 2026-08-04 | Base path moved to /v1 (was /operator/v1) and scopes simplified to read / command — before any consumer connected. Added GET /members/:id/membership/pause-options. |
| 2026-08-04 | v1 launch: member lookup by email, member summary, membership status, and the request_membership_pause command. |
Questions or need an endpoint that isn't here? admin@rollcallaccess.com.