Classes

19 operations. Every schema and example on this page is generated from the platform contract.

List scheduled class occurrences in a date window, with booking counts.

The merchant schedule view. Occurrences are joined to their class template and carry confirmed + waitlist counts. The default window anchors on the STORE’s calendar day (today → +14 days) because scheduled_date is a store-local date, a naive UTC "today" drifted a day around midnight for any store offset from UTC.

Parameters

store_idstring · uuidqueryrequired

Store that owns the class (also authorises the merchant).

start_datestringqueryoptional

First store-local date to include. Defaults to TODAY in the store’s timezone.

end_datestringqueryoptional

Last store-local date to include. Defaults to 14 days after the start default.

Response, 200

occurrencesarray of objectrequired

Ordered by date then start time.

25 child fields
idstringrequired
class_idstring, nullablerequired
store_idstring, nullablerequired
scheduled_datestringrequired

Store-local calendar date (YYYY-MM-DD).

start_timestring, nullablerequired

Gym wall-clock start, "HH:MM:SS".

end_timestring, nullablerequired

Gym wall-clock end, "HH:MM:SS".

statusstringrequired

'scheduled' | 'cancelled' | …

cancelled_reasonstring, nullableoptional
created_atstring, nullableoptional
class_namestring, nullablerequired
categorystring, nullablerequired
difficultystring, nullablerequired
duration_minutesnumber, nullablerequired

Derived from start/end time (wrapping past midnight), falling back to the template default.

max_capacitynumber, nullablerequired
instructor_namestring, nullablerequired

Per-occurrence override if set, else the template’s instructor.

coach_idstring, nullablerequired
image_urlstring, nullablerequired
require_membershipboolean, nullablerequired
allow_drop_inboolean, nullablerequired
drop_in_public_enabledboolean, nullablerequired
drop_in_members_enabledboolean, nullablerequired
drop_in_price_pencenumber, nullablerequired
confirmed_countnumberrequired

Derived as max_capacity - spots_remaining.

waitlist_countnumberrequired
cancellation_cutoff_override_minutesnumber, nullablerequired

Level-3 per-occurrence cancellation override; null falls back to Level 2 / Level 1.

class_move_policyenumrequired

The store’s policy for what happens to booked members when a class is moved. Ridden along so a drag-to-move confirm dialog needs no second round-trip. Defaults to reconfirm_warning_only.

notify_onlyreconfirm_warning_onlyreconfirm_or_release
curl -G "https://www.membber.com/api/v1/classes/occurrences" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  --data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"
Response, 200
{
  "occurrences": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "class_id": "2945b542-0000-4000-8000-d0c500000029",
      "store_id": "6659c139-0000-4000-8000-d0c500000066",
      "scheduled_date": "<scheduled_date>",
      "start_time": "<start_time>",
      "end_time": "<end_time>",
      "status": "<status>",
      "cancelled_reason": "Added at the front desk",
      "created_at": "<created_at>",
      "class_name": "<class_name>",
      "category": "<category>",
      "difficulty": "<difficulty>",
      "duration_minutes": 1,
      "max_capacity": 1,
      "instructor_name": "<instructor_name>",
      "coach_id": "c711eee0-0000-4000-8000-d0c5000000c7",
      "image_url": "https://example.com/image.jpg",
      "require_membership": true,
      "allow_drop_in": true,
      "drop_in_public_enabled": true,
      "drop_in_members_enabled": true,
      "drop_in_price_pence": 1500,
      "confirmed_count": 1,
      "waitlist_count": 1,
      "cancellation_cutoff_override_minutes": 1
    }
  ],
  "class_move_policy": "notify_only"
}

Bulk-mark attendance on a class occurrence roster.

Front-desk bulk attendance: one atomic UPDATE flips every requested booking whose current status allows the transition (attended ← confirmed/no_show, no_show ← confirmed/attended, confirmed ← attended as the Undo). Returns exactly the rows that changed. Naturally idempotent, a retry updates 0 rows and succeeds.

Parameters

occIdstringpathrequired

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

booking_idsarray of stringrequired

The bookings to flip (max 200). Only rows whose current status allows the transition change.

statusenumrequired

Target status. 'attended' from confirmed/no_show, 'no_show' from confirmed/attended, 'confirmed' ONLY from attended (the Undo of a bulk mark, it can never resurrect a cancelled booking).

attendedno_showconfirmed

Response, 200

statusstringrequired

The target status that was applied.

updatedarray of objectrequired

EXACTLY the rows that changed, the only ids a client may flip optimistically.

2 child fields
idstringrequired

Booking id that changed.

customer_idstring, nullablerequired

The booking’s customer.

updated_countnumberrequired
skipped_countnumberrequired

Requested ids whose current status did not allow the transition.

curl -X POST "https://www.membber.com/api/v1/classes/occurrences/064aae8a-0000-4000-8000-d0c500000006/bulk-attendance" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "booking_ids": [
      "ae11dec1-0000-4000-8000-d0c5000000ae"
    ],
    "status": "attended"
  }'
Response, 200
{
  "status": "<status>",
  "updated": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "customer_id": "96607d1c-0000-4000-8000-d0c500000096"
    }
  ],
  "updated_count": 1,
  "skipped_count": 1
}

Cancel a single class occurrence and notify + refund every affected customer.

Merchant cancels one class occurrence: atomically cancels all bookings and restores session credits, then runs the counterparty fan-out (push "Class Cancelled" to booked + waitlisted, end promotion Live Activities, Rule C FULL automated drop-in refunds via Stripe, and a live dashboard broadcast). Gated by the can_use_classes entitlement. MONEY-MOVING (issues refunds). Idempotent: a retry on an already-cancelled occurrence returns ALREADY_CANCELLED and the refund engine dedupes per booking so no double-refund is possible.

Parameters

occIdstringpathrequired

Request body

store_idstring · uuidrequired

Store the occurrence belongs to (also authorises the merchant; the occurrence must belong to it).

reasonstring, nullableoptional

Optional merchant reason, recorded on the occurrence + audit log + included in the customer push copy. Truncated to 500 chars server-side.

Response, 200

cancelled_bookingsnumberrequired

Bookings cancelled by the atomic RPC (booked + waitlisted).

members_notifiednumberrequired

Affected customers whose "Class Cancelled" push was fulfilled.

refunds_issuednumberrequired

Drop-in refunds that reached a terminal good state (Rule C full refund).

curl -X POST "https://www.membber.com/api/v1/classes/occurrences/064aae8a-0000-4000-8000-d0c500000006/cancel" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "cancelled_bookings": 1,
  "members_notified": 1,
  "refunds_issued": 1
}

Change one occurrence’s capacity or instructor, notifying everyone affected.

Covers "the merchant tweaked the cap or swapped the coach", time and date moves go through /reschedule so they get the full reconfirmation flow. A capacity RAISE does not simply widen spots_remaining (that handed freed seats to whoever booked next and jumped the waitlist queue): it raises the cap and runs cascade_after_booking_release once per freed seat, so each seat becomes a promotion OFFER to the next waitlisted member, with its offer push, or returns to open headroom when nobody is waiting. A capacity LOWER clamps spots_remaining at the confirmed count. Every confirmed booking then gets a "Class details updated" push, and the merchant dashboard is broadcast to. Cancelled or already-finished occurrences are refused. Idempotency `rpc`.

Parameters

occIdstringpathrequired

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

max_capacityintegeroptional

New capacity. May never drop below the current confirmed count (409 CAPACITY_BELOW_BOOKINGS), releasing a member is an explicit, audited action on the remove-member endpoint.

instructor_namestring, nullableoptional

Per-occurrence instructor override. null (or an empty string) clears it back to the template’s.

Response, 200

occurrenceobjectrequired

The updated occurrence: { id, max_capacity, instructor_name, status }.

notified_bookingsnumberrequired

Confirmed bookings whose "Class details updated" push was fulfilled.

promotions_offerednumberrequired

Waitlisted members offered a freed seat by the capacity-raise cascade (each got an offer push).

curl -X PATCH "https://www.membber.com/api/v1/classes/occurrences/064aae8a-0000-4000-8000-d0c500000006/details" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "occurrence": {},
  "notified_bookings": 1,
  "promotions_offered": 1
}

Mark a booked member as a no-show for a class occurrence.

Merchant marks one confirmed or attended booking as a no-show: flips the booking status, stamps no_show_marked_at (the timestamp the separate no-show fee/strike cron keys off), clears attended_at, and resyncs the member retention analytics. Gated by the can_use_classes entitlement. Idempotent: marking an already-no-show booking is a no-op success (status already_no_show).

Parameters

occIdstringpathrequired

Request body

store_idstring · uuidrequired

Store the occurrence + booking belong to (also authorises the merchant).

booking_idstring · uuidrequired

The confirmed or attended booking to mark as a no-show.

Response, 200

statusenumrequired

'no_show' when this call flipped the booking; 'already_no_show' when it was already marked (idempotent no-op).

no_showalready_no_show
no_show_marked_atstring, nullablerequired

When this call stamped the no-show (null on the already_no_show no-op, mirroring the legacy replay body).

idempotentbooleanrequired

true when the booking was already a no-show and no write happened.

curl -X POST "https://www.membber.com/api/v1/classes/occurrences/064aae8a-0000-4000-8000-d0c500000006/mark-no-show" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "booking_id": "ae11dec1-0000-4000-8000-d0c5000000ae"
  }'
Response, 200
{
  "status": "no_show",
  "no_show_marked_at": "<no_show_marked_at>",
  "idempotent": true
}

Move a single class occurrence to a new date/time and notify every affected customer.

Merchant moves one class occurrence (this-occurrence-only, the parent recurrence is unchanged). Runs the counterparty fan-out: reset reminders, push booked + waitlisted customers, flip re-confirmation flags + enqueue reconfirmation-expiry jobs (material moves), email confirmed bookings via Resend, and update active waitlist Live Activities. Gated by the can_use_classes entitlement. Guards: cannot move a cancelled, past, in-progress, or finished occurrence. NOT money-moving. Idempotent: re-sending the same new values is a no-op (no double notification), matching the legacy route which sends no Idempotency-Key.

Parameters

occIdstringpathrequired

Request body

store_idstring · uuidrequired

Store the occurrence belongs to (also authorises the merchant; the occurrence must belong to it).

scheduled_datestringrequired

New wall-clock date, yyyy-MM-dd. Must not be in the past (store-timezone-aware, checked server-side).

start_timestringrequired

New wall-clock start, HH:mm or HH:mm:ss (store local).

end_timestringrequired

New wall-clock end, HH:mm or HH:mm:ss (store local). Must be after start and within the same day.

Response, 200

occurrenceobjectrequired

The occurrence row after the move (or the unchanged row on a no-op).

5 child fields
idstringrequired
scheduled_datestringrequired
start_timestringrequired
end_timestring, nullablerequired
statusstringrequired
no_opbooleanrequired

true when the requested date/time equalled the current values, so nothing moved and NO customer fan-out ran.

curl -X PATCH "https://www.membber.com/api/v1/classes/occurrences/064aae8a-0000-4000-8000-d0c500000006/reschedule" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "scheduled_date": "<scheduled_date>",
    "start_time": "<start_time>",
    "end_time": "<end_time>"
  }'
Response, 200
{
  "occurrence": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "scheduled_date": "<scheduled_date>",
    "start_time": "<start_time>",
    "end_time": "<end_time>",
    "status": "<status>"
  },
  "no_op": true
}

Manually generate upcoming occurrences for every recurring class in the store.

Runs generate_store_occurrences, which creates only the occurrences that do not already exist. Naturally idempotent (idempotency `rpc`): running it twice generates 0 the second time. Normally the cron keeps the schedule topped up; this is the merchant’s manual catch-up.

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

days_aheadintegeroptional

How far ahead to generate. Defaults to 14; values above 90 are capped at 90.

Response, 200

generatednumberrequired

Occurrences actually created (0 when everything already existed).

days_aheadnumberrequired

The window actually used, after the 90-day cap.

curl -X POST "https://www.membber.com/api/v1/classes/occurrences/generate" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "generated": 1,
  "days_ahead": 1
}

Hand-place a single class on the schedule (from a template, or creating one inline).

The Schedule Builder’s "tap an empty cell" flow. The occurrence is tagged source="manual" so a later recurrence regeneration can NEVER delete it, and spots_remaining is set explicitly (the column default is 0, which would otherwise create a class that is instantly full). Rejects past dates in the store’s timezone and any class that would run past midnight. In inline-create mode every rejection after the template insert rolls that template back, so a failed quick-add leaves no orphan. ⚠️ NOT idempotent: this is a plain INSERT and `class_occurrences` has no unique constraint beyond its primary key, so a retried quick-add places a SECOND class at the same time. Do not blind-retry a timeout, re-read the schedule and confirm the occurrence is absent first. (Same behaviour as the legacy route this twins; a dedup key is deliberately not claimed here rather than falsely asserted.)

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

class_idstring · uuidoptional

TEMPLATE MODE: an existing ACTIVE class template to schedule. When omitted, `name` switches to INLINE-CREATE MODE.

namestringoptional

INLINE-CREATE MODE: an active template with the same normalised name is silently reused if one exists, otherwise a fresh template is minted alongside the occurrence.

scheduled_datestringrequired

Store-local date. Must not be before TODAY in the store’s timezone.

start_timestringrequired

Gym wall-clock start, "HH:MM" or "HH:MM:SS".

duration_minutesintegeroptional

Per-occurrence override, 15-240. Falls back to the template, then 60.

max_capacityintegeroptional

Per-occurrence override, 1-100. Falls back to the template, then 15.

categorystringoptional

Inline-create mode only: category for the new template.

coach_idstring · uuidoptional

Overrides the template coach; the coach’s name is copied onto the occurrence.

Response, 200

occurrenceobjectrequired

The created occurrence, with the template’s metadata flattened in so the client needs no refetch.

25 child fields
idstringrequired
class_idstring, nullablerequired
store_idstring, nullablerequired
scheduled_datestringrequired

Store-local calendar date (YYYY-MM-DD).

start_timestring, nullablerequired

Gym wall-clock start, "HH:MM:SS".

end_timestring, nullablerequired

Gym wall-clock end, "HH:MM:SS".

statusstringrequired

'scheduled' | 'cancelled' | …

cancelled_reasonstring, nullableoptional
created_atstring, nullableoptional
class_namestring, nullablerequired
categorystring, nullablerequired
difficultystring, nullablerequired
duration_minutesnumber, nullablerequired

Derived from start/end time (wrapping past midnight), falling back to the template default.

max_capacitynumber, nullablerequired
instructor_namestring, nullablerequired

Per-occurrence override if set, else the template’s instructor.

coach_idstring, nullablerequired
image_urlstring, nullablerequired
require_membershipboolean, nullablerequired
allow_drop_inboolean, nullablerequired
drop_in_public_enabledboolean, nullablerequired
drop_in_members_enabledboolean, nullablerequired
drop_in_price_pencenumber, nullablerequired
confirmed_countnumberrequired

Derived as max_capacity - spots_remaining.

waitlist_countnumberrequired
cancellation_cutoff_override_minutesnumber, nullablerequired

Level-3 per-occurrence cancellation override; null falls back to Level 2 / Level 1.

curl -X POST "https://www.membber.com/api/v1/classes/occurrences/quick-add" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "scheduled_date": "<scheduled_date>",
    "start_time": "<start_time>"
  }'
Response, 200
{
  "occurrence": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "class_id": "2945b542-0000-4000-8000-d0c500000029",
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "scheduled_date": "<scheduled_date>",
    "start_time": "<start_time>",
    "end_time": "<end_time>",
    "status": "<status>",
    "cancelled_reason": "Added at the front desk",
    "created_at": "<created_at>",
    "class_name": "<class_name>",
    "category": "<category>",
    "difficulty": "<difficulty>",
    "duration_minutes": 1,
    "max_capacity": 1,
    "instructor_name": "<instructor_name>",
    "coach_id": "c711eee0-0000-4000-8000-d0c5000000c7",
    "image_url": "https://example.com/image.jpg",
    "require_membership": true,
    "allow_drop_in": true,
    "drop_in_public_enabled": true,
    "drop_in_members_enabled": true,
    "drop_in_price_pence": 1500,
    "confirmed_count": 1,
    "waitlist_count": 1,
    "cancellation_cutoff_override_minutes": 1
  }
}

Search the store’s scheduled classes by text, category, instructor and date range.

Date filters run in Postgres; text, category and instructor filters run over the fetched page because PostgREST cannot filter on a joined table, so `total` is the match count within that page, not a global count. Only status="scheduled" occurrences are returned.

auth merchant:readop searchClassOccurrencesrate limitederrors

Parameters

store_idstring · uuidqueryrequired

Store that owns the class (also authorises the merchant).

qstringqueryoptional

Free text matched against class name, description and instructor.

categorystringqueryoptional

Exact (case-insensitive) category match.

instructorstringqueryoptional

Substring match on the occurrence or template instructor.

date_fromstringqueryoptional

Earliest store-local date (YYYY-MM-DD).

date_tostringqueryoptional

Latest store-local date (YYYY-MM-DD).

limitintegerqueryoptional

Page size, default 30.

offsetintegerqueryoptional

Page offset, default 0.

Response, 200

occurrencesarray of objectrequired

Matching occurrences with their joined class template.

totalnumberrequired

Number of matches WITHIN the fetched page. The text/category/instructor filters run after paging (PostgREST cannot filter on a joined table), so this is not a database-wide count.

curl -G "https://www.membber.com/api/v1/classes/occurrences/search" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  --data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"
Response, 200
{
  "occurrences": [
    {}
  ],
  "total": 1
}

List the store’s class templates (with media + skip dates) and active coaches.

The merchant class library. Returns every `gym_classes` row for the store, including inactive ones, so the editor can show archived types, each enriched with its published class media and its recent + future skip dates, plus the store’s active coaches. Gated by the can_use_classes entitlement.

Parameters

store_idstring · uuidqueryrequired

Store that owns the class (also authorises the merchant).

Response, 200

classesarray of objectrequired

Every class template for the store INCLUDING inactive ones, ordered by name.

coachesarray of objectrequired

Active coaches for the store, ordered by name.

curl -G "https://www.membber.com/api/v1/classes/templates" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  --data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"
Response, 200
{
  "classes": [
    {}
  ],
  "coaches": [
    {}
  ]
}

Create a class template, optionally generating its first 28 days of occurrences.

Creates a `gym_classes` row. When a legacy single-time recurrence (recurring_days + recurring_time) is supplied it first probes venue capacity across the next 14 days and REJECTS with VENUE_CAPACITY_EXCEEDED (409) if the class would overfill the venue, deleting the just-created row so no orphan survives, then persists any skip dates and generates 28 days of occurrences. A duplicate active name answers TEMPLATE_NAME_TAKEN (409). Idempotency is `rpc`: the partial unique index on (store, lower(trim(name))) makes a retried create collide rather than duplicate.

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

namestringoptional

Display name. Must be unique per store among ACTIVE classes.

descriptionstring, nullableoptional

Long description shown to members.

categorystring, nullableoptional

Free-text grouping (e.g. "Strength").

difficultystring, nullableoptional

Free-text level (e.g. "Beginner").

instructor_namestring, nullableoptional

Display instructor. IGNORED when `coach_id` is sent and resolves, the coach name wins.

coach_idstring · uuid, nullableoptional

Link to a `gym_coaches` row; its name is copied into instructor_name. null clears the link.

duration_minutesintegeroptional

Default class length, 15-240 minutes.

max_capacityintegeroptional

Default capacity, 1-100 people.

image_urlstring, nullableoptional

Must be a URL produced by the class image uploader (cropped for mobile); anything else is rejected.

video_urlstring, nullableoptional

Optional class video.

what_to_bringstring, nullableoptional

Member-facing kit list.

require_membershipbooleanoptional

Only members may book.

allow_drop_inbooleanoptional

Master switch for paid drop-ins.

drop_in_public_enabledbooleanoptional

Non-members may buy a drop-in.

drop_in_members_enabledbooleanoptional

Members may buy a drop-in.

drop_in_price_penceinteger, nullableoptional

Drop-in price in pence (0-100000). null clears it.

recurring_daysarray of stringoptional

Legacy single-time recurrence: day-of-week strings '0'(Sun)..'6'(Sat).

recurring_timestring, nullableoptional

Legacy single-time recurrence start time, "HH:MM:SS".

recurring_end_datestring, nullableoptional

Last date the recurrence generates. Must not be in the past.

skip_datesarray of objectoptional

Dates the recurrence should skip. Rare on create, usually authored after the template exists.

2 child fields
datestringrequired

Store-local calendar date to skip (YYYY-MM-DD).

reasonstring, nullableoptional

Optional merchant note, max 200 chars (e.g. "Bank holiday").

Response, 200

classobjectrequired

The created `gym_classes` row.

occurrences_generatednumberrequired

Occurrences generated for the next 28 days (0 unless a legacy single-time recurrence was supplied).

curl -X POST "https://www.membber.com/api/v1/classes/templates" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "class": {},
  "occurrences_generated": 1
}

Soft-delete a class type, cancelling + refunding + notifying every future occurrence.

🔴 MONEY-MOVING. Sets the class inactive and then, for EVERY future scheduled occurrence (>= today in the store’s timezone): runs cancel_class_occurrence_atomic (cancels all bookings and restores members’ class credits in one transaction, a raw update would burn those credits forever), then runs the shared runCancelOccurrenceConsequences fan-out, "Class Cancelled" push to booked AND waitlisted members, promotion Live Activity ends, Rule C FULL drop-in refunds via Stripe, waitlist dashboard broadcast, and per-occurrence audit rows. The fan-out is AWAITED before responding, so a 200 means every notification and refund was attempted. Idempotent: a repeat delete finds the occurrences already cancelled (benign) and the refund engine dedupes per (booking_id, trigger_reason), so it can never double-refund.

Parameters

classIdstringpathrequired
store_idstring · uuidqueryrequired

Store that owns the class (also authorises the merchant).

Response, 200

occurrences_cancellednumberrequired

Future scheduled occurrences cancelled by the atomic RPC.

members_notifiednumberrequired

Affected customers whose "Class Cancelled" push was fulfilled across all cancelled occurrences.

refunds_issuednumberrequired

Drop-in refunds that reached a terminal good state (Rule C FULL refund).

failed_occurrencesnumberrequired

Occurrences whose cancel or fan-out had a failure. Non-zero still returns success, the class IS deactivated and the failures are captured to Sentry for the mop-up crons.

curl -X DELETE "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032?store_id=6659c139-0000-4000-8000-d0c500000066" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "occurrences_cancelled": 1,
  "members_notified": 1,
  "refunds_issued": 1,
  "failed_occurrences": 1
}

Update a class template, atomically re-applying its recurrence when the schedule changed.

Partial update of a `gym_classes` row. When (and ONLY when) recurrence actually changed AND the class is recurring on either side of the edit, the skip-date replace + delete-unbooked + regenerate run inside ONE Postgres transaction (apply_class_recurrence_change), so a failure mid-flight cannot leave a half-edited schedule. Occurrences are only ever deleted from TOMORROW in the store’s timezone onward, today’s classes are never removed, and booked occurrences always survive. A class that is one-off both before and after the edit never enters the regen path. Idempotency `rpc`: re-sending the same patch converges on the same row.

Parameters

classIdstringpathrequired

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

namestringoptional

Display name. Must be unique per store among ACTIVE classes.

descriptionstring, nullableoptional

Long description shown to members.

categorystring, nullableoptional

Free-text grouping (e.g. "Strength").

difficultystring, nullableoptional

Free-text level (e.g. "Beginner").

instructor_namestring, nullableoptional

Display instructor. IGNORED when `coach_id` is sent and resolves, the coach name wins.

coach_idstring · uuid, nullableoptional

Link to a `gym_coaches` row; its name is copied into instructor_name. null clears the link.

duration_minutesintegeroptional

Default class length, 15-240 minutes.

max_capacityintegeroptional

Default capacity, 1-100 people.

image_urlstring, nullableoptional

Must be a URL produced by the class image uploader (cropped for mobile); anything else is rejected.

video_urlstring, nullableoptional

Optional class video.

what_to_bringstring, nullableoptional

Member-facing kit list.

require_membershipbooleanoptional

Only members may book.

allow_drop_inbooleanoptional

Master switch for paid drop-ins.

drop_in_public_enabledbooleanoptional

Non-members may buy a drop-in.

drop_in_members_enabledbooleanoptional

Members may buy a drop-in.

drop_in_price_penceinteger, nullableoptional

Drop-in price in pence (0-100000). null clears it.

recurring_daysarray of stringoptional

Legacy single-time recurrence: day-of-week strings '0'(Sun)..'6'(Sat).

recurring_timestring, nullableoptional

Legacy single-time recurrence start time, "HH:MM:SS".

recurring_end_datestring, nullableoptional

Last date the recurrence generates. Must not be in the past.

is_activebooleanoptional

Archive / restore the template without deleting it.

skip_datesarray of objectoptional

REPLACES the whole skip-date set when sent. Sending it also counts as a recurrence change, which triggers the atomic delete-unbooked + regenerate for a recurring class.

2 child fields
datestringrequired

Store-local calendar date to skip (YYYY-MM-DD).

reasonstring, nullableoptional

Optional merchant note, max 200 chars (e.g. "Bank holiday").

if_matchstringoptional

Optimistic concurrency: the `updated_at` you last read. If it no longer matches, the write is refused with TEMPLATE_STALE (409) and the current value is returned, so device A cannot silently overwrite device B.

Response, 200

classobjectrequired

The updated `gym_classes` row.

recurrence_applyanyrequired

Present ONLY when the atomic recurrence regen ran: { success, skip_dates_persisted, deleted_count, generated_count }. null when the edit did not touch recurrence.

curl -X PATCH "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "class": {},
  "recurrence_apply": "<recurrence_apply>"
}

Preview what a proposed recurrence change would delete, preserve and create.

Powers the merchant review screen BEFORE they save a schedule change. PURE READ, writes nothing, which is why it declares mutates=false despite being POST-shaped (the proposed schedule is too structured for a query string). The create-count comes from the count_planned_occurrences RPCs, which mirror the generator exactly (including its venue-capacity skip logic), so the previewed number equals what will actually generate. If a count RPC fails the count falls back to 0 rather than failing the preview.

Parameters

classIdstringpathrequired

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

recurring_daysarray of stringoptional

Proposed day-of-week strings '0'(Sun)..'6'(Sat). Falls back to the class’s current value.

recurring_timestringoptional

Proposed start time "HH:MM[:SS]". Falls back to current.

recurring_end_datestring, nullableoptional

Proposed last date. Explicit null means "no end date".

skip_datesarray of objectoptional

Proposed skip dates.

2 child fields
datestringrequired

YYYY-MM-DD.

reasonstring, nullableoptional
slotsarray of objectoptional

Proposed MULTI-SLOT set. When present it is counted instead of the single-time fields.

5 child fields
daysarray of stringrequired

Day-of-week strings this slot runs on: '0'(Sun)..'6'(Sat).

start_timestringrequired

Gym wall-clock start, "HH:MM" or "HH:MM:SS" (normalised server-side).

duration_minutesinteger, nullableoptional

Per-slot length override; falls back to the class template’s duration.

max_capacityinteger, nullableoptional

Per-slot capacity override; falls back to the class template’s capacity.

instructor_namestring, nullableoptional

Per-slot instructor override; falls back to the class template’s.

Response, 200

to_remove_countnumberrequired

Future UNBOOKED occurrences that would be deleted (>= tomorrow in the store’s timezone).

booked_to_preservearray of objectrequired

Future BOOKED occurrences that would survive on the OLD schedule, with head counts so the merchant knows exactly who they would need to message.

5 child fields
occurrence_idstringrequired
scheduled_datestringrequired
start_timestring, nullablerequired
confirmed_countnumberrequired
waitlist_countnumberrequired
to_create_countnumberrequired

Occurrences the proposed schedule would generate over the next 28 days.

curl -X POST "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032/recurrence-preview" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "to_remove_count": 1,
  "booked_to_preserve": [
    {
      "occurrence_id": "8770ea49-0000-4000-8000-d0c500000087",
      "scheduled_date": "<scheduled_date>",
      "start_time": "<start_time>",
      "confirmed_count": 1,
      "waitlist_count": 1
    }
  ],
  "to_create_count": 1
}

List a class’s active weekly recurring slots.

The multi-slot Schedule Builder’s read. Returns only ACTIVE slots (removed slots are soft-deactivated and stay in the table for history) plus the parent template’s duration / capacity / instructor, which any slot field left null inherits.

auth merchant:readop listClassRecurringSlotsrate limitederrors

Parameters

classIdstringpathrequired
store_idstring · uuidqueryrequired

Store that owns the class (also authorises the merchant).

Response, 200

slotsarray of objectrequired

ACTIVE slots only, ordered by start time.

class_defaultsobjectrequired

The parent class template’s values, which any slot field left null inherits.

3 child fields
duration_minutesnumber, nullablerequired
max_capacitynumber, nullablerequired
instructor_namestring, nullablerequired
curl -G "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032/slots" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  --data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"
Response, 200
{
  "slots": [
    {}
  ],
  "class_defaults": {
    "duration_minutes": 1,
    "max_capacity": 1,
    "instructor_name": "<instructor_name>"
  }
}

Add one weekly recurring time to a class.

Adds a single slot and regenerates WITHOUT deleting anything, nothing was removed, so nothing is cleaned up. A duplicate start time for the same class is refused (DUPLICATE_SLOT_TIME, 409) by the partial unique index, which is also what makes a retry safe (idempotency `rpc`). If the regeneration itself fails the slot is still saved and the next cron cycle catches the occurrences up.

Parameters

classIdstringpathrequired

Request body

daysarray of stringrequired

Day-of-week strings this slot runs on: '0'(Sun)..'6'(Sat).

start_timestringrequired

Gym wall-clock start, "HH:MM" or "HH:MM:SS" (normalised server-side).

duration_minutesinteger, nullableoptional

Per-slot length override; falls back to the class template’s duration.

max_capacityinteger, nullableoptional

Per-slot capacity override; falls back to the class template’s capacity.

instructor_namestring, nullableoptional

Per-slot instructor override; falls back to the class template’s.

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

Response, 200

slotobjectrequired

The inserted slot.

recurrence_applyanyrequired

What the regeneration created.

curl -X POST "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032/slots" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "days": [
      "<day>"
    ],
    "start_time": "<start_time>",
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "slot": {},
  "recurrence_apply": "<recurrence_apply>"
}

Replace a class’s entire weekly recurring schedule in one transaction.

The canonical editor save. Deactivating the current slots, inserting the new set, deleting unbooked future occurrences and regenerating all happen inside ONE Postgres transaction (replace_class_recurring_slots), so a failure mid-flight cannot leave a half-saved schedule. Only occurrences from TOMORROW in the store’s timezone onward, with source="recurring", are ever deleted, hand-placed classes and any BOOKED occurrence survive on their original time. Idempotency `rpc`.

Parameters

classIdstringpathrequired

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

slotsarray of objectrequired

The COMPLETE new slot set. An empty array removes all recurrence.

5 child fields
daysarray of stringrequired

Day-of-week strings this slot runs on: '0'(Sun)..'6'(Sat).

start_timestringrequired

Gym wall-clock start, "HH:MM" or "HH:MM:SS" (normalised server-side).

duration_minutesinteger, nullableoptional

Per-slot length override; falls back to the class template’s duration.

max_capacityinteger, nullableoptional

Per-slot capacity override; falls back to the class template’s capacity.

instructor_namestring, nullableoptional

Per-slot instructor override; falls back to the class template’s.

if_matchstringoptional

Optimistic concurrency against the class template’s `updated_at`; mismatch → TEMPLATE_STALE (409).

Response, 200

slotsarray of objectrequired

The resolved active slots after the save (server truth).

class_defaultsobjectrequired

The parent class template’s values, which any slot field left null inherits.

3 child fields
duration_minutesnumber, nullablerequired
max_capacitynumber, nullablerequired
instructor_namestring, nullablerequired
recurrence_applyanyrequired

What the transaction did: deleted_count / generated_count for the regenerated window.

curl -X PUT "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032/slots" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "slots": [
      {
        "days": [
          "<day>"
        ],
        "start_time": "<start_time>"
      }
    ]
  }'
Response, 200
{
  "slots": [
    {}
  ],
  "class_defaults": {
    "duration_minutes": 1,
    "max_capacity": 1,
    "instructor_name": "<instructor_name>"
  },
  "recurrence_apply": "<recurrence_apply>"
}

Remove one weekly recurring slot, preserving any member bookings on that time.

Soft-deactivates the slot (never a hard delete, so audit history survives), then deletes unbooked future occurrences at that time and regenerates. BOOKED occurrences are deliberately left standing on the removed time, member contracts are honoured, and the response reports exactly how many member bookings that is, so the merchant knows who still needs handling. Idempotency `rpc`: removing an already-removed slot is benign.

Parameters

classIdstringpathrequired
slotIdstringpathrequired
store_idstring · uuidqueryrequired

Store that owns the class (also authorises the merchant).

Response, 200

recurrence_applyobjectrequired

What removing the slot did to the schedule.

2 child fields
deleted_countnumberrequired

Unbooked future occurrences removed at that time.

preserved_booked_countnumberrequired

Member bookings (confirmed + waitlisted + promotion_pending) that STILL STAND on the removed time, the merchant must honour or message these.

curl -X DELETE "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032/slots/ca636bb9-0000-4000-8000-d0c5000000ca?store_id=6659c139-0000-4000-8000-d0c500000066" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "recurrence_apply": {
    "deleted_count": 1,
    "preserved_booked_count": 1
  }
}

Update one weekly recurring slot, reshaping future unbooked occurrences.

Merges the sent fields over the stored ones and re-validates the FINAL state, so cross-field rules such as "must not run past midnight" are checked against what the slot will actually become, then writes only the fields you sent. An empty patch is a no-op that returns the current row. After a real write it deletes unbooked future occurrences and regenerates, so the new time takes effect from tomorrow onward while BOOKED occurrences stay on the old time. Idempotency `rpc`.

Parameters

classIdstringpathrequired
slotIdstringpathrequired

Request body

store_idstring · uuidrequired

Store that owns the class (also authorises the merchant).

daysarray of stringoptional

Day-of-week strings '0'(Sun)..'6'(Sat).

start_timestringoptional

Gym wall-clock start, "HH:MM" or "HH:MM:SS".

duration_minutesinteger, nullableoptional
max_capacityinteger, nullableoptional
instructor_namestring, nullableoptional

Response, 200

slotobjectrequired

The slot after the update (or unchanged, when the patch was empty).

recurrence_applyanyrequired

What the regeneration deleted / created.

curl -X PATCH "https://www.membber.com/api/v1/classes/templates/32e134b3-0000-4000-8000-d0c500000032/slots/ca636bb9-0000-4000-8000-d0c5000000ca" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "slot": {},
  "recurrence_apply": "<recurrence_apply>"
}
WhatsApp
Book a Call
Start Free