Developers

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 URLhttps://api.rollcallaccess.com/v1
AuthAuthorization: Bearer <token>— a scoped key minted from your gym's Settings page
VersioningPath-versioned (/v1). Additive changes don't bump; breaking changes ship as /v2 alongside.
TenancyEvery key is bound to one gym. Every response is scoped to it.

Every read response uses one envelope — the payload plus explicit freshness:

Response envelope
{
  "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:

ScopeGrants
readThe read projections below
commandPOST /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

GET/members/lookup?email=<urlencoded>scope: read

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

200 — member summary
{
  "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

GET/members/:memberIdscope: read

The 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

GET/members/:memberId/membershipscope: read

The member's current plan and hold state. No price instruments, no processor payloads.

200 — membership status
{
  "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)

GET/members/:memberId/membership/gymmasterscope: read

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

200 — GymMaster check
{
  "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

GET/members/:memberId/membership/pause-optionsscope: read

Answers "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.

200 — pause options
{
  "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

GET/members/:memberId/attendancescope: read

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

200 — attendance
{
  "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)

GET/members/:memberId/paymentsscope: read

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

200 — payment state
{
  "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

GET/members/:memberId/trialscope: read

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

200 — trial status
{
  "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

GET/timetable?days=7scope: read

Upcoming sessions with live seat counts (computed from bookings at request time, never a cached number). days is optional: 1–28, default 7.

200 — timetable
{
  "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

GET/classes/:sessionIdscope: read

One session in the same shape as the timetable entries — use it to re-check seats just before proposing a booking.

Staff

GET/staffscope: read

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

200 — staff
{
  "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

GET/stats/funnel?days=30scope: read

Lead → 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.

200 — funnel
{
  "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

POST/commandsscope: command

Pauses (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.

Request
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"
  }
}
200 — receipt
{
  "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:

reasonMeaning
member_not_foundNo such member in your gym
policy_member_not_activeMember's status doesn't allow a pause (e.g. cancelled)
no_active_membershipMember has no membership (or no plan) to hold
already_on_holdThe membership is already paused
invalid_resume_dateresumeAt is in the past or unreasonably far out
rail_not_pausableThe membership is billed on a rail Roll Call can't pause (pause it at the source)
hold_limit_exceededThe plan's hold rules refuse this pause (cap / min / max days) — detail carries the message
billing_pause_failedPayment collection couldn't be paused, so nothing was changed
internal_errorSomething 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

POST/commandsscope: command

Appends 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

POST/commandsscope: command

Books 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

POST/commandsscope: command

A 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

StatusMeaningRetry?
401Missing, invalid, or revoked keyNo
403Valid key, wrong scope or wrong gymNo
404Unknown member / resourceNo
409Idempotency-Key reused with a different bodyNo
422Unsupported command type or invalid payloadNo
429Rate limited — honour Retry-AfterAfter delay
503API not configured / dependency downYes

GETs are safe to retry. Commands are only safe to retry by re-sending the same Idempotency-Key.

Changelog

DateChange
2026-08-05Big 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-04New base URL: https://api.rollcallaccess.com/v1. The previous Railway address keeps working indefinitely — no action needed for existing integrations.
2026-08-04Added 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-04Base 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-04v1 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.