13 operations. Every schema and example on this page is generated from the platform contract.
Merchant lists their store memberships for the Members screen: membership rows with embedded plan and customer, the filtered total count, and a whole-roster status rollup (active / frozen / past_due / total) on the first unsearched page. Supports status filter, server-side name/email/phone search, an exact per-customer lookup, and pagination.
Store whose memberships to list (also authorises the merchant).
Filter to a single membership status (e.g. active, frozen, past_due).
Exact per-customer lookup, returns all of this customer's membership rows, unbounded by the page window.
Server-side name/email/phone match across the whole roster.
Page size (default 50).
Page offset (default 0).
curl -G "https://www.membber.com/api/v1/memberships" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/memberships", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.listMemberships(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"memberships": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"plan_id": "e28fa9f1-0000-4000-8000-d0c5000000e2",
"status": "<status>",
"created_at": "<created_at>"
}
],
"total": 1,
"statusCounts": {
"active": 1,
"frozen": 1,
"past_due": 1,
"total": 1
}
}Merchant staff enrols a customer into a gym membership plan: creates the membership and its Stripe subscription (or one-time PaymentIntent), gated by the can_use_member_billing entitlement and refused on a store that no longer serves new obligations. Money-moving. Returns the full membership row (with embedded plan + customer) for an immediate optimistic roster insert.
Store enrolling the member (also authorises the merchant).
Customer to enrol into the plan.
The gym membership plan to enrol the customer into.
Optional free-text staff note stored on the membership.
The enrolled membership row (with embedded plan + customer when the detail read-back succeeds).
curl -X POST "https://www.membber.com/api/v1/memberships" \
-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",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"plan_id": "e28fa9f1-0000-4000-8000-d0c5000000e2"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
customer_id: "96607d1c-0000-4000-8000-d0c500000096",
plan_id: "e28fa9f1-0000-4000-8000-d0c5000000e2"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.enrollMember(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
customerId: "96607d1c-0000-4000-8000-d0c500000096",
planId: "e28fa9f1-0000-4000-8000-d0c5000000e2"
))
).ok.body.json
print(response){
"membership": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"plan_id": "e28fa9f1-0000-4000-8000-d0c5000000e2",
"status": "<status>",
"created_at": "<created_at>"
}
}The reason-correct recovery action on the Members › Money needs-attention list: emails (and best-effort pushes) a past-due member to add/update their card in the app. `ask_to_update` (no working card) uses firm copy; `remind` (decline while Stripe still retries) uses a softer nudge. Store-scoped and business-authed; gated by the can_use_member_billing entitlement + the can_process_payments staff permission. Sends no money. Cooldown-protected: a repeat within the window is a no-op returning already_asked_recently=true so the member is never blasted. The lifecycle-notification row is the append-only audit.
Store the membership belongs to (also authorises the merchant).
ask_to_update = no working card on file (firm "add a card" copy); remind = a decline while Stripe is still auto-retrying (softer "check your card" nudge). Mirrors the row's suggested_action; a "retry" row must use retry-payment, not this.
ask_to_updateremindTrue when at least one channel (email/push) delivered the ask just now.
True when the member was asked within the cooldown window, so nothing was sent this time.
Which channels delivered the ask this call (empty when already_asked_recently, or the member is unreachable).
emailpushISO timestamp of the ask that is in effect (this send, or the prior one when in cooldown); null when unreachable.
ISO timestamp the merchant can ask this member again; null when unreachable (may ask again now).
curl -X POST "https://www.membber.com/api/v1/memberships/00000d1b-0000-4000-8000-d0c500000000/ask-card-update" \
-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",
"mode": "ask_to_update"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships/{id}/ask-card-update", {
params: { path: { id: "00000d1b-0000-4000-8000-d0c500000000" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
mode: "ask_to_update"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.askMemberToUpdateCard(
path: .init(id: "00000d1b-0000-4000-8000-d0c500000000"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
mode: .askToUpdate
))
).ok.body.json
print(response){
"sent": true,
"already_asked_recently": true,
"channels": [
"email"
],
"asked_at": "<asked_at>",
"next_allowed_at": "<next_allowed_at>"
}Merchant staff cancels a gym membership: flips the Stripe subscription to cancel at period end and records the cancellation atomically (UPDATE membership + INSERT history in one transaction), then invalidates cache, pushes the wallet update, sends the scheduled-cancellation notification, and recomputes retention analytics. Gated by the can_use_member_billing entitlement + the can_process_payments staff permission. Money-adjacent (stops future billing). Idempotent: a membership already scheduled for cancellation returns already_scheduled=true.
Store the membership belongs to (also authorises the merchant).
Optional staff reason recorded on the cancellation history row (defaults to "Staff cancelled").
ISO timestamp the cancellation takes effect (end of current billing period).
True when a cancellation was already scheduled and this call was a no-op (idempotent).
curl -X POST "https://www.membber.com/api/v1/memberships/00000d1b-0000-4000-8000-d0c500000000/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"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships/{id}/cancel", {
params: { path: { id: "00000d1b-0000-4000-8000-d0c500000000" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.cancelMembership(
path: .init(id: "00000d1b-0000-4000-8000-d0c500000000"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"cancellation_effective_date": "<cancellation_effective_date>",
"already_scheduled": true
}Merchant staff freezes a gym membership: pauses the Stripe subscription collection (behavior: void) and records the freeze atomically (status flip + per-year freeze counters + history INSERT in one transaction), then recomputes retention analytics, refreshes the wallet pass, and pushes the customer a "membership frozen" notification. Gated by the can_use_member_billing entitlement + the can_process_payments staff permission. Enforces the freeze business rules (must be active, no pending cancellation, min-commitment met, max 2 freezes/year, min 7 days, max 90 days/year). Money-moving (changes what Stripe charges). Idempotent on (membership_id, freeze_end): a retry never double-bumps the per-year counters.
Store the membership belongs to (also authorises the merchant).
ISO timestamp the freeze ends (the resume date). Must be at least 7 days out.
Optional staff reason recorded on the freeze history row. Truncated to 500 chars server-side.
The date the freeze started (YYYY-MM-DD, server clock at the time of the call).
ISO timestamp the freeze ends (echoes the requested resume date).
Number of days the membership is frozen for.
curl -X POST "https://www.membber.com/api/v1/memberships/00000d1b-0000-4000-8000-d0c500000000/freeze" \
-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",
"freeze_end": "2026-07-01T09:00:00Z"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships/{id}/freeze", {
params: { path: { id: "00000d1b-0000-4000-8000-d0c500000000" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
freeze_end: "2026-07-01T09:00:00Z"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.pauseMembership(
path: .init(id: "00000d1b-0000-4000-8000-d0c500000000"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
freezeEnd: "2026-07-01T09:00:00Z"
))
).ok.body.json
print(response){
"freeze_start": "<freeze_start>",
"freeze_end": "<freeze_end>",
"freeze_days": -9007199254740991
}Merchant charges a member a single fee (PT session, product, damaged kit, a manually-settled no-show): an off-session destination charge on their saved card carrying the standard uniform platform fee (1.5% margin + Stripe-cost recovery). Requires a note; the member is notified. Store-scoped and business-authed; gated by the can_use_member_billing entitlement + the can_process_payments staff permission. Idempotent via the caller key (no double-charge). A non-ok result (no card, cancelled, no account, SCA/decline) is a successful body with ok=false, not an HTTP error.
Store the membership belongs to (also authorises the merchant). Never trusted for identity beyond authorisation.
The fee to charge, in pence (min 50, max 100000 = £1,000 fat-finger guard).
REQUIRED reason for the fee, recorded on the ledger and shown to the member (e.g. "PT session", "damaged kit").
Caller-generated token, stable across retries of ONE submission → the Stripe idempotency key (prevents a double-charge).
Whether the fee was charged. false = a friendly, non-error outcome the merchant UI shows.
Stripe PaymentIntent id of the successful charge.
Amount charged, in pence.
Why the charge did not complete (drives the merchant-readable reason).
no_cardno_accountcancellednot_foundinvalidfailedUnderlying Stripe/guard failure code (e.g. authentication_required, store_dark).
Human-readable failure detail.
curl -X POST "https://www.membber.com/api/v1/memberships/00000d1b-0000-4000-8000-d0c500000000/one-off-fee" \
-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",
"amount_pence": 1500,
"note": "Added at the front desk",
"idempotency_key": "1f0e2d3c-4b5a-4678-9abc-def012345678"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships/{id}/one-off-fee", {
params: { path: { id: "00000d1b-0000-4000-8000-d0c500000000" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
amount_pence: 1500,
note: "Added at the front desk",
idempotency_key: "1f0e2d3c-4b5a-4678-9abc-def012345678"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.chargeOneOffFee(
path: .init(id: "00000d1b-0000-4000-8000-d0c500000000"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
amountPence: 1500,
note: "Added at the front desk",
idempotencyKey: "1f0e2d3c-4b5a-4678-9abc-def012345678"
))
).ok.body.json
print(response){
"ok": true,
"payment_intent_id": "0b6642a5-0000-4000-8000-d0c50000000b",
"amount_pence": 1500,
"status": "no_card",
"failure_code": "EXAMPLE10",
"error": "<error>"
}Returns the member’s imported (transferred) membership awaiting setup: plan, preserved first-charge date, the £0-today terms and the consent hash the claim must echo. Null when the member has nothing to claim at this store.
curl -G "https://www.membber.com/api/v1/memberships/claim" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/memberships/claim", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getMembershipClaim(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"claim": {
"membership_id": "bc83d224-0000-4000-8000-d0c5000000bc",
"plan_id": "e28fa9f1-0000-4000-8000-d0c5000000e2",
"plan_name": "<plan_name>",
"price_pence": 1500,
"billing_cycle": "<billing_cycle>",
"first_charge_date": "<first_charge_date>",
"first_charge_pence": 1500,
"requires_setup": true,
"consent_content_hash": "<consent_content_hash>",
"consent_snapshot": "<consent_snapshot>"
}
}Creates the Stripe subscription for an imported membership with the member’s preserved billing date honoured as a free period (nothing charged today), adopting the existing membership row. Returns the SetupIntent used to save the card. Idempotent: retrying resumes the same subscription.
Member accepted the claim billing terms.
Hash from the claim preview, must match the current terms.
Client-generated key; a retried claim resumes the same subscription.
curl -X POST "https://www.membber.com/api/v1/memberships/claim" \
-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",
"billing_consent_accepted": true,
"billing_consent_content_hash": "<billing_consent_content_hash>",
"idempotency_key": "1f0e2d3c-4b5a-4678-9abc-def012345678"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships/claim", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
billing_consent_accepted: true,
billing_consent_content_hash: "<billing_consent_content_hash>",
idempotency_key: "1f0e2d3c-4b5a-4678-9abc-def012345678"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.claimImportedMembership(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
billingConsentAccepted: true,
billingConsentContentHash: "<billing_consent_content_hash>",
idempotencyKey: "1f0e2d3c-4b5a-4678-9abc-def012345678"
))
).ok.body.json
print(response){
"membership_id": "bc83d224-0000-4000-8000-d0c5000000bc",
"requires_setup": true,
"setup_intent_client_secret": "<setup_intent_client_secret>",
"ephemeral_key": "<ephemeral_key>",
"stripe_customer_id": "847b302a-0000-4000-8000-d0c500000084",
"connected_account_id": "231b6f63-0000-4000-8000-d0c500000023",
"first_charge_date": "<first_charge_date>",
"first_charge_pence": 1500
}Verifies the saved card on the claimed subscription and switches the membership on. Safe to retry; the billing webhooks provide the belt-and-braces path if this call is missed.
curl -X POST "https://www.membber.com/api/v1/memberships/claim/confirm" \
-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",
"membership_id": "bc83d224-0000-4000-8000-d0c5000000bc"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships/claim/confirm", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
membership_id: "bc83d224-0000-4000-8000-d0c5000000bc"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.confirmMembershipClaim(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
membershipId: "bc83d224-0000-4000-8000-d0c5000000bc"
))
).ok.body.json
print(response){
"status": "<status>"
}The customer's own membership status at a store: the active membership with classes remaining, or when they have no membership the available plans enriched with capacity (member count, is-full, spots remaining) plus any waitlist entries, the membership display config, and the self-service action config.
Store whose membership to read for the signed-in customer.
What this member pays today, in pence.
What they will pay after the change, in pence.
ISO currency of both amounts.
ISO timestamp the new price takes effect.
ISO timestamp the change was announced.
Whole days until the change lands. Never negative.
Optional note from the store owner.
The member's own view of a refund on this membership. Never includes an amount.
Does this gym take refund requests in the app at all? Per-store, default OFF.
May she open a NEW request right now. False when the gym has it off, the window has closed, the membership has ended, or one is already open. Every clause mirrors a refusal the POST handler enforces, so the screen never offers what the write path will reject.
When the window shuts. Null when unknowable or already shut.
The live or most recent refund request on this membership.
Where her request has got to, in her words rather than the table's. "problem" is a refund that failed and is deliberately not dressed up as still coming, that is how someone waits a week before ringing.
askedapprovedon_its_wayreturneddeclinedproblemWhy she said she was asking.
Her own words, echoed back so the screen can show what she sent.
The owner's reason, and only ever once he has actually decided. Never a number.
curl -G "https://www.membber.com/api/v1/memberships/me" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/memberships/me", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getMyMembership(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"has_membership": true,
"membership": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"plan_id": "e28fa9f1-0000-4000-8000-d0c5000000e2",
"member_number": 1,
"status": "<status>",
"status_changed_at": "<status_changed_at>",
"stripe_subscription_id": "d8bc238b-0000-4000-8000-d0c5000000d8",
"stripe_customer_id": "847b302a-0000-4000-8000-d0c500000084",
"current_period_start": "<current_period_start>",
"current_period_end": "<current_period_end>",
"classes_used_this_cycle": 1,
"cycle_reset_date": "<cycle_reset_date>",
"freeze_start": "<freeze_start>",
"freeze_end": "<freeze_end>",
"freeze_reason": "Added at the front desk",
"freezes_used_this_year": 1,
"total_freeze_days_year": 1,
"grace_period_start": "<grace_period_start>",
"grace_period_end": "<grace_period_end>",
"cancelled_at": "<cancelled_at>",
"cancellation_effective_date": "<cancellation_effective_date>",
"cancellation_reason": "Added at the front desk",
"min_commitment_end": "<min_commitment_end>",
"joined_at": "<joined_at>",
"enrolled_by": "<enrolled_by>",
"notes": "Added at the front desk",
"created_at": "<created_at>",
"updated_at": "<updated_at>",
"plan": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"description": "Added at the front desk",
"plan_type": "<plan_type>",
"billing_cycle": "<billing_cycle>",
"price_pence": 1500,
"currency": "GBP",
"classes_per_cycle": 1,
"credit_expiry_days": 1,
"allowed_class_categories": [
"<allowed_class_categorie>"
],
"allowed_time_start": "<allowed_time_start>",
"allowed_time_end": "<allowed_time_end>",
"allowed_days": [
1
],
"min_commitment_months": 1,
"cancellation_notice_days": 1,
"joining_fee_pence": 1500,
"stripe_product_id": "25d87d9d-0000-4000-8000-d0c500000025",
"stripe_price_id": "588c2783-0000-4000-8000-d0c500000058",
"platform_stripe_price_id": "a1e99fb7-0000-4000-8000-d0c5000000a1",
"is_active": true,
"sort_order": 1,
"highlight_label": "<highlight_label>",
"max_members": 1,
"billing_cycle_type": "<billing_cycle_type>",
"billing_anchor_day": 1,
"billing_anchor_weekday": 1,
"billing_anchor_month": 1,
"proration_enabled": true,
"hero_image_url": "https://example.com/image.jpg",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
},
"classes_remaining": 1,
"available_plans": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"description": "Added at the front desk",
"plan_type": "<plan_type>",
"billing_cycle": "<billing_cycle>",
"price_pence": 1500,
"currency": "GBP",
"classes_per_cycle": 1,
"credit_expiry_days": 1,
"allowed_class_categories": [
"<allowed_class_categorie>"
],
"allowed_time_start": "<allowed_time_start>",
"allowed_time_end": "<allowed_time_end>",
"allowed_days": [
1
],
"min_commitment_months": 1,
"cancellation_notice_days": 1,
"joining_fee_pence": 1500,
"stripe_product_id": "25d87d9d-0000-4000-8000-d0c500000025",
"stripe_price_id": "588c2783-0000-4000-8000-d0c500000058",
"platform_stripe_price_id": "a1e99fb7-0000-4000-8000-d0c5000000a1",
"is_active": true,
"sort_order": 1,
"highlight_label": "<highlight_label>",
"max_members": 1,
"billing_cycle_type": "<billing_cycle_type>",
"billing_anchor_day": 1,
"billing_anchor_weekday": 1,
"billing_anchor_month": 1,
"proration_enabled": true,
"hero_image_url": "https://example.com/image.jpg",
"created_at": "<created_at>",
"updated_at": "<updated_at>",
"member_count": 1,
"is_full": true,
"spots_remaining": 1
}
],
"membership_display": {
"allow_self_service_signup": true,
"show_prices_publicly": true,
"default_picker_view_mode": "<default_picker_view_mode>"
},
"qualifies_for_gym_app_home": true,
"waitlist_entries": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"plan_id": "e28fa9f1-0000-4000-8000-d0c5000000e2",
"plan_name": "<plan_name>",
"position": 1,
"status": "<status>",
"joined_at": "<joined_at>"
}
],
"self_service_config": {
"allow_self_cancel": true,
"allow_self_cancel_reversal": true,
"allow_self_freeze": true,
"allow_self_resume": true,
"allow_plan_change": true
},
"pending_price_change": {
"plan_name": "<plan_name>",
"current_price_pence": 1500,
"new_price_pence": 1500,
"is_increase": true,
"currency": "GBP",
"billing_cycle": "<billing_cycle>",
"effective_date": "<effective_date>",
"announced_at": "<announced_at>",
"days_until_effective": 1,
"owner_message": "Added at the front desk"
},
"refund": {
"enabled": true,
"can_request": true,
"window_closes_at": "<window_closes_at>",
"request": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"stage": "asked",
"reason_code": "Added at the front desk",
"message": "Added at the front desk",
"decision_note": "Added at the front desk",
"created_at": "<created_at>",
"decided_at": "<decided_at>",
"completed_at": "<completed_at>"
}
}
}The signed-in customer cancels their OWN gym membership: gated by the gym allow_self_cancel toggle, it flips the Stripe subscription to cancel at period end and records the cancellation atomically (UPDATE membership + INSERT history in one transaction), then invalidates cache, pushes the wallet update, sends the scheduled-cancellation notification, and recomputes retention analytics. Money-adjacent (stops future billing, no partial refund). Idempotent: a membership already scheduled for cancellation returns already_scheduled=true.
Store whose membership the signed-in customer is cancelling (gates the gym's self-cancel toggle).
Optional member reason recorded on the cancellation history row (defaults to null). Truncated to 500 chars server-side.
ISO timestamp the cancellation takes effect (end of current billing period).
True when a cancellation was already scheduled and this call was a no-op (idempotent).
curl -X POST "https://www.membber.com/api/v1/memberships/me/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"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/memberships/me/cancel", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.cancelMyMembership(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"cancellation_effective_date": "<cancellation_effective_date>",
"already_scheduled": true
}The gym Members › Money dashboard hero: a store's money at a glance, collected this month (net of refunds), money at risk, active/late/failed member counts, and the past-due needs-attention list (failed ranked above late). Store-scoped and business-authed; read-only; every value is real store data and anonymized/archived customers are excluded via the reportable-customers guard.
Store whose money summary to read (also authorises the merchant).
ISO currency of every amount (e.g. "gbp").
This-month collected money (single source: subscription_payment − subscription_refund).
Collected this calendar month to date, NET of refunds, in pence.
Whether the store has EVER taken a membership charge, false ⇒ render a designed blank, never £0.00.
Money the store may not collect (past-due members).
Number of past-due members.
Sum of past-due members monthly price, an honest proxy for money at risk.
Headline member-money counts.
Active paying members.
Past-due members Stripe is still auto-retrying.
Past-due members whose retries are spent.
Past-due members, failed ranked above late (act-now before wait-and-see).
The gym_memberships row id.
The member (customer) id, open their page to act.
Member display name.
The plan they are on.
The member own monthly price in pence (what an overdue cycle is worth).
late = Stripe is still auto-retrying (grace window open); failed = retries spent, act now.
latefailedISO timestamp the member entered trouble, or null.
Why the payment is failing, from the member default card: no_card / card_expired (dead) / declined.
no_cardcard_expireddeclinedThe reason-correct recovery action (never retry a dead card): ask_to_update (no/dead/expired card), remind (declined while still auto-retrying), retry (declined with retries spent).
ask_to_updateremindretryISO timestamp: when set and in the future, this member was asked to update their card within the cooldown window, so the UI shows an "asked, resend in N" state instead of a fresh ask button. Null ⇒ never asked or the cooldown has passed (ask freely).
curl -G "https://www.membber.com/api/v1/memberships/money-summary" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/memberships/money-summary", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getMembershipMoneySummary(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"currency": "GBP",
"collected": {
"month_to_date_net_pence": 1500,
"has_any_charges": true
},
"at_risk": {
"count": 1,
"pence": 1
},
"counts": {
"active": 1,
"late": 1,
"failed": 1
},
"needs_attention": [
{
"membership_id": "bc83d224-0000-4000-8000-d0c5000000bc",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"name": "Example name",
"plan_name": "<plan_name>",
"monthly_pence": 1500,
"state": "late",
"since": "<since>",
"reason": "no_card",
"suggested_action": "ask_to_update",
"ask_cooldown_until": "<ask_cooldown_until>"
}
]
}The Taken half of the gym Money face: a store-wide, newest-first list of real charges, filterable by kind, date window and member, and searchable by description or Stripe object id. Every figure is summed from real ledger rows; an empty window returns real zeros so the client can render a designed blank. Gated on the payments permission because it exposes per-member charge amounts store-wide.
Store whose charges to read (also authorises the merchant).
Inclusive ISO lower bound on charge time. Omit for all time.
Exclusive ISO upper bound on charge time. Omit for up-to-now.
Comma-separated kinds to include (membership,fee,extra,credit,refund). An unknown kind is REJECTED rather than ignored: silently widening a filter would show more money than the merchant asked to see and make the header disagree with the filter chips on screen.
Restrict to one member.
Free text matched against the charge DESCRIPTION and the Stripe object id. Deliberately NOT the member name: names live on the customers table, and filtering a joined column would silently drop rows rather than search them. Name search resolves to a customer_id first.
Page size (default 100, max 500).
The membership_ledger_entries row id.
ISO timestamp the charge was taken; the client groups by day.
The raw ledger event type.
How a charge is filed on the board, driven by the ledger event_type and NOT by the sign of the amount (a downgrade credit and a refund are both negative but are not the same thing to a merchant): membership = subscription payment / upgrade proration; fee = a penalty such as a no-show or late cancel; extra = day passes / PT / anything else taken; credit = goodwill given to the member (credit applied, freeze credit, downgrade credit) which is NOT a refund of a charge; refund = money that went back (subscription or drop-in refund, or a chargeback). There is deliberately no `pack`: no ledger event_type distinguishes a class-pack purchase, and an accepted-but-unmappable filter would return an EMPTY board with zero totals, reading as "you took nothing" rather than "that filter is unavailable".
membershipfeeextracreditrefundSigned pence exactly as recorded. Refunds are negative.
What it was for, as recorded on the ledger row.
The Stripe invoice / charge / refund id. This is the handle a per-period refund targets (target_object_id on the refund contract), so a row can be refunded straight from the board.
The membership this charge belongs to. Required to refund FROM a row: the refund endpoint is membership-scoped, so without it a row is readable but not actionable.
The member name, or NULL when no reportable customer stands behind the row (deleted/anonymized, or a charge with no customer). Never a placeholder: inventing a name over a real charge would be a fabricated fact about money.
Sum of money IN across the whole filtered window (not just the page).
Sum of money OUT as a POSITIVE figure, never netted off the take.
How many refunds in the window, the deck "2 refunded this month".
Sum of goodwill/credit rows as a POSITIVE figure. Neither taken nor refunded.
Money-in breakdown driving the header, biggest first.
How a charge is filed on the board, driven by the ledger event_type and NOT by the sign of the amount (a downgrade credit and a refund are both negative but are not the same thing to a merchant): membership = subscription payment / upgrade proration; fee = a penalty such as a no-show or late cancel; extra = day passes / PT / anything else taken; credit = goodwill given to the member (credit applied, freeze credit, downgrade credit) which is NOT a refund of a charge; refund = money that went back (subscription or drop-in refund, or a chargeback). There is deliberately no `pack`: no ledger event_type distinguishes a class-pack purchase, and an accepted-but-unmappable filter would return an EMPTY board with zero totals, reading as "you took nothing" rather than "that filter is unavailable".
membershipfeeextracreditrefundTRUE when the window held more rows than the totals read could cover, so every figure here is a FLOOR rather than the whole truth and the client must say so. A silently-capped total is the worst failure on this screen: the header is what a merchant reconciles against their bank.
More rows exist in the window than this page holds.
curl -G "https://www.membber.com/api/v1/memberships/taken" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/memberships/taken", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066" } },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.getTakenBoard(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"charges": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"created_at": "<created_at>",
"event_type": "<event_type>",
"kind": "membership",
"amount_pence": 1500,
"currency": "GBP",
"description": "Added at the front desk",
"stripe_object_id": "dedeb0a9-0000-4000-8000-d0c5000000de",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"membership_id": "bc83d224-0000-4000-8000-d0c5000000bc",
"customer_name": "Alex Example"
}
],
"totals": {
"taken_pence": 1500,
"refunded_pence": 1500,
"refund_count": 1,
"charge_count": 1,
"credited_pence": 1500,
"currency": "GBP",
"by_kind": [
{
"kind": "membership",
"count": 1,
"amount_pence": 1500
}
],
"truncated": true
},
"has_more": true
}