24 operations. Every schema and example on this page is generated from the platform contract.
Cross-store ACTIVE collect legs for the wallet Collections hub. One row per in-flight pickup (paid, not collected/cancelled) across every store the customer holds. Caller-only, index-backed, light by design so it is cheap to poll while a pickup is in flight.
The collect leg's own guest_orders id (child_collect for a mixed leg, else the standalone order id).
The order the customer owns (parent_order_id for a mixed leg, else leg_id).
Display order number (the parent's for a mixed leg).
Store has live chat → the tile can offer "Message <store>".
none | accepted | preparing | ready (active legs are never collected/cancelled).
What this leg's QR encodes: the leg identity, HMAC-signed. The short collection_code is a human label and is NOT sufficient to collect.
'now' (ASAP) | 'scheduled' (booked slot).
ISO instant of the booked slot (scheduled legs); null for ASAP.
Merchant-announced NEW ready time (the order_delayed flow); null = no announced delay.
The merchant's own words for the delay, verbatim.
curl -G "https://www.membber.com/api/v1/customer/active-collections" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/active-collections");
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.getActiveCollections().ok.body.json
print(response){
"legs": [
{
"leg_id": "be0aecec-0000-4000-8000-d0c5000000be",
"order_id": "4991ffac-0000-4000-8000-d0c500000049",
"order_number": "<order_number>",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"store_name": "<store_name>",
"store_logo_url": "https://example.com/image.jpg",
"store_logo_blurhash": "<store_logo_blurhash>",
"store_primary_color": "<store_primary_color>",
"store_secondary_color": "<store_secondary_color>",
"messaging_enabled": true,
"fulfilment_status": "<fulfilment_status>",
"collection_code": "EXAMPLE10",
"collect_token": "<collect_token>",
"prep_minutes": 1,
"collection_mode": "<collection_mode>",
"requested_collection_at": "<requested_collection_at>",
"accepted_at": "<accepted_at>",
"preparing_at": "<preparing_at>",
"ready_at": "<ready_at>",
"revised_ready_at": "<revised_ready_at>",
"delay_note": "Added at the front desk",
"created_at": "<created_at>",
"paid_at": "<paid_at>",
"currency": "GBP",
"total_pence": 1500,
"refunded_pence": 1500,
"item_count": 1,
"items": [
{
"name": "Example name",
"quantity": 1
}
]
}
]
}The member's unified history, stores joined, stamps, vouchers earned and redeemed, memberships and bookings, merged across every store they belong to and sorted newest first. Caller-only: the feed is scoped to the session identity and cannot be asked for on behalf of anyone else.
Rows per page, 1-50. Defaults to 10. (Coerced: it arrives as a query string.)
Where to start, 0-5000. Defaults to 0. (Coerced: it arrives as a query string.)
Stable id for this row, unique within the feed.
Which kind of thing happened.
storestampvouchermembershipbookingHeadline line, already humanised (e.g. "Free coffee earned").
Secondary line, usually the store name or the reward.
ISO instant the thing happened. The feed is sorted by this, newest first.
The store's short public code; falls back to 'store' when unknown.
The store's display name; falls back to the code, then 'Store'.
Null for a row whose store row has since gone.
Row-specific state (e.g. a booking's status); null when the type has none.
Optional third line of context.
Voucher rows only, the code shown at the counter.
Voucher rows only, what the reward is.
Voucher rows only, ISO instant it was spent; null while unspent.
The store's logo, for the row's avatar.
Offset pagination over the feed.
How many rows this page asked for, after clamping.
Where this page started.
Offset to pass for the next page; null when there is no next page.
Whether another page exists.
curl -G "https://www.membber.com/api/v1/customer/activity" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/activity");
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.getCustomerActivity().ok.body.json
print(response){
"activity": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"type": "store",
"title": "Morning class",
"subtitle": "<subtitle>",
"timestamp": "<timestamp>",
"storeCode": "EXAMPLE10",
"storeName": "<storeName>",
"storeId": "8fb04ddc-0000-4000-8000-d0c50000008f",
"status": "<status>",
"detail": "<detail>",
"redemptionCode": "EXAMPLE10",
"rewardTitle": "<rewardTitle>",
"redeemedAt": "<redeemedAt>",
"storeLogoUrl": "https://example.com/image.jpg"
}
],
"pagination": {
"limit": 25,
"offset": 0,
"nextOffset": -9007199254740991,
"hasMore": true
}
}The signed-in customer's own booking for a specific class occurrence, with the occurrence and class context the iOS ClassBooking decoder needs (schedule, instructor, capacity, class details, promotion / reconfirmation state). Read-only. Only the caller own booking is ever returned; cancelled rows are excluded.
Booking id.
Booking status (waitlisted / promotion_pending / confirmed).
Rank in the waitlist queue, if waitlisted.
Whether the waitlist position is locked.
Date the booking was made.
Row creation timestamp.
The class occurrence this booking is for.
Occurrence scheduled date.
Occurrence start time.
Occurrence end time.
Instructor for the occurrence.
Occurrence maximum capacity.
Number of confirmed bookings on the occurrence.
Occurrence status.
Class name.
Class category.
Class description.
Class image URL.
Class difficulty.
Class duration in minutes.
When a pending promotion offer expires.
Whether reconfirmation is required.
Reconfirmation deadline.
When the booking was reconfirmed.
curl -G "https://www.membber.com/api/v1/customer/bookings/by-occurrence/a987c2cc-0000-4000-8000-d0c5000000a9" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/bookings/by-occurrence/{occurrenceId}", {
params: { path: { occurrenceId: "a987c2cc-0000-4000-8000-d0c5000000a9" } },
});
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.getCustomerBookingByOccurrence(
path: .init(occurrenceId: "a987c2cc-0000-4000-8000-d0c5000000a9")
).ok.body.json
print(response){
"booking": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"status": "<status>",
"waitlistPosition": 1,
"waitlistLocked": true,
"bookingDate": "<bookingDate>",
"createdAt": "<createdAt>",
"occurrenceId": "a987c2cc-0000-4000-8000-d0c5000000a9",
"scheduledDate": "<scheduledDate>",
"startTime": "<startTime>",
"endTime": "<endTime>",
"instructorName": "<instructorName>",
"maxCapacity": 1,
"confirmedCount": 1,
"occurrenceStatus": "<occurrenceStatus>",
"className": "<className>",
"category": "<category>",
"description": "Added at the front desk",
"imageUrl": "https://example.com/image.jpg",
"difficulty": "<difficulty>",
"durationMinutes": 1,
"promotionExpiresAt": "<promotionExpiresAt>",
"reconfirmationRequired": true,
"reconfirmationDeadline": "<reconfirmationDeadline>",
"reconfirmedAt": "<reconfirmedAt>"
}
}The signed-in customer cancels one of their own class bookings or leaves a waitlist. Flips status + releases the seat + re-numbers the waitlist + cascades any pending promotion (cancel_class_booking), then classifies the drop-in refund early-vs-late (store-timezone aware) and issues it on the connected account. Idempotent at the service layer: the refund is deduped per (booking, trigger) and an already-cancelled booking is rejected, so a retry never double-refunds or double-releases a seat.
The caller own booking to cancel / leave. Ownership is enforced server-side against the session.
The booking was cancelled / the waitlist was left.
The drop-in refund outcome for this cancellation (membership-paid bookings report a no-payment skip).
Refund outcome (succeeded / already_succeeded / skipped_no_payment / failed / ...).
How the refund was classified: voluntary_leave_early (full) or voluntary_leave_late (minus fee).
Amount refunded to the member, in pence.
Amount withheld (late-leave fee), in pence.
Stripe refund id, when a refund was actually issued.
Non-fatal refund error message, if the refund engine failed.
curl -X POST "https://www.membber.com/api/v1/customer/bookings/cancel" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"booking_id": "ae11dec1-0000-4000-8000-d0c5000000ae"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/customer/bookings/cancel", {
body: {
booking_id: "ae11dec1-0000-4000-8000-d0c5000000ae"
},
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.cancelClassBooking(
body: .json(.init(
bookingId: "ae11dec1-0000-4000-8000-d0c5000000ae"
))
).ok.body.json
print(response){
"left": true,
"refund": {
"status": "<status>",
"triggerReason": "Added at the front desk",
"amountRefundedPence": 1500,
"amountWithheldPence": 1500,
"stripeRefundId": "41b31220-0000-4000-8000-d0c500000041",
"errorMessage": "Added at the front desk"
}
}Soft 5-minute stock hold for a customer basket. On add/quantity change the client sets the hold to the new quantity and reads back `granted` (so two shoppers can't both hold the last unit); on basket clear it releases every hold. Holds lapse on their own after 5 minutes. Idempotent, a set replaces (never stacks) the cart's hold.
Opaque per-basket token the client generates.
true → release every hold for this cart (item_id/quantity ignored).
Stock-tracked menu item to hold (UUID). Required unless clear=true.
Desired hold quantity (clamped 0..99). Required unless clear=true.
false when the item isn't stock-tracked (nothing held).
How much of the requested quantity was actually held (the client caps the basket to this).
Units still available to this cart after the grant (null when untracked).
How many holds were released (clear mode only).
curl -X POST "https://www.membber.com/api/v1/customer/cart-hold" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"cart_token": "<cart_token>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/customer/cart-hold", {
body: {
cart_token: "<cart_token>"
},
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.setCartHold(
body: .json(.init(
cartToken: "<cart_token>"
))
).ok.body.json
print(response){
"tracked": true,
"granted": 1,
"available": 1,
"cleared": 1
}Customer mini-store collection slot picker: the open slots for the date (with tz-correct start instants) and, per collectable menu item, the earliest instant it can be collected (now + its lead time). The app greys an item for a chosen slot when earliest_collectable_at > slot.start_at.
Store whose collection day-view to fetch.
Collection date (YYYY-MM-DD, interpreted in the store timezone).
The requested date (echoed).
HH:MM
HH:MM
ISO instant (store-tz aware).
ISO instant = now + max(item prep, store default). Grey the item for a slot when this > slot.start_at.
curl -G "https://www.membber.com/api/v1/customer/collection-slots" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "date=<date>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/collection-slots", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066", date: "<date>" } },
});
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.getCollectionDay(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", date: "<date>")
).ok.body.json
print(response){
"date": "<date>",
"slots": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"start_time": "<start_time>",
"end_time": "<end_time>",
"orders_remaining": 1,
"max_orders": 1,
"full": true,
"start_at": "<start_at>"
}
],
"items": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"collect_eligible": true,
"earliest_collectable_at": "<earliest_collectable_at>"
}
]
}The customer app's "Your agreements" screen: every consent decision the member has recorded, across all of their stores plus the platform-scope Terms/Privacy, grouped by (agreement type, store) with each store's name, the full history newest first, and an up-to-date flag versus the active definition. The customer is always resolved from the session, never a client-supplied id.
acceptedrejectedwithdrawnacceptedrejectedwithdrawncurl -G "https://www.membber.com/api/v1/customer/consent/mine" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/consent/mine");
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.getCustomerConsents().ok.body.json
print(response){
"groups": [
{
"agreement_type": "<agreement_type>",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"store_name": "<store_name>",
"latest_decision": "accepted",
"latest_version": 1,
"latest_accepted_at": "<latest_accepted_at>",
"active_version": 1,
"up_to_date": true,
"events": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"decision": "accepted",
"agreement_version": 1,
"accepted_at": "<accepted_at>",
"source": "<source>",
"terms_snapshot": "<terms_snapshot>",
"terms_url": "https://example.com/image.jpg",
"ip_address": "<ip_address>",
"user_agent": "<user_agent>",
"signature_image_url": "https://example.com/image.jpg",
"metadata": {}
}
]
}
]
}Records a customer accept / reject / withdraw decision against the active definition for a platform agreement type (Terms, Privacy, marketing), freezing the wording at the version validated. Powers the onboarding "Before you start" gate and the "Your agreements" withdraw affordance. The customer is always resolved from the session, never a client-supplied id. Appends to the immutable consent_events ledger.
Agreement type to record against. One of: platform_terms, platform_privacy, marketing_email.
The member's decision. One of: accepted, rejected, withdrawn.
Consent scope: the store the agreement belongs to (null / omitted = platform scope). NOT an identity field.
If supplied, must match the active definition hash (else CONSENT_STALE 409), guards a TOCTOU where the wording changed after the client fetched it.
curl -X POST "https://www.membber.com/api/v1/customer/consent/record" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"agreement_type": "<agreement_type>",
"decision": "<decision>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/customer/consent/record", {
body: {
agreement_type: "<agreement_type>",
decision: "<decision>"
},
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.recordConsentDecision(
body: .json(.init(
agreementType: "<agreement_type>",
decision: "<decision>"
))
).ok.body.json
print(response){
"event_id": "1093c0e0-0000-4000-8000-d0c500000010",
"version": 1
}Drives the onboarding "Before you start" gate: the platform Terms and Privacy (required, blocking) and the marketing opt-in (optional), each with whether the signed-in customer already satisfies it at the current version. needsGate is true when any required agreement is unsatisfied. The customer is always resolved from the session, never a client-supplied id.
member has a valid current-version ACCEPTANCE
member made ANY current-version decision (accepted OR rejected), so a declined optional (marketing) is not re-asked as if never seen
curl -G "https://www.membber.com/api/v1/customer/consent/required" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/consent/required");
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.getRequiredConsents().ok.body.json
print(response){
"needsGate": true,
"consents": [
{
"agreement_type": "<agreement_type>",
"definition_id": "bc15d787-0000-4000-8000-d0c5000000bc",
"version": 1,
"terms_text": "<terms_text>",
"terms_url": "https://example.com/image.jpg",
"terms_hash": "<terms_hash>",
"required": true,
"satisfied": true,
"answered": true
}
]
}The customer app's emergency-contact save: upserts the store-scoped member_emergency_contacts row (insert when no id is supplied, update the customer-scoped row when an id is), then recomputes onboarding progress (non-fatal, a failure is logged, never surfaced). Returns the saved contact. Idempotency is rpc-mode: the update path is idempotent (scoped by id + customer_id, converges to the same row on retry); the create path has no HTTP dedup, exactly matching the legacy route which sent no Idempotency-Key. The customer is always resolved from the session, never a client-supplied id.
Store the emergency contact belongs to (the contact is store-scoped).
Existing contact id to update; omit to create a new contact.
Emergency contact's full name (required).
Emergency contact's phone number (required).
Relationship to the member (e.g. Partner, Parent).
Whether this is the primary emergency contact (defaults to false).
member_emergency_contacts row id.
The signed-in customer id (owner of the contact).
Store the contact belongs to.
curl -X POST "https://www.membber.com/api/v1/customer/emergency-contacts" \
-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",
"contact_name": "<contact_name>",
"contact_phone": "+44 7700 900123"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/customer/emergency-contacts", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
contact_name: "<contact_name>",
contact_phone: "+44 7700 900123"
},
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.upsertEmergencyContact(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
contactName: "<contact_name>",
contactPhone: "+44 7700 900123"
))
).ok.body.json
print(response){
"contact": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"contact_name": "<contact_name>",
"contact_phone": "+44 7700 900123",
"relationship": "<relationship>",
"is_primary": true,
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}A store's active, upcoming special events sorted by date, each enriched with an is_booked flag set from the signed-in customer's own confirmed bookings. Read-only. The customer is resolved from the session; a client-supplied id is never trusted for identity.
Store whose upcoming events to list.
Event id.
Store the event belongs to.
Event name.
Event description.
Event date (YYYY-MM-DD).
Event start time.
Event end time, if set.
Event location, if set.
Ticket price in pence (0 = free).
ISO currency code for the price.
Capacity cap, if any.
Whether the event is restricted to members.
Whether the event is active.
Event image URL, if set.
Merchant user who created the event, if recorded.
Row creation timestamp.
Row last-update timestamp.
True when the signed-in customer holds a confirmed booking for this event.
curl -G "https://www.membber.com/api/v1/customer/events" \
-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/customer/events", {
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.getCustomerUpcomingEvents(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"events": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"description": "Added at the front desk",
"event_date": "<event_date>",
"start_time": "<start_time>",
"end_time": "<end_time>",
"location": "<location>",
"price_pence": 1500,
"currency": "GBP",
"max_capacity": 1,
"is_members_only": true,
"is_active": true,
"image_url": "https://example.com/image.jpg",
"created_by": "<created_by>",
"created_at": "<created_at>",
"updated_at": "<updated_at>",
"is_booked": true
}
]
}Returns the most recent refund request on this membership (or null), plus whether a NEW one may be raised and a plain-English reason when it may not. Read-only. Never returns the suggested refund figure: that is the owner's starting point, not a promise to the member.
The gym the membership belongs to.
The most recent request, or null when they have never asked.
requested | approved | refunding | refunded | partially_refunded | rejected | failed
A sentence for the member when can_request is false. Null when they can ask.
curl -G "https://www.membber.com/api/v1/customer/memberships/1edaf391-0000-4000-8000-d0c50000001e/refund-request" \
-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/customer/memberships/{membershipId}/refund-request", {
params: { path: { membershipId: "1edaf391-0000-4000-8000-d0c50000001e" }, 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.getMembershipRefundRequest(
path: .init(membershipId: "1edaf391-0000-4000-8000-d0c50000001e"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"request": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"status": "<status>",
"reason_code": "Added at the front desk",
"created_at": "<created_at>",
"decided_at": "<decided_at>",
"decision_note": "Added at the front desk",
"refunded_pence": 1500
},
"can_request": true,
"blocked_reason": "Added at the front desk"
}Creates a refund_requests row (status=requested) for the gym OWNER to approve or reject. NO money moves here and no entitlement changes, approval is always a human merchant decision. Validates that the membership belongs to the caller, that it has not already ended, that the gym accepts refund requests at all (per-store opt-in, default off), and that the refund window is still open. Freezes what the member had and had used onto the row, so the answer to "why this amount" survives the membership changing afterwards. A duplicate open request 409s, idempotent via a unique index.
The gym the membership belongs to.
Why the member is asking. Gym-shaped, not shop-shaped.
injuredmoving_awaynot_as_describedbought_by_mistakeotherWhat the member wants to say about it, in their own words. Shown to the owner.
Always "requested" here, approval is a human merchant decision.
curl -X POST "https://www.membber.com/api/v1/customer/memberships/1edaf391-0000-4000-8000-d0c50000001e/refund-request" \
-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",
"reason_code": "injured"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/customer/memberships/{membershipId}/refund-request", {
params: { path: { membershipId: "1edaf391-0000-4000-8000-d0c50000001e" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
reason_code: "injured"
},
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.requestMembershipRefund(
path: .init(membershipId: "1edaf391-0000-4000-8000-d0c50000001e"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
reasonCode: .injured
))
).ok.body.json
print(response){
"request": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"status": "<status>",
"created_at": "<created_at>"
}
}The Membber Customer iOS Orders list/detail feed: real persisted guest_orders scoped to the signed-in customer for one store. Caller-only, paid-only, keyset-paginated. details=false returns the LIGHT response for fast first render (summary rows, no thumbnails/collect children/loyalty aggregation).
Store whose order history to list.
Page size (default 20, max 50).
Keyset cursor ("<created_at>|<id>") from a prior next_cursor.
Anything other than a case-insensitive "false" → FULL response; "false" → LIGHT response (summary rows only, no thumbnails/children/loyalty). Default true.
Store branding + legal/receipt fields; null for an unknown store.
paid | ready | fulfilled | partially_refunded | refunded | disputed | cancelled
'buy_now' | 'collect'
Signed QR payload for a STANDALONE collect order; null for buy-now and mixed parents (each leg carries its own).
The store rate only when it verifiably matches this order's snapshot, else 0.
C74: the merchant's reason for the most recent refund/rejection, for the customer's order history. null when never refunded / no reason given.
C77: when the refund was actually issued. The screen used to print `paid_at` here, so it told the customer the time they PAID.
C77: an honest arrival date, 5 UK WORKING days after the refund. Always worded 'by', never 'on', the bank decides the day, not us.
Merchant-announced NEW ready time (order_delayed flow); null = no announced delay.
The merchant's own words for the delay, verbatim.
Joined from menu_items; null when the item was deleted.
reorderreceipttrackmessagereport'standalone' | 'parent'
B358, the hand-over mode STAMPED on the order at checkout, not the store setting now. Null for legacy orders before stamping; client defaults to counter_collection.
What this leg's QR encodes: the leg identity, HMAC-signed. The short collection_code is a human label and is NOT sufficient to collect.
none | accepted | preparing | ready | collected | cancelled
Merchant-announced NEW ready time for this leg (order_delayed flow); null = no announced delay.
The merchant's own words for the delay, verbatim.
Joined from menu_items; null when the item was deleted.
in_progress | ready | complete | not_collected | cancelled | refunded. not_collected = every pickup ended as a no-show and nothing was collected: finished, but never a success, so it is not folded into complete.
Pass as ?before= for the next page.
curl -G "https://www.membber.com/api/v1/customer/orders" \
-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/customer/orders", {
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.listCustomerStoreOrders(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"store": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code": "EXAMPLE10",
"name": "Example name",
"logo_url": "https://example.com/image.jpg",
"primary_color": "<primary_color>",
"reward_title": "<reward_title>",
"address": "<address>",
"phone": "+44 7700 900123",
"email": "alex@example.com",
"vat_registered": true,
"vat_rate_percent": 1,
"vat_number": "<vat_number>"
},
"orders": [
{
"order_id": "4991ffac-0000-4000-8000-d0c500000049",
"order_number": "<order_number>",
"status": "<status>",
"order_type": "<order_type>",
"fulfilment_status": "<fulfilment_status>",
"collection_code": "EXAMPLE10",
"collect_token": "<collect_token>",
"prep_minutes": 1,
"collection_mode": "<collection_mode>",
"requested_collection_at": "<requested_collection_at>",
"currency": "GBP",
"subtotal_pence": 1500,
"discount_pence": 1500,
"promo_discount_pence": 1500,
"voucher_discount_pence": 1500,
"applied_promo_code": "EXAMPLE10",
"applied_promo_label": "<applied_promo_label>",
"tip_pence": 1500,
"service_charge_pence": 1500,
"vat_pence": 1500,
"vat_rate_percent": 1,
"total_pence": 1500,
"refunded_pence": 1500,
"refund_reason": "Added at the front desk",
"refunded_at": "<refunded_at>",
"refund_expected_by": "<refund_expected_by>",
"created_at": "<created_at>",
"paid_at": "<paid_at>",
"accepted_at": "<accepted_at>",
"preparing_at": "<preparing_at>",
"ready_at": "<ready_at>",
"revised_ready_at": "<revised_ready_at>",
"delay_note": "Added at the front desk",
"collected_at": "<collected_at>",
"item_count": 1,
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"quantity": 1,
"unit_amount_pence": 1500,
"line_total_pence": 1500,
"image_url": "https://example.com/image.jpg"
}
],
"allowed_actions": [
"reorder"
],
"loyalty_awarded": true,
"stamps_earned": 1,
"voucher_earned": true,
"fulfilment_role": "<fulfilment_role>",
"handed_over_at": "<handed_over_at>",
"order_fulfilment_mode": "<order_fulfilment_mode>",
"groups": [
{
"child_order_id": "f1ff3b6f-0000-4000-8000-d0c5000000f1",
"collection_code": "EXAMPLE10",
"collect_token": "<collect_token>",
"fulfilment_status": "<fulfilment_status>",
"prep_minutes": 1,
"collection_mode": "<collection_mode>",
"requested_collection_at": "<requested_collection_at>",
"accepted_at": "<accepted_at>",
"preparing_at": "<preparing_at>",
"ready_at": "<ready_at>",
"revised_ready_at": "<revised_ready_at>",
"delay_note": "Added at the front desk",
"collected_at": "<collected_at>",
"total_pence": 1500,
"refunded_pence": 1500,
"item_count": 1,
"items": [
{
"menu_item_id": null,
"name": null,
"quantity": null,
"unit_amount_pence": null,
"line_total_pence": null,
"image_url": null
}
]
}
],
"overall_status": "<overall_status>"
}
],
"next_cursor": "<next_cursor>",
"has_more": true
}A customer cancels their own paid Click & Collect order and is refunded in full, with no merchant approval, allowed ONLY while the order is still waiting to be accepted, because up to that point nothing has been made and nothing has been spent. Once the shop taps Accept this returns 409 and the customer is pointed at the shop instead.
What went back to the card, in pence.
curl -X POST "https://www.membber.com/api/v1/customer/orders/b80cf509-0000-4000-8000-d0c5000000b8/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/customer/orders/{orderId}/cancel", {
params: { path: { orderId: "b80cf509-0000-4000-8000-d0c5000000b8" } },
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.cancelMyOrder(
path: .init(orderId: "b80cf509-0000-4000-8000-d0c5000000b8"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"cancelled": true,
"refunded_pence": 1500
}Creates a refund_requests row (status=requested) for the merchant to approve or reject. NO money moves here, approval is always a human merchant decision. Validates order ownership, refundability, the refund window, and a remaining balance; a duplicate open request 409s (idempotent via the unique index).
Store the order belongs to.
One of item_missing | wrong_item | quality | not_received | other.
Optional free-text detail from the customer.
Optional partial amount in pence; omit/null to request the full remaining balance.
curl -X POST "https://www.membber.com/api/v1/customer/orders/b80cf509-0000-4000-8000-d0c5000000b8/refund-request" \
-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",
"reason_code": "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.POST("/api/v1/customer/orders/{orderId}/refund-request", {
params: { path: { orderId: "b80cf509-0000-4000-8000-d0c5000000b8" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
reason_code: "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.requestOrderRefund(
path: .init(orderId: "b80cf509-0000-4000-8000-d0c5000000b8"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
reasonCode: "Added at the front desk"
))
).ok.body.json
print(response){
"request": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"status": "<status>",
"reason_code": "Added at the front desk",
"created_at": "<created_at>"
}
}Powers the one-tap "Order again" card. Returns the single newest paid order for the signed-in customer at one store, trimmed to order_number + order_type + collection_mode + bare item lines. The client re-prices and re-validates each line against the CURRENT menu, so no stale prices are ever returned.
Store whose most-recent paid order to seed a reorder from.
The newest paid order trimmed to what a reorder needs; null when there is none.
'buy_now' | 'collect'
'now' | 'scheduled' | null
curl -G "https://www.membber.com/api/v1/customer/orders/recent" \
-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/customer/orders/recent", {
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.getRecentStoreOrder(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"order": {
"order_number": "<order_number>",
"order_type": "<order_type>",
"collection_mode": "<collection_mode>",
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"name": "Example name",
"quantity": 1
}
]
}
}Returns the active card links for the authenticated customer, so they can see what was remembered on their behalf and revoke any of it. Scoped to the caller, the session identifies the customer, never a parameter. Revoked links are omitted: they are history, not something to show back as if still on.
Narrow to one store. Omit for every store this customer has a remembered card at.
Active (un-revoked) links only, newest first.
Pass to revokePosCardLink to forget this card.
For display; null if the store row is unreadable.
e.g. "VISA". Recognition only.
e.g. "4242". Recognition only, never enough to transact.
When the customer agreed to this card being remembered (ISO-8601).
Last time this card auto-matched a payment (ISO-8601).
How many payments this link has matched.
curl -G "https://www.membber.com/api/v1/customer/pos-card-links" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/pos-card-links");
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.listPosCardLinks().ok.body.json
print(response){
"cards": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"store_name": "<store_name>",
"card_brand": "<card_brand>",
"card_last4": "<card_last4>",
"consented_at": "<consented_at>",
"last_used_at": "<last_used_at>",
"times_used": -9007199254740991
}
]
}Stops a card auto-matching this customer at that store, immediately. A tombstone rather than a delete, so the audit trail survives and the card is free for its next owner. Idempotent: revoking an already-revoked or unknown link returns { revoked: false } and 200, because the customer's intent holds either way.
The card link to forget (from listPosCardLinks).
true if this call revoked it; false if it was already revoked or is not the caller's. Either way the card is not remembered, so the request succeeded.
curl -X POST "https://www.membber.com/api/v1/customer/pos-card-links/revoke" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"link_id": "0a8e0780-0000-4000-8000-d0c50000000a"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/customer/pos-card-links/revoke", {
body: {
link_id: "0a8e0780-0000-4000-8000-d0c50000000a"
},
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.revokePosCardLink(
body: .json(.init(
linkId: "0a8e0780-0000-4000-8000-d0c50000000a"
))
).ok.body.json
print(response){
"revoked": true
}The customer profile for one store: the store-scoped member_profiles row (health, address, ID verification fields) merged with the name and email from the customers table, so the settings screen can render one unified form. If no profile row exists yet, an empty shell is returned carrying the customer id, store id, name and email. The customer is always resolved from the session, never a client-supplied id.
Store whose profile to read (the profile is store-scoped).
member_profiles row id, or an empty string when no profile exists yet.
The signed-in customer id.
Store the profile belongs to.
Customer display name from the customers table.
Customer email from the customers table.
curl -G "https://www.membber.com/api/v1/customer/profile" \
-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/customer/profile", {
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.getCustomerProfile(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"profile": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"email": "alex@example.com",
"profile_image_url": "https://example.com/image.jpg",
"gender": "<gender>",
"date_of_birth": "<date_of_birth>",
"address_line_1": "<address_line_1>",
"address_line_2": "<address_line_2>",
"city": "<city>",
"postcode": "EXAMPLE10",
"medical_notes": "Added at the front desk",
"id_document_url": "https://example.com/image.jpg",
"id_verification_status": "<id_verification_status>",
"id_verified_at": "<id_verified_at>",
"id_verified_by": "<id_verified_by>",
"id_rejection_reason": "Added at the front desk",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}The customer app auto-save for the settings form: upserts the store-scoped member_profiles row (health, address, gender) and updates name/email on the customers table, then recomputes onboarding progress (non-fatal). Returns the merged profile view (identical shape to getCustomerProfile). Idempotent: the member_profiles upsert is keyed on (customer_id, store_id) and the customers update is a plain set, so a retried identical request converges to the same state. The customer is always resolved from the session, never a client-supplied id.
Store whose profile to write (the profile is store-scoped).
Customer display name (persisted on the customers table).
Customer email (lower-cased + trimmed server-side; customers table).
Gender display string; normalized to the DB value server-side.
ISO date; empty string is coerced to null.
member_profiles row id.
The signed-in customer id.
Store the profile belongs to.
Customer display name from the customers table.
Customer email from the customers table.
curl -X POST "https://www.membber.com/api/v1/customer/profile" \
-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/customer/profile", {
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.upsertCustomerProfile(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"profile": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"email": "alex@example.com",
"profile_image_url": "https://example.com/image.jpg",
"gender": "<gender>",
"date_of_birth": "<date_of_birth>",
"address_line_1": "<address_line_1>",
"address_line_2": "<address_line_2>",
"city": "<city>",
"postcode": "EXAMPLE10",
"medical_notes": "Added at the front desk",
"id_document_url": "https://example.com/image.jpg",
"id_verification_status": "<id_verification_status>",
"id_verified_at": "<id_verified_at>",
"id_verified_by": "<id_verified_by>",
"id_rejection_reason": "Added at the front desk",
"created_at": "<created_at>",
"updated_at": "<updated_at>"
}
}The server owns this answer because the cooldown, the three-dismissal limit and the Apple private-relay rule are cross-platform: the App Clip and the app write to the SAME ledger, so a customer who has said no three times in one must not be asked again by the other. Email and phone are decided independently. Read-only, record the ask with `markProfilePromptShown`.
curl -G "https://www.membber.com/api/v1/customer/profile-prompt/decision" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/customer/profile-prompt/decision");
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.getProfilePromptDecision().ok.body.json
print(response){
"shouldShow": true,
"needsEmail": true,
"needsPhone": true,
"shouldShowEmail": true,
"shouldShowPhone": true
}Being SHOWN is what starts the seven-day cooldown, not being dismissed. Recording only on dismissal meant a customer who ignored the ask was asked again immediately, on every surface. Idempotent in effect: stamping the same field twice in a cooldown window changes nothing.
Which ask was put in front of them.
emailphonecurl -X POST "https://www.membber.com/api/v1/customer/profile-prompt/shown" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"field": "email"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/customer/profile-prompt/shown", {
body: {
field: "email"
},
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.markProfilePromptShown(
body: .json(.init(
field: .email
))
).ok.body.json
print(response){
"ok": true
}Booking pre-flight check for the signed-in customer at a given store: whether they satisfy the store-configured requirements (health questionnaire, waivers, emergency contact), the list of any outstanding requirement keys, and the per-requirement status. Each requirement is only enforced when the store enables it. The member is resolved from the session; the store is context, never identity.
Store whose booking requirements to check readiness against (context, not identity).
True when every store-required item is satisfied (missing is empty).
The requirement keys still outstanding (e.g. 'health_questionnaire', 'waiver', 'emergency_contact').
Whether a valid, unexpired health questionnaire is on file (true when not required by the store).
Whether all active waivers are signed (true when not required by the store).
Whether at least one emergency contact is on file (true when not required by the store).
curl -G "https://www.membber.com/api/v1/customer/readiness" \
-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/customer/readiness", {
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.getCustomerReadiness(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"ready": true,
"missing": [
"<missing>"
],
"healthQuestionnaireValid": true,
"waiversComplete": true,
"emergencyContactSet": true
}