8 operations. Every schema and example on this page is generated from the platform contract.
The Business app customer page save. Partial update of a customer the authenticated store actually has a membership row for (anyone else reads as 404, a shop may only edit its own customers). Omit a field to leave it untouched; send an empty string to clear it. The mobile is normalised to E.164 and refused if it is not a UK mobile; the email is trimmed and lower-cased; a mobile or email already sitting on another customer is refused rather than silently moved. A customer who asked to be deleted can no longer be edited. Idempotent: every field is a plain set, so replaying the same body converges on the same record.
The shop whose customer this is. May travel in the body or as ?store_id=; the authenticated store wins either way.
Display name. Trimmed; up to 80 characters. Empty string clears it.
UK mobile in any spelling (07…, +44…, spaces). Stored as E.164. Empty string clears it.
Email address. Trimmed and lower-cased. Empty string clears it.
The shop's own private note about this person, up to 2000 characters. Empty string clears it. Never travels to another shop.
customers.id.
Display name, or null when nobody has added one.
E.164 mobile, or null.
Lower-cased email, or null.
This shop's private note, or null.
When the note was last written (ISO), or null.
Names of the fields this call changed: name, phone, email, merchant_note. Never their values.
curl -X PATCH "https://www.membber.com/api/v1/customers/a1c10999-0000-4000-8000-d0c5000000a1" \
-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",
"name": "Example name",
"phone": "+44 7700 900123",
"email": "alex@example.com",
"merchant_note": "Added at the front desk"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PATCH("/api/v1/customers/{customerId}", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name",
phone: "+44 7700 900123",
email: "alex@example.com",
merchant_note: "Added at the front desk"
},
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.updateCustomer(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name",
phone: "+44 7700 900123",
email: "alex@example.com",
merchantNote: "Added at the front desk"
))
).ok.body.json
print(response){
"customer": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"phone": "+44 7700 900123",
"email": "alex@example.com",
"merchant_note": "Added at the front desk",
"merchant_note_updated_at": "Added at the front desk"
},
"changed_fields": [
"<changed_field>"
]
}The Business-app member drill-down's attendance view: every class booking for one member at one store (attended, no-show, cancelled, confirmed, waitlisted), newest-first, paginated by limit/offset, with the class + occurrence context for each row. Store-scoped and relationship-gated: a customer with no relationship to this store, or a deleted / anonymized member, returns 404. Requires the classes entitlement.
Store whose bookings for this member to read (also authorises the merchant).
Page size (default 25, max 50).
Rows to skip from the newest-first list (default 0).
Booking id.
Booking status: attended / no_show / cancelled / confirmed / waitlisted / promotion_pending / promotion_expired.
Date the booked class is for (YYYY-MM-DD).
How the booking was made (app / staff / drop-in), if recorded.
Class name.
Class category.
Instructor for the occurrence (falls back to the class default).
Occurrence scheduled date, when the booking is tied to an occurrence.
Occurrence start time (HH:MM:SS).
Occurrence end time (HH:MM:SS).
When the member checked in, if they attended.
When the booking was marked a no-show, if it was.
When the booking was cancelled, if it was.
When the booking row was created.
curl -G "https://www.membber.com/api/v1/customers/a1c10999-0000-4000-8000-d0c5000000a1/attendance" \
-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/customers/{customerId}/attendance", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" }, 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.getMemberAttendance(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"bookings": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"status": "<status>",
"booking_date": "<booking_date>",
"booking_source": "<booking_source>",
"class_name": "<class_name>",
"class_category": "<class_category>",
"instructor_name": "<instructor_name>",
"scheduled_date": "<scheduled_date>",
"start_time": "<start_time>",
"end_time": "<end_time>",
"attended_at": "<attended_at>",
"no_show_marked_at": "<no_show_marked_at>",
"cancelled_at": "<cancelled_at>",
"created_at": "<created_at>"
}
],
"pagination": {
"limit": 25,
"offset": 0,
"has_more": true,
"next_offset": 1
}
}A shop stops one person acting here (joining, ordering, messaging), mutes single channels, keeps a private note, or archives someone with history so they leave the lists and counts while every order and stamp is kept. All reversible, all scoped to this shop alone, and none of them a deletion. A customer this shop has no relationship with reads as 404.
The shop making the decision (also authorises the merchant).
true blocks them here, false lifts it (which also clears the note and the one-notice stamp).
Replaces the muted list wholesale. Any of: email, sms, drops_chat, app_message.
The merchant's private note, at most 500 characters. Never sent to the customer.
true hides them from this shop's lists and counts, false brings them back.
Wholly blocked at this shop: no joining, no ordering, no messages.
When this shop blocked them. Null when they are not blocked.
Channels muted while NOT wholly blocked. A block stops all four regardless.
emailsmsdrops_chatapp_messageThe merchant's private note. Shown back to the merchant only; never sent to the customer.
Hidden from this shop's lists and counts. Stops nothing.
curl -X PATCH "https://www.membber.com/api/v1/customers/a1c10999-0000-4000-8000-d0c5000000a1/block" \
-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.PATCH("/api/v1/customers/{customerId}/block", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" } },
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.blockCustomer(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"customer": {
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"blocked": true,
"blocked_at": "<blocked_at>",
"blocked_channels": [
"email"
],
"block_reason": "Added at the front desk",
"archived": true,
"archived_at": "<archived_at>"
}
}The gym-facing read of a member's PAR-Q answers: your own current questions with their latest answers beside each one, when they answered, which version they answered, and when it needs renewing. Store-scoped and relationship-gated, a customer who is not a member of the authenticated store reads as 404. A member who has never answered returns an empty answer list with `required_by_gym` telling you whether you ask at all. Answers stored under questions you have since removed are still returned, because the member did give them.
The gym whose questionnaire this is (also authorises the merchant).
Always present for a member of this store; empty answers mean nothing on file.
Whether this gym asks for a questionnaire at all, so the screen can tell "nobody asked" from "asked, not answered yet".
When the member submitted it. Null if they never have.
When this submission stops counting, if the gym expires them.
Question-set version the member answered.
The gym's CURRENT questions with this member's answers. An answer stored under a question since removed is still listed, because the member did give it.
Question id as stored in the gym's question set.
The question as the member was asked it.
Question type: yes_no, text, or number.
Free-text or numeric answer, as text. Null for a yes/no question.
Yes/no answer. Null when the question was never answered, deliberately distinct from "answered no".
curl -G "https://www.membber.com/api/v1/customers/a1c10999-0000-4000-8000-d0c5000000a1/health-questionnaire" \
-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/customers/{customerId}/health-questionnaire", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" }, 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.getMemberHealthQuestionnaire(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"questionnaire": {
"required_by_gym": true,
"completed_at": "<completed_at>",
"expires_at": "<expires_at>",
"version": -9007199254740991,
"answers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"question": "<question>",
"type": "<type>",
"answer_text": "<answer_text>",
"answer_bool": true
}
]
}
}The Business-app customer-detail timeline: a single chronological (newest-first) feed interleaving every recorded touchpoint for one member at one store, paginated by a before cursor. Order events carry how the order was paid (wallet, card brand and last four, funding) as recorded at the moment the charge succeeded. Also returns the page header (how this person came to the shop, the shop's own note about them) and whether the customer has any store-scoped agreements on record. Store-scoped and relationship-gated: a customer with no relationship at this store returns 404.
Store whose journey with the customer to read (also authorises the merchant).
Page size (default 25, max 50).
ISO 8601 cursor, return events strictly older than this timestamp.
joinedstampvoucher_earnedvoucher_redeemedordermessageemailjourneymomentclass_bookingmembershipin_storein_appemailjourneymomentgymStripe's payment_method_details.type when the charge succeeded: card, link, paypal …
apple_pay | google_pay | link … from card.wallet.type. Null for a plain card.
visa | mastercard | amex … Null when Stripe could not identify the network.
The last four digits only, recognition, never a PAN and never a token.
credit | debit | prepaid | unknown, as Stripe reports it.
The same fact already worded for a human: "Apple Pay", "Visa ending 4242". Null when nothing was recorded, so a caller prints a blank rather than guessing "Card".
How this person arrived. Null when there is no acquisition row, which renders as nothing.
Raw acquisition enum. For filtering and telemetry, never printed.
The finished shop-facing sentence, e.g. "Joined the alert list".
When they first met this shop (ISO 8601).
The batch that won them, where one did. Null on every other door.
The shop's own note about this person. Per-store on purpose: a bakery's note never travels to the gym down the road. Null (never an empty string) when there is no note.
curl -G "https://www.membber.com/api/v1/customers/a1c10999-0000-4000-8000-d0c5000000a1/journey" \
-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/customers/{customerId}/journey", {
params: { path: { customerId: "a1c10999-0000-4000-8000-d0c5000000a1" }, 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.getCustomerJourney(
path: .init(customerId: "a1c10999-0000-4000-8000-d0c5000000a1"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"events": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"type": "joined",
"channel": "in_store",
"timestamp": "<timestamp>",
"title": "Morning class",
"subtitle": "<subtitle>",
"status": "<status>",
"detail": "<detail>",
"campaignName": "<campaignName>",
"opened": true,
"clicked": true,
"returningVisitAt": "<returningVisitAt>",
"returnDays": 1,
"amountPence": 1500,
"refundedPence": 1,
"promoDiscountPence": 1,
"appliedPromoCode": "EXAMPLE10",
"currency": "GBP",
"paymentMethodKind": "<paymentMethodKind>",
"paymentWallet": "<paymentWallet>",
"paymentCardBrand": "<paymentCardBrand>",
"paymentCardLast4": "<paymentCardLast4>",
"paymentCardFunding": "<paymentCardFunding>",
"paymentMethod": "<paymentMethod>",
"autoSent": true,
"resultKind": "<resultKind>",
"resultAt": "<resultAt>",
"resultValuePence": 1
}
],
"header": {
"cameFrom": {
"via": "<via>",
"label": "<label>",
"atISO": "<atISO>",
"dropName": "<dropName>"
},
"merchantNote": "Added at the front desk"
},
"pagination": {
"limit": 25,
"before": "<before>",
"nextCursor": "<nextCursor>",
"hasMore": true
},
"has_store_agreements": true
}The same change applied to a group in ONE statement: either every named person is changed or nobody is. If any id is not already this shop's customer the call changes nothing and says how many were unknown.
The shop making the decision (also authorises the merchant).
The people this change applies to. All of them must already be this shop's customers.
true blocks them here, false lifts it (which also clears the note and the one-notice stamp).
Replaces the muted list wholesale. Any of: email, sms, drops_chat, app_message.
The merchant's private note, at most 500 characters. Never sent to the customer.
true hides them from this shop's lists and counts, false brings them back.
How many customers the change was applied to.
Wholly blocked at this shop: no joining, no ordering, no messages.
When this shop blocked them. Null when they are not blocked.
Channels muted while NOT wholly blocked. A block stops all four regardless.
emailsmsdrops_chatapp_messageThe merchant's private note. Shown back to the merchant only; never sent to the customer.
Hidden from this shop's lists and counts. Stops nothing.
curl -X PATCH "https://www.membber.com/api/v1/customers/block" \
-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_ids": [
"96607d1c-0000-4000-8000-d0c500000096"
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PATCH("/api/v1/customers/block", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
customer_ids: [
"96607d1c-0000-4000-8000-d0c500000096"
]
},
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.blockCustomers(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
customerIds: ["96607d1c-0000-4000-8000-d0c500000096"]
))
).ok.body.json
print(response){
"updated": 1,
"customers": [
{
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"blocked": true,
"blocked_at": "<blocked_at>",
"blocked_channels": [
"email"
],
"block_reason": "Added at the front desk",
"archived": true,
"archived_at": "<archived_at>"
}
]
}Merchant browses their full customer roster, everyone who has interacted with the store, enriched with lifecycle stage, visits, stamp count, order count + net spend, and marketing consent. Supports stage/facet/text filters, sort by recent/spend/visits/name, and pagination with a true filtered total_count.
Store whose roster to list (also authorises the merchant).
Sort order (default recent).
recentspendvisitsnameLifecycle-stage filter.
newactivestalelapsedreturningComma-separated AND filters: has_orders, has_stamps, marketing_opted_in.
Filter the roster by name/email/phone.
Page size (default 30, max 100).
Page offset.
curl -G "https://www.membber.com/api/v1/customers/list" \
-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/customers/list", {
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.listCustomers(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"customers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"phone": "+44 7700 900123",
"email": "alex@example.com",
"last_visit_at": "<last_visit_at>",
"lifecycle_stage": "<lifecycle_stage>",
"total_visits": 1,
"stamp_count": 1,
"order_count": 1,
"total_spent_pence": 1500,
"marketing_opted_in": true
}
],
"pagination": {
"limit": 25,
"offset": 0,
"has_more": true,
"total_count": 1
}
}Merchant searches the customers who have interacted with their store (class bookings, stamps, or paid orders). A 2+ character term filters by name/phone/email; a short/empty term returns the most-recent customers as a list.
Store whose customers to search (also authorises the merchant).
Search term; 2+ chars filters by name/phone/email, else returns the most-recent 20.
curl -G "https://www.membber.com/api/v1/customers/search" \
-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/customers/search", {
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.searchCustomers(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"customers": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"phone": "+44 7700 900123",
"email": "alex@example.com",
"last_seen": "<last_seen>"
}
]
}