6 operations. Every schema and example on this page is generated from the platform contract.
Read-only cart promo preview using the SAME validator checkout uses. Enumeration-safe: invalid / expired / used-up codes all collapse to one generic "can't be used" message; only min_spend and email_required stay specific. With basket lines it returns a server-computed discount + per-line strike (display == charge).
The promo code entered in the cart (absent/null/empty/oversized collapses to the generic message).
Current basket subtotal in pence.
Optional basket lines → a server-computed estimate + per-line strike for item-scope codes.
min_spend | email_required (the only non-collapsed reasons).
Shopper-facing message (generic "can't be used" collapses all enumeration-sensitive failures).
curl -X POST "https://www.membber.com/api/v1/checkout/promo/preview" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"subtotal_pence": 1500
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/checkout/promo/preview", {
body: {
subtotal_pence: 1500
},
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.previewPromoCode(
body: .json(.init(
subtotalPence: 1500
))
).ok.body.json
print(response){
"valid": true,
"reason": "Added at the front desk",
"message": "Added at the front desk",
"discount_type": "<discount_type>",
"discount_value": 1,
"description": "Added at the front desk",
"stackable_with_voucher": true,
"min_spend_pence": 1500,
"estimated_discount_pence": 1500,
"applies_to": "<applies_to>",
"affected_item_id": "257c9efc-0000-4000-8000-d0c500000025",
"discounted_unit_pence": 1500
}Public guest checkout for the Android stopgap / web /pay. Re-prices + re-validates the basket server-side and creates a hosted Stripe Checkout session. Idempotent via the body idempotency_key (order-level replay reuses the same session). When a member is signed in (verified Bearer session) the order is anchored to their customer id so loyalty lands on their card; a client-supplied customer_id is always ignored.
Public store code.
Basket lines (re-priced + re-validated server-side).
Order idempotency key (body, or the Idempotency-Key header). Order-level replay reuses the same session.
Anonymous contact details (loyalty resolves by email at webhook time when no member is signed in).
buy_nowcollectnowscheduledMixed-cart groups (take-now + collect).
nowcollectnowscheduledThe created hosted-checkout session (Stripe Checkout URL + order handle).
curl -X POST "https://www.membber.com/api/v1/checkout/sessions" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_code": "EXAMPLE10",
"items": [
{
"menu_item_id": "ca65a6e7-0000-4000-8000-d0c5000000ca",
"quantity": 1
}
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/checkout/sessions", {
body: {
store_code: "EXAMPLE10",
items: [
{
menu_item_id: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
}
]
},
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.createCheckoutSession(
body: .json(.init(
storeCode: "EXAMPLE10",
items: [.init(
menuItemId: "ca65a6e7-0000-4000-8000-d0c5000000ca",
quantity: 1
)]
))
).ok.body.json
print(response){
"session": {
"order_id": "4991ffac-0000-4000-8000-d0c500000049",
"order_number": "<order_number>",
"status": "<status>",
"checkout_url": "https://example.com/image.jpg",
"stripe_checkout_session_id": "3c37c50b-0000-4000-8000-d0c50000003c",
"expires_at": "<expires_at>",
"total_pence": 1500,
"currency": "GBP"
}
}Public status poll after a hosted Stripe Checkout redirect: resolves the order by stripe_checkout_session_id and returns its status (paid/expired/etc.), totals, and collection code. 404 when no order matches the session.
The order behind the checkout session (post-redirect status polling).
curl -G "https://www.membber.com/api/v1/checkout/sessions/243a3e51-0000-4000-8000-d0c500000024"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/checkout/sessions/{sessionId}", {
params: { path: { sessionId: "243a3e51-0000-4000-8000-d0c500000024" } },
});
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.getCheckoutSession(
path: .init(sessionId: "243a3e51-0000-4000-8000-d0c500000024")
).ok.body.json
print(response){
"order": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"order_number": "<order_number>",
"status": "<status>",
"total_pence": 1500,
"currency": "GBP",
"customer_email": "alex@example.com",
"created_at": "<created_at>",
"paid_at": "<paid_at>",
"expires_at": "<expires_at>",
"order_type": "<order_type>",
"collection_code": "EXAMPLE10"
}
}Binds an APNs device token to the order behind a hosted-checkout session, for the eight hours Apple grants an App Clip without a permission prompt. Authenticated by possession of the session id, the same secret the status poll uses. Refuses an order that has not been paid for (409): an unpaid basket may never become an order, and the window would be spent on nothing. Idempotent per (order, device), re-registering EXTENDS the window rather than producing a second banner. 404 when no order matches the session.
The APNs device token, lowercase hex. Bound to this order only, and only for Apple's eight-hour window.
The Clip's OWN APNs topic, the parent bundle id with `.Clip` appended. A push aimed at the parent id never reaches the Clip.
True for a DEBUG build (APNs sandbox). Sending the wrong one is the classic silent "push never arrives".
When the grant lapses. Never extended beyond Apple's window.
Apple's ephemeral grant, in hours. Not a tunable, it is the length of the grant.
curl -X POST "https://www.membber.com/api/v1/checkout/sessions/243a3e51-0000-4000-8000-d0c500000024/notifications" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"device_token": "<device_token>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/checkout/sessions/{sessionId}/notifications", {
params: { path: { sessionId: "243a3e51-0000-4000-8000-d0c500000024" } },
body: {
device_token: "<device_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.registerCheckoutSessionNotifications(
path: .init(sessionId: "243a3e51-0000-4000-8000-d0c500000024"),
body: .json(.init(
deviceToken: "<device_token>"
))
).ok.body.json
print(response){
"expires_at": "<expires_at>",
"window_hours": 1
}Merchant read of the full checkout configuration for one store.
Store whose checkout settings to fetch.
Drop orders (guest_orders.batch_drop_id set) earn a stamp. Off by default.
Click & Collect orders (order_type collect, no drop) earn a stamp. On by default.
Buy-now orders and POS card payments earn a stamp. On by default.
curl -G "https://www.membber.com/api/v1/checkout/settings" \
-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.getCheckoutSettings({
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.getCheckoutSettings(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"checkout_enabled": true,
"menu_checkout_enabled": true,
"allow_guest_checkout": true,
"test_mode_enabled": true,
"currency": "GBP",
"min_amount_pence": 1500,
"max_amount_pence": 1500,
"prices_include_vat": true,
"vat_receipts_enabled": true,
"vat_number": "<vat_number>",
"tip_enabled": true,
"tip_presets_pence": [
1500
],
"service_charge_enabled": true,
"service_charge_percent": 1,
"service_charge_label": "<service_charge_label>",
"customer_receipts_enabled": true,
"receipt_email_collection": "alex@example.com",
"refund_window_days": 1,
"purchase_to_loyalty_enabled": true,
"loyalty_on_drops": true,
"loyalty_on_click_collect": true,
"loyalty_on_buy_now": true,
"loyalty_rule": "<loyalty_rule>",
"loyalty_spend_threshold_pence": 1500,
"order_fulfilment_mode": "<order_fulfilment_mode>",
"public_note": "Added at the front desk",
"offline_message": "Added at the front desk",
"buy_now_enabled": true,
"collect_enabled": true,
"collect_mode": "<collect_mode>",
"default_prep_minutes": 1,
"max_concurrent_orders": 1,
"orders_per_slot": 1,
"collection_window_days": 1,
"slot_minutes": 1,
"collect_window_start": "<collect_window_start>",
"collect_window_end": "<collect_window_end>",
"collect_daily_release_time": "<collect_daily_release_time>",
"collect_timezone": "Europe/London",
"collect_lead_time_minutes": 1,
"collect_cutoff_minutes": 1,
"collect_paused": true,
"collect_instructions": "<collect_instructions>",
"auto_accept": true,
"show_slot_spots": true
}Public (Android stopgap / web /pay) store payload: branding, checkout + Order & Collect settings, and the purchasable menu with media, allergens, stock and prices. checkout_available reflects the resolved entitlement + Stripe readiness. fulfilment_today carries today's ordering state (whether the at-counter lanes are shut, why, and when they reopen) so a surface can say so before a basket is built.
Public guest-checkout store payload (branding + settings + menu).
Full public checkout settings (currency, tips, VAT, Order & Collect, loyalty).
Purchasable menu items with media, allergens, stock, prices.
Today's ordering state. Null when the shop is unavailable for other reasons (nothing to time) or the resolve failed, a blip must never hide a working shop.
The AT-COUNTER lanes (buy-now / ASAP collect) are not serving right now, the day is switched off, or we are outside today's hours. NOT "no orders at all": order-ahead can still be open.
True when `closed` is caused by the clock (outside today's Order & Collect hours) rather than by the day or a date override being switched off. Scheduled Click & Collect keeps taking pre-orders.
Buy-now (at-counter) is orderable this instant: lane on, day on, doors open, inside the hours.
Today's configured opening time, "HH:MM" store-local. Null when unset or open all day.
Today's configured closing time, "HH:MM" store-local, always the merchant's OWN value, never a normalised midnight. Null when unset or open all day.
Store-local "YYYY-MM-DDTHH:MM" the at-counter lane next reopens (scanned up to 7 days over the weekday rules + date overrides, intersected with the physical door hours when buy-now is offered). Emitted only while closed; null when no reopen can be computed, show no hint rather than a wrong one.
Pre-rendered reason + reopen time, the SAME sentence checkout would reject with.
curl -G "https://www.membber.com/api/v1/checkout/shop/EXAMPLE10"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/checkout/shop/{storeCode}", {
params: { path: { storeCode: "EXAMPLE10" } },
});
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.getCheckoutShop(
path: .init(storeCode: "EXAMPLE10")
).ok.body.json
print(response){
"store": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"code": "EXAMPLE10",
"name": "Example name",
"description": "Added at the front desk",
"logo_url": "https://example.com/image.jpg",
"primary_color": "<primary_color>",
"brand_color": "<brand_color>",
"secondary_color": "<secondary_color>",
"loyalty_goal": 1,
"reward_title": "<reward_title>",
"stamp_icon_filled_url": "https://example.com/image.jpg",
"stamp_icon_empty_url": "https://example.com/image.jpg",
"is_active": true,
"checkout_available": true,
"unavailable_reason": "Added at the front desk",
"menu_version": "<menu_version>",
"menu_updated_at": "<menu_updated_at>",
"settings": {},
"menu_items": [
{}
],
"fulfilment_today": {
"closed": true,
"hours_closed": true,
"now_available": true,
"opens_at": "<opens_at>",
"closes_at": "<closes_at>",
"next_open_at": "<next_open_at>",
"closed_message": "Added at the front desk"
}
}
}