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
    "source": "live",                     // computed at request time, never a cached mirror
    "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 | current | 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", "source": "live", "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 },
    "scheduledHolds": [],            // upcoming pauses, soonest first
    "revenueClass": "recurring"
  },
  "meta": { "asOf": "2026-08-04T09:30:00+10:00", "source": "live", "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 },
    "scheduledHolds": [              // upcoming pauses, soonest first ([] = none)
      { "startsAt": "2026-08-17T00:00:00+10:00", "resumesAt": "2026-08-24T00:00:00+10:00", "days": 7, "source": "api" }
    ],
    "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", "source": "live", "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", "source": "live", "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", "source": "live", "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", "source": "live", "requestId": "req_…" }
}

Lead reads

The speed-to-lead surface: who just enquired, what they were offered, whether they're ready to train, and what contact has already happened. Every lead is minted as a Roll-Call-native member the moment it exists, so lead ids are member ids — there is no separate lead record to reconcile. Roll Call also pushes signed lead lifecycle events (lead.created@v1, lead.status_changed@v1, and trial.booked@v1 / trial.cancelled@v1 for lead bookings) to a consumer endpoint configured per deployment — outbound events are not configured per API key. These reads are the pull side: the reconciliation and detail behind those events.

Recent leads

GET/leads/recent?since=<ISO8601>scope: read

Leads created or changed since the timestamp — the reconciliation read behind the pushed lead events. Converted and dead leads stay visible so your status reconciliation works; filter on status yourself. Ordered by most-recent activity (created or status-changed) descending, capped at 200 (meta.cap — narrow since if you hit it; meta.since echoes your parameter). A since that isn't ISO 8601 is 422.

200 — lead summaries
{
  "data": [
    {
      "memberId": "9c41d2ab-…",
      "displayName": "Jess Lee",
      "status": "pending",              // same vocab as the member summary; a lead on
                                        // an active trial reads "trial", a converted
                                        // lead reads "active"
      "statusReason": "trial_lead",     // nullable
      "source": "facebook",             // creation channel; "direct" when unknown
      "createdAt": "2026-08-20T09:00:00+10:00",
      "contact": {                      // keys omitted when unknown
        "phone": "+61400123123",
        "email": "jess@example.com"
      },
      "audience": "any",                // kids | adult | any — kids when a minor,
                                        // else the attached plan's audience
      "trialPlan": {                    // null until a trial plan is attached
        "name": "Trial Week", "priceCents": 2000, "accessDays": 7
      },
      "readyToTrain": false,            // waiver signed + emergency contact + medical
                                        // answered — derived at read time, never stored
      "attribution": { "channel": "facebook", "campaignRef": null }
    }
  ],
  "meta": { "asOf": "2026-08-20T09:30:00+10:00", "source": "live",
            "since": "2026-08-19T09:00:00+10:00", "cap": 200,
            "requestId": "req_…" }
}

Single lead

GET/leads/:memberIdscope: read

One lead in the same LeadSummary shape as the list above. status uses the member-status vocab — a native member on an active trial reads trial, any other current member reads active. readyToTrain is derived at read time (waiver signed + emergency contact + medical answered) and never stored. Unknown id — or a member of another gym — is 404.

Comms preferences

GET/members/:memberId/comms-preferencesscope: read

Marketing consent, reachable channels, and quiet hours — honestly minimal. Roll Call holds no consent or quiet-hours record today, so marketing.state is always unknown (with recordedAt null) and quietHours is null. channels.sms / channels.email report whether that contact point is on file — reachability, not consent. Treat consent as your own problem until this read says otherwise. Wrong gym is 404.

200 — comms preferences
{
  "data": {
    "marketing": { "state": "unknown", "recordedAt": null },
    "channels": { "sms": true, "email": false },   // contact point on file — reachability, NOT consent
    "quietHours": null
  },
  "meta": { "asOf": "2026-08-20T09:30:00+10:00", "source": "live", "requestId": "req_…" }
}

Lead contact timeline

GET/leads/:memberId/timelinescope: read

The contact history behind a double-contact guard: check this before reaching out so two systems never message the same person twice. kind is one of email_sent, sms_sent, note, booking, attendance, status_change (call_logged is defined but never served — no call log exists). email_sent / sms_sent cover automated sends only (journeys, booking confirmations) — an empty timeline means unknown, never "never contacted", and coveragestates exactly what is and isn't held. Wrong gym is 404.

200 — lead timeline
{
  "data": {
    "entries": [                        // newest first, capped at 100
      { "at": "2026-08-20T09:05:00+10:00", "kind": "sms_sent",
        "summary": "Welcome SMS sent (trial journey)" },
      { "at": "2026-08-20T09:00:00+10:00", "kind": "status_change",
        "summary": "Lead created via facebook" }
    ],
    "coverage": {
      "served":   ["email_sent", "sms_sent", "note", "booking",
                   "attendance", "status_change"],
      "not_held": ["call_logged"],      // defined but never served — no call log exists
      "notes": "email_sent / sms_sent cover automated sends only. An empty timeline means unknown, never \"never contacted\"."
    }
  },
  "meta": { "asOf": "2026-08-20T09:30:00+10:00", "source": "live", "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", "source": "live", "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", "source": "live", "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", "source": "live", "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-…",
      "startAt": "2026-08-31T00:00:00+10:00",
      "resumeAt": "2026-09-07T00:00:00+10:00",
      "reason": "Member-requested pause (1 week), 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.

Build board

Roll Call's own delivery roadmap, read-only. These require the roadmap scope, which is not issued on partner keys — this is our internal build plan rather than anything about your gym, so a partner key returns 403here by design. It is never part of a key's default scopes and has to be asked for explicitly. Documented because the reference never trails the deployed API.

Every response carries ranAt: these are served from a dated snapshot, not computed per request, because the underlying facts come from probes that read the repo, the production database and the door units. Treat a stale ranAt as stale data rather than assuming it is current.

Condition state is never a boolean. Each condition reports one of four states, and they do not mean the same thing: verified (a probe passed), attested (a person signed for it, with a date, and the signature expires), stale (that signature has expired — quote it as “as of” a date, never as done) and open (no claim at all). A per-condition rollup is provided as the worst state across its steps; the individual step states are always present alongside it and must not be collapsed away.

Every goal and its progress

GET/roadmap/goalsscope: roadmap

Each goal with priority, conditions met/total, how many are startable now, whether it is parked (and the condition that brings it back), and conditionStates — a count per state, so a consumer reading only the headline still cannot mistake three attested conditions for three verified ones.

One goal's full chain

GET/roadmap/goals/:idscope: roadmap

Every condition under the goal with its dependency wave, size, what it is blocked by, and each probe step with its own state and detail. 404 when the goal id is unknown.

What is startable, best first

GET/roadmap/next?limit=8scope: roadmap

Ordered by goal priority, then by how much each unlocks, then smallest first — the same ordering the terminal uses, so the two never disagree. Also returns any failing gate, and topGoalHasNothingStartable naming a goal to move instead when the top one is entirely blocked.

Changelog

DateChange
2026-08-29ADDED: GET /roadmap/goals, /roadmap/goals/:id and /roadmap/next — Roll Call's own build board, behind a new `roadmap` scope that is not issued on partner keys. Served from a dated snapshot (every response carries ranAt) because the facts come from probes over the repo, the production database and the door units. Condition state is reported as verified/attested/stale/open and never as a boolean: a probe passing and a person signing something that later expires are different facts, and collapsing them is how a masked file became a retired one.
2026-08-24REMOVED: GET /members/:id/membership/gymmaster — the temporary GymMaster-billed check added 2026-08-04, retired as promised now the GymMaster migration is complete. No membership is GymMaster-billed; the endpoint now returns 404. The same fact lives permanently on the membership read as billingRail (no value ever reads "gymmaster" again), and the pause command's rail_not_pausable rejection is unchanged for genuinely un-pausable rails.
2026-08-21billingRail no longer falls back to "gymmaster" for a rail we don't recognise, and a membership with no rail recorded is no longer reported as GymMaster-billed (isGymMasterBilled false, billingRail "rollcall"). GymMaster was decoupled and bills nobody, so the old fallback asserted something that cannot be true — and made such a member read as un-pausable. If you branch on isGymMasterBilled, it now means what it says.
2026-08-21status can now return "current" — the value a Roll-Call-native member has always carried in the database. It was previously reported as "pending" by mistake, so some live, training members appeared not to be set up yet. Treat "current" and "active" as the same thing: the member is in good standing. No other value changed meaning.
2026-08-20Speed-to-lead reads for connected software: GET /leads/recent?since= (leads created or changed since a timestamp, LeadSummary shape, cap 200), GET /leads/:memberId (single LeadSummary), GET /leads/:memberId/timeline (contact history + honest coverage — the double-contact guard), and GET /members/:memberId/comms-preferences (consent honestly unknown; channels report reachability).
2026-08-20meta.source: "live" now included on every read response — every projection is computed at request time, and the envelope now says so.
2026-08-20Roll Call now pushes signed lead lifecycle events (lead.created@v1, lead.status_changed@v1, and trial.booked@v1 / trial.cancelled@v1 for lead bookings) to a configured consumer endpoint. Outbound events are configured per deployment, not per API key — contact us to point them at your software.
2026-08-12Scheduled pauses: request_membership_pause accepts an optional payload.startAt (future ISO datetime) — today/absent pauses immediately as before; a future start books a scheduled hold that activates on the day. Both membership reads gain an additive scheduledHolds array (startsAt, resumesAt, days, source) so consumers can tell paused / pause scheduled / no pause apart. New reject reason: invalid_start_date.
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.