43 operations. Every schema and example on this page is generated from the platform contract.
Books a walk-up / phone appointment as the merchant, into a unit the authenticated store owns. Wraps the one booking authority, so a normal add still cannot double-book. With `override: true` the merchant FORCE-BOOKS a squeeze-in the authority would refuse, the resulting block is exempt from the double-booking guard and blocks nobody. Idempotent per `client_token`. No money moves.
The staff member / room / table to book. Must belong to the authenticated store.
ISO instant the appointment starts.
ISO instant the appointment ends.
An existing customer to book for.
Who the appointment is for, for a walk-up with no account.
Required if there is no customer_id, someone must be reachable.
Agreed price, snapshotted onto the appointment.
Widen what the UNIT is occupied for (buffers), without changing what the customer is told.
The OTHER tables to push together with this one, for a party too big for a single table. Front of house takes an eight over the phone the same way a guest books one online. With `override` the merchant may combine tables they have not declared, their floor, their call, otherwise the join must be declared.
FORCE-BOOK: squeeze this in even where the availability authority would refuse. The merchant-only power, a squeeze-in blocks nobody else. Defaults to false.
Caller-generated idempotency key, a retry with the same token returns the SAME appointment, never a duplicate.
The short reference the customer quotes.
How many of the party are children (age 12 and under). 0 for a normal appointment.
High chairs the diner asked for, so the host can fetch them.
true when an idempotent retry returned the appointment the first call created.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments" \
-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",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.createMerchantAppointment({
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
unit_id: "eeebf9b6-0000-4000-8000-d0c5000000ee",
starts_at: "<starts_at>",
ends_at: "<ends_at>"
},
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.createMerchantAppointment(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
unitId: "eeebf9b6-0000-4000-8000-d0c5000000ee",
startsAt: "<starts_at>",
endsAt: "<ends_at>"
))
).ok.body.json
print(response){
"appointment": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"booking_ref": "<booking_ref>",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"guest_name": "<guest_name>",
"guest_email": "alex@example.com",
"guest_phone": "+44 7700 900123",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"status": "<status>",
"payment_status": "<payment_status>",
"price_pence": 1500,
"deposit_pence": 1500,
"balance_pence": 1500,
"currency": "GBP",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"source": "<source>",
"is_override": true
},
"reused": true
}Resolves the short reference a diner quotes ("I booked under K4M2P7") to the one booking that carries it at the AUTHENTICATED store. References are unique per store, so the answer is one booking or NOT_FOUND, never a list and never a near miss. Input is forgiving about how the code was heard (see `ref`); the match itself is exact. This is what makes the reference usable by a phone agent rather than merely printable.
The reference the caller said, as they said it. Case, spaces and hyphens are all forgiven, and so are the eight characters the alphabet leaves out: O and Q resolve as 0, I and L as 1, S as 5, Z as 2, B as 8 and U as V, because those are exactly the mistakes a person makes writing down a code they heard rather than saw. Anything that is still not a reference after that comes back as VALIDATION_ERROR rather than being guessed at.
The store's IANA timezone, the instants above happen in it.
The account holder, or null for a guest booking and for a customer who has been erased.
What they booked, in the merchant's own words.
Which table / chair / room.
ISO instant it starts.
ISO instant it ends.
curl -G "https://www.membber.com/api/v1/bookings/appointments/by-reference" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "ref=<ref>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/bookings/appointments/by-reference", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066", ref: "<ref>" } },
});
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.getAppointmentByReference(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", ref: "<ref>")
).ok.body.json
print(response){
"timezone": "Europe/London",
"appointment": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"booking_ref": "<booking_ref>",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"guest_name": "<guest_name>",
"guest_email": "alex@example.com",
"guest_phone": "+44 7700 900123",
"customer_name": "Alex Example",
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"service_name": "<service_name>",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"unit_name": "<unit_name>",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"status": "<status>",
"payment_status": "<payment_status>",
"price_pence": 1500,
"deposit_pence": 1500,
"balance_pence": 1500,
"currency": "GBP",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"preferred_area_id": "afa05c4f-0000-4000-8000-d0c5000000af",
"preferred_area_name": "<preferred_area_name>",
"source": "<source>",
"checked_in_at": "<checked_in_at>",
"completed_at": "<completed_at>",
"cancelled_at": "<cancelled_at>"
}
}Cancels a booking the authenticated store owns and frees its slot (the block is RELEASED, never deleted, so the audit trail survives). Idempotent, cancelling twice is a no-op, never a second state change. No fee is charged here (cancel/no-show fees are Stage 7).
The appointment to cancel. Must belong to the authenticated store.
Why, for the record.
true when it was already cancelled/completed, an idempotent no-op.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments/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",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.cancelMerchantAppointment({
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d"
},
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.cancelMerchantAppointment(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d"
))
).ok.body.json
print(response){
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"cancelled": true,
"noop": true
}Marks a confirmed appointment as checked-in (they have arrived). Single-winner: only a confirmed booking can be checked in, and a second check-in is a no-op. Store-scoped. Audited. No money moves.
The appointment. Must belong to the authenticated store.
The status the appointment now holds.
true when it was already in that status, an idempotent no-op.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments/check-in" \
-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",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.checkInAppointment({
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d"
},
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.checkInAppointment(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d"
))
).ok.body.json
print(response){
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"status": "<status>",
"noop": true
}Marks an appointment done, from confirmed or checked-in. Single-winner: a second complete is a no-op, and a cancelled / no-show booking cannot be completed. Store-scoped. Audited. No money moves (settle is Stage 6).
The appointment. Must belong to the authenticated store.
The status the appointment now holds.
true when it was already in that status, an idempotent no-op.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments/complete" \
-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",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.completeAppointment({
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d"
},
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.completeAppointment(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d"
))
).ok.body.json
print(response){
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"status": "<status>",
"noop": true
}What the host types when somebody rings up and says "my wife is coeliac", the same structured record the diner could have made themselves, stored identically, so the card at check-in reads the same either way. Store-scoped at the database layer: the RPC only acts on a booking whose store is the authenticated one. Audited, with the staff member as the actor. Idempotent on client_token. No money moves. Errors: APPOINTMENT_NOT_FOUND, INVALID_TRANSITION (cancelled, completed or no-show).
The booking. Must belong to the authenticated store, a foreign id reads as not found.
The COMPLETE set of requirements for this booking. This REPLACES whatever stood before, send the whole set every time, never a delta. Send an empty array to say "we asked, there is nothing", which is a real and useful statement and is recorded as one.
CeleryGlutenCrustaceansEggsFishLupinMilkMolluscsMustardPeanutsSesameSoyaSulphitesTree nutsVegetarianVeganHalalKosherPescatarianDairy-freeGluten-freeThe detail one tap in. Omit or null to clear it. At most 280 characters, the same ceiling as a diner note.
Your own id for this act. The same token twice records once and returns the same answer, so a retried tap or an offline queue flushed twice never double-writes.
The requirements standing on this booking. EMPTY IS NOT "NONE", read it together with `dietary_recorded_at`, which is the only field that says whether anybody has been asked.
CeleryGlutenCrustaceansEggsFishLupinMilkMolluscsMustardPeanutsSesameSoyaSulphitesTree nutsVegetarianVeganHalalKosherPescatarianDairy-freeGluten-freeThe detail, in the diner's own words, "severe, airborne". Never a replacement for the list above: anything the vocabulary can name names itself, and this carries what it cannot.
🔴 ISO instant somebody last stated this booking's requirements. NULL means NOBODY HAS SAID, which is NOT the same fact as "no requirements" and must never be rendered as one. A client that drops this field cannot tell a coeliac table from a table nobody asked.
Who stated it. Front of house treats "the diner told us" and "we typed it up off a phone call" differently, so the provenance travels with the fact.
customermerchantagentTrue when at least one requirement is one of the FOURTEEN a UK food business must declare, rather than a preference. Computed SERVER-SIDE from the one exported decider, deliberately: a client that held its own copy of the statutory list would be a second list to drift, and the distinction is not cosmetic, "vegan" is a choice a kitchen should honour, "peanuts" is a thing that can put somebody in an ambulance, and the two must never render as the same chip.
true when this client_token had already been used, so nothing new was written. Still a success.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments/dietary" \
-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",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"dietary_requirements": [
"Celery"
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/appointments/dietary", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d",
dietary_requirements: [
"Celery"
]
},
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.setMerchantAppointmentDietary(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d",
dietaryRequirements: [.Celery]
))
).ok.body.json
print(response){
"dietary_requirements": [
"Celery"
],
"dietary_note": "Added at the front desk",
"dietary_recorded_at": "<dietary_recorded_at>",
"dietary_recorded_by": "customer",
"dietary_has_allergen": true,
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"duplicate": true
}Marks a DUE confirmed appointment as a no-show (the time has passed and nobody came). A checked-in guest showed up, so a no-show cannot fire from checked-in; a future booking is not yet due and is refused. Single-winner and idempotent. Store-scoped. Audited. NO fee is charged here (no-show fees are Stage 7).
The appointment. Must belong to the authenticated store.
The status the appointment now holds.
true when it was already in that status, an idempotent no-op.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments/no-show" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.markAppointmentNoShow({
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d"
},
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.markAppointmentNoShow(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d"
))
).ok.body.json
print(response){
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"status": "<status>",
"noop": true
}Secures the NEW slot before releasing the OLD, so a half-failed move leaves the original untouched. Both the appointment and the destination unit must belong to the authenticated store. With `override: true` the merchant moves past the cancellation window and past the free-slot check (their diary, their call), a power the customer reschedule never has.
The appointment to move. Must belong to the authenticated store.
Where it is moving to, may be the same unit. Must belong to the authenticated store.
ISO instant of the new start.
ISO instant of the new end.
Move even inside the cancellation policy window and even onto an occupied slot (a merchant squeeze-move). The merchant-only power. Defaults to false (respects the window and the free-slot check).
true when the booking was nudged within the same unit.
Every table the booking now occupies. A joined booking moves in TIME as a whole; moving it onto a different table is refused (JOINED_BOOKING) rather than half-done.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments/reschedule" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.rescheduleMerchantAppointment({
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d",
unit_id: "eeebf9b6-0000-4000-8000-d0c5000000ee",
starts_at: "<starts_at>",
ends_at: "<ends_at>"
},
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.rescheduleMerchantAppointment(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d",
unitId: "eeebf9b6-0000-4000-8000-d0c5000000ee",
startsAt: "<starts_at>",
endsAt: "<ends_at>"
))
).ok.body.json
print(response){
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"moved_in_place": true,
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
]
}Answers "when was this person last in?" across the store's WHOLE history, grouped by person rather than by row. Store-wide and business-authenticated; the store is the authenticated store, never a client-supplied id. A customer who has been deleted under the data-integrity law is unfindable here: their financial rows stay in the books, but their identity cannot be searched back out of them. Counts are real statuses only, visits means completed or checked-in, and no-shows, cancellations and unresolved past bookings are each reported as themselves.
Part of a name or an email address. Two characters minimum: one letter matches most of a roster.
How many people to return. Default 20.
How many recent appointments per person. Default 12.
The store's IANA timezone, every instant above is anchored to it for display.
The normalised needle actually searched for.
How many people matched in total, counted BEFORE `limit`, so a client can say "20 of 34" honestly.
Most recently seen first.
Stable grouping key for this person within this store. `c:<customer_id>` for an account booking, `g:<name>|<email>` for a guest, a guest has no id, so name plus email is the most honest identity the data supports. Two different people sharing both would merge; nothing in the data can separate them.
Set only where the booking was made from an account.
The most recent name this person booked under.
Times they were actually in the chair: completed + checked-in. A no-show is not a visit and is counted separately.
Past appointments still sitting in `confirmed`, nobody ever said whether they came. Reported on its own rather than guessed either way; this is the state B497 found two of yesterday's rows in.
ISO instant of their first actual visit.
ISO instant of their most recent actual visit, the answer to the question.
ISO instant of their soonest future booking, if any.
Sum of the price on visits that happened. Not a forecast, not a lifetime value.
Their most recent appointments, newest first, capped by `visit_limit`.
ISO instant the appointment started.
'completed' | 'checked_in' | 'confirmed' | 'cancelled' | 'no_show' | 'pending_payment'.
Who or what they were booked with.
curl -G "https://www.membber.com/api/v1/bookings/appointments/search" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "q=<q>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/bookings/appointments/search", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066", q: "<q>" } },
});
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.searchAppointmentPeople(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", q: "<q>")
).ok.body.json
print(response){
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"timezone": "Europe/London",
"query": "<query>",
"total": 1,
"people": [
{
"key": "<key>",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"name": "Example name",
"email": "alex@example.com",
"currency": "GBP",
"visits": 1,
"no_shows": 1,
"cancelled": 1,
"unresolved": 1,
"first_visit": "<first_visit>",
"last_visit": "<last_visit>",
"next_visit": "<next_visit>",
"spend_pence": 1500,
"recent": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"status": "<status>",
"payment_status": "<payment_status>",
"price_pence": 1500,
"currency": "GBP",
"unit_name": "<unit_name>"
}
]
}
]
}The merchant takes the outstanding balance at the end of an appointment, in cash or on their own card machine, and records it here. NO Stripe money moves and no platform fee is taken, the money changed hands outside Membber, so this row is the only evidence it was collected. The amount is DERIVED server-side (price − deposit actually paid − everything already settled) and is never accepted from the client. Settling completes the appointment. A short settle leaves the residual collectable: the no-op gate is "nothing outstanding", never "already stamped", and the recorded total accumulates. Nothing outstanding is an idempotent no-op rather than an error, and a retry can never record twice. Losing a race to a colleague comes back as outcome=already_settled with who/how/how much, never as your own success. A no-show is never settled here (that is the no-show fee path). Store-scoped and business-authed; gated by the can_use_bookings entitlement + the can_process_payments staff permission.
Store the appointment belongs to (authorisation only, the store acted on always comes from auth).
The appointment to settle. Must belong to the authenticated store.
How the money was taken. Both are RECORDED ONLY (the money moved outside Membber, at the counter or on the shop's own terminal, so this is the only record it was collected). No Stripe charge is made and no platform fee is taken.
cashcard_machine'settled' = this call recorded the money. 'already_settled' = somebody else's settle got there first and this call recorded nothing (never draw it as your own success, the person who lost may be holding the cash). 'nothing_owed' = there was nothing left to take.
settledalready_settlednothing_owedThe appointment status after the call, settling completes the appointment.
'none' | 'deposit_paid' | 'paid' | 'refunded' | 'partially_refunded' | 'failed'.
What is STILL owed AFTER this call, derived server-side as price − deposit actually paid − everything settled. Unambiguous on every outcome, so a client can write it straight onto the row it is showing.
What THIS call recorded. 0 on any outcome other than 'settled'.
Everything ever recorded as settled on this booking, this call included.
How the settle being reported was taken (null when nothing was ever owed).
cashcard_machineISO instant of the settle being reported.
true when the settle being reported is the caller's own.
Who recorded it, when that was somebody else and we can name them. null = we cannot say who.
curl -X POST "https://www.membber.com/api/v1/bookings/appointments/settle" \
-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",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"method": "cash"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/appointments/settle", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d",
method: "cash"
},
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.settleAppointmentBalance(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d",
method: .cash
))
).ok.body.json
print(response){
"outcome": "settled",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"status": "<status>",
"payment_status": "<payment_status>",
"outstanding_pence": 1500,
"collected_pence": 1500,
"settled_total_pence": 1500,
"currency": "GBP",
"method": "cash",
"settled_at": "<settled_at>",
"settled_by_you": true,
"settled_by_name": "<settled_by_name>"
}The merchant view of the diary: every active bookable unit for a date RANGE (store-local dates), each carrying its appointments, live holds and time-off. Store-wide and business-authenticated, the customer appointments read is caller-scoped and cannot see another booking. Timezone-correct: day boundaries read the store timezone.
First day to show, in the STORE's timezone.
Last day to show (inclusive), in the STORE's timezone.
Restrict to one unit. Omit for every unit (the full diary).
Include cancelled appointments. Defaults to excluding them.
The store's IANA timezone, every instant below is anchored to it for display.
ISO instant the window starts (00:00 store-local on from_date).
ISO instant the window ends, EXCLUSIVE (00:00 store-local the day after to_date).
Every active bookable unit, each with its appointments, holds and time-off.
Diary column colour.
The dining AREA this unit stands in, e.g. the Terrace. Null for every non-restaurant unit and for a table nobody has filed.
Resolved name of area_id ("Dining room", "Terrace", "The bar"), joined from restaurant_areas. Lets the diary group a floor into its sections instead of one flat list of tables.
The other tables this one pushes together with. Empty for a table that stands alone and for every non-restaurant unit.
The PRIMARY table this booking is filed under. On a joined booking the same appointment appears in more than one column, and this is how the client knows which column is the original and which is the pushed-together one.
Every table this booking occupies. Two when tables were pushed together for a big party, one otherwise. The diary draws the booking on each of them, because a table that is genuinely taken must not look free to the person standing at the pass.
The short reference the diner quotes on the phone. Front of house needs to SEE it on the booking, not merely be able to search by it, or a caller reading a code back can never be told "yes, that is the one".
The account holder's name, for the ordinary signed-in booking that carries no guest_name. Without it the diary can only say "Customer". Null for a guest booking (read guest_name instead), and null for a customer who has been erased or archived, their booking survives, their name does not.
ISO instant the appointment starts.
ISO instant the appointment ends (what the customer was told).
'pending_payment' | 'confirmed' | 'checked_in' | 'completed' | 'cancelled' | 'no_show'.
How many of the party are children (age 12 and under). 0 for a normal appointment.
High chairs the diner asked for, so the host can fetch them.
The dining AREA the guest asked for at booking (Level-1 seating). Null for no-preference and every non-restaurant booking.
Resolved name of preferred_area_id (e.g. "Window"), joined from restaurant_areas. Null when there is no preference.
true when this was FORCE-BOOKED (a squeeze-in exempt from the double-booking guard).
How many minutes late the diner says they will be. Null when nothing has been said. A LATER message replaces an earlier one, so this is always the current claim. The booking itself is UNCHANGED: running late shifts no time and re-lets no table.
ISO instant the diner said it. Front of house needs the age of the claim as much as the number, "fifteen minutes late" said forty minutes ago is a different fact.
Everything the diner has told this restaurant about this booking, OLDEST FIRST. A list rather than one field because they accumulate: an allergy declared at noon must not be buried by a high chair asked for at six. Empty for every booking nobody has written to.
Stable id of the ledger entry, so a client can diff without re-rendering.
ISO instant it was sent.
What they said, in their own words. At most 280 characters.
The requirements standing on this booking. EMPTY IS NOT "NONE", read it together with `dietary_recorded_at`, which is the only field that says whether anybody has been asked.
CeleryGlutenCrustaceansEggsFishLupinMilkMolluscsMustardPeanutsSesameSoyaSulphitesTree nutsVegetarianVeganHalalKosherPescatarianDairy-freeGluten-freeThe detail, in the diner's own words, "severe, airborne". Never a replacement for the list above: anything the vocabulary can name names itself, and this carries what it cannot.
🔴 ISO instant somebody last stated this booking's requirements. NULL means NOBODY HAS SAID, which is NOT the same fact as "no requirements" and must never be rendered as one. A client that drops this field cannot tell a coeliac table from a table nobody asked.
Who stated it. Front of house treats "the diner told us" and "we typed it up off a phone call" differently, so the provenance travels with the fact.
customermerchantagentTrue when at least one requirement is one of the FOURTEEN a UK food business must declare, rather than a preference. Computed SERVER-SIDE from the one exported decider, deliberately: a client that held its own copy of the statutory list would be a second list to drift, and the distinction is not cosmetic, "vegan" is a choice a kitchen should honour, "peanuts" is a thing that can put somebody in an ambulance, and the two must never render as the same chip.
Live pending reservations mid-checkout.
When this pending reservation lapses.
Time-off / lunch, from both the time-off entity and manual blocks.
'time_off' (an explicit time-off entry) or 'block' (a manual lunch/errand block).
curl -G "https://www.membber.com/api/v1/bookings/diary" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "from_date=<from_date>" \
--data-urlencode "to_date=<to_date>"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/bookings/diary", {
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066", from_date: "<from_date>", to_date: "<to_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.getStoreDiary(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", fromDate: "<from_date>", toDate: "<to_date>")
).ok.body.json
print(response){
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"timezone": "Europe/London",
"from": "<from>",
"to": "<to>",
"units": [
{
"unit": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"display_name": "<display_name>",
"kind": "<kind>",
"unit_type": "<unit_type>",
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"colour": "<colour>",
"photo_url": "https://example.com/image.jpg",
"sort": 1,
"bookable": true,
"online_bookable": true,
"is_active": true,
"area_id": "d381ba0d-0000-4000-8000-d0c5000000d3",
"area_name": "<area_name>",
"joins_with": [
"<joins_with>"
]
},
"appointments": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"booking_ref": "<booking_ref>",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"guest_name": "<guest_name>",
"guest_email": "alex@example.com",
"guest_phone": "+44 7700 900123",
"customer_name": "Alex Example",
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"status": "<status>",
"payment_status": "<payment_status>",
"price_pence": 1500,
"deposit_pence": 1500,
"balance_pence": 1500,
"currency": "GBP",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"preferred_area_id": "afa05c4f-0000-4000-8000-d0c5000000af",
"preferred_area_name": "<preferred_area_name>",
"source": "<source>",
"is_override": true,
"checked_in_at": "<checked_in_at>",
"completed_at": "<completed_at>",
"cancelled_at": "<cancelled_at>",
"running_late_minutes": 1,
"running_late_at": "<running_late_at>",
"diner_notes": [
{
"id": null,
"at": null,
"text": null
}
],
"dietary_requirements": [
"Celery"
],
"dietary_note": "Added at the front desk",
"dietary_recorded_at": "<dietary_recorded_at>",
"dietary_recorded_by": "customer",
"dietary_has_allergen": true
}
],
"holds": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"expires_at": "<expires_at>"
}
],
"time_off": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"kind": "<kind>",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"reason": "Added at the front desk"
}
]
}
]
}Always scoped to the signed-in customer, and the scoping is part of the query rather than a filter applied afterwards. There is deliberately no customer_id parameter and no store-wide list here, the venue's view is a different, business-authenticated surface. This is the read that answers "did I ask, and has anybody replied", which is the question that otherwise makes people ring up.
Narrow to one venue. Omit for every venue this person has asked.
Include answered, declined, expired and withdrawn requests. Default false, the open ones are what somebody opens this list for.
Newest first.
The short code the diner quotes while this is still a request, six characters from the same alphabet a booking reference uses, and unique within the store across BOTH requests and bookings, so a host handed a code never has to ask which kind of thing it is. When the request is accepted the resulting booking gets its own booking reference, and THAT is the one the diner keeps.
'pending' asked, nobody has answered · 'accepted' the venue turned it into a real booking · 'declined' the venue said no, and `decline_reason` says why · 'expired' the day went by without an answer, which is the commonest outcome and a DESIGNED one, not a failure · 'withdrawn' the diner took it back.
pendingaccepteddeclinedexpiredwithdrawnOf the party, how many are children. Counted within party_size, never on top of it.
The day they want, in the STORE's timezone, YYYY-MM-DD.
The time they want, local to the store, 24 hour. Omit for 'any time that day'.
They would take another time that day.
They would take another day.
The area they asked for, carried forward from the booking flow. A preference, never a promise.
Anything the flow did not already know, in their own words.
Why the venue said no, in their words. Present only on a declined request, and it reaches the diner.
The booking this became. Present only when accepted.
The reference of the booking this became. Present only when accepted.
ISO instant this request stops being answerable. The end of the requested day in the store's zone, or seven days past it when they said they were flexible on the date. Stated to the diner at the moment they ask, because a request with no stated end is a promise nobody made.
When the venue was reminded. One reminder, ever, a second is nagging, and a nagged merchant mutes the channel.
Where this party could ACTUALLY sit: the runs of tables that would hold them, built from the merchant's own declared joins, the same column the customer availability search uses, so the two can never drift apart. Fewest tables first, then tightest fit, because burning two tables where one would do is how a floor plan quietly loses a sitting. EMPTY is a real answer and an important one: it means nothing on this floor holds them, and whatever the host picks she will be moving chairs. Merchant reads only.
The tables in this run. One id means a single table.
Their names, as the merchant wrote them, 'Decking 2', 'Decking 3'.
What they seat between them.
True when this is more than one table pushed together.
curl -G "https://www.membber.com/api/v1/bookings/enquiries" \
-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/bookings/enquiries");
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.listBookingEnquiries().ok.body.json
print(response){
"enquiries": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"enquiry_ref": "<enquiry_ref>",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"status": "pending",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"requested_date": "<requested_date>",
"requested_time": "<requested_time>",
"flexible_time": true,
"flexible_date": true,
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"preferred_area_id": "afa05c4f-0000-4000-8000-d0c5000000af",
"note": "Added at the front desk",
"guest_name": "<guest_name>",
"guest_email": "alex@example.com",
"guest_phone": "+44 7700 900123",
"decline_reason": "Added at the front desk",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"booking_ref": "<booking_ref>",
"expires_at": "<expires_at>",
"nudged_at": "<nudged_at>",
"responded_at": "<responded_at>",
"created_at": "<created_at>",
"seating_options": [
{
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"names": [
"Example name"
],
"seats_total": 1,
"table_count": 1,
"is_join": true
}
]
}
]
}For the party that does not fit, the night that is full, and the booking made inside the venue's notice period. Reserves nothing and charges nothing: it puts a question in front of the venue, who may turn it into a real booking or decline it with a reason. Everything the caller already knows is carried in, so the diner is never asked twice. Named error codes: NOT_REACHABLE (no customer and no email, nowhere to send the answer), PAST_DATE (the day has gone), ENQUIRY_LIMIT (three open requests at this venue already), BOOKINGS_UNAVAILABLE (this venue does not take bookings at all).
The venue being asked.
How many people. This is the number that did not fit.
The day they want, in the STORE's timezone, YYYY-MM-DD.
The time they want, local to the store, 24 hour. Omit for 'any time that day'.
True if they would take another time that day. Default false, never assume flexibility nobody offered.
True if they would take another day. Default false.
Carried forward from the flow they were already in. A stale or foreign one is dropped to null rather than refusing the request.
Carried forward. Same rule: dropped, never fatal.
The only free text. Bounded, because it is rendered into an email.
Required when there is no signed-in customer. The answer has to have somewhere to go: a request nobody can reply to is worse than no request, because the diner waits.
Optional, and worth asking for: a restaurant answering at 18:50 for a 19:00 table will ring, not email.
Caller-generated idempotency key. The SAME token returns the SAME request, so a double tap or a retried POST never leaves two parties waiting on one table.
The short code the diner quotes while this is still a request, six characters from the same alphabet a booking reference uses, and unique within the store across BOTH requests and bookings, so a host handed a code never has to ask which kind of thing it is. When the request is accepted the resulting booking gets its own booking reference, and THAT is the one the diner keeps.
'pending' asked, nobody has answered · 'accepted' the venue turned it into a real booking · 'declined' the venue said no, and `decline_reason` says why · 'expired' the day went by without an answer, which is the commonest outcome and a DESIGNED one, not a failure · 'withdrawn' the diner took it back.
pendingaccepteddeclinedexpiredwithdrawnOf the party, how many are children. Counted within party_size, never on top of it.
The day they want, in the STORE's timezone, YYYY-MM-DD.
The time they want, local to the store, 24 hour. Omit for 'any time that day'.
They would take another time that day.
They would take another day.
The area they asked for, carried forward from the booking flow. A preference, never a promise.
Anything the flow did not already know, in their own words.
Why the venue said no, in their words. Present only on a declined request, and it reaches the diner.
The booking this became. Present only when accepted.
The reference of the booking this became. Present only when accepted.
ISO instant this request stops being answerable. The end of the requested day in the store's zone, or seven days past it when they said they were flexible on the date. Stated to the diner at the moment they ask, because a request with no stated end is a promise nobody made.
When the venue was reminded. One reminder, ever, a second is nagging, and a nagged merchant mutes the channel.
Where this party could ACTUALLY sit: the runs of tables that would hold them, built from the merchant's own declared joins, the same column the customer availability search uses, so the two can never drift apart. Fewest tables first, then tightest fit, because burning two tables where one would do is how a floor plan quietly loses a sitting. EMPTY is a real answer and an important one: it means nothing on this floor holds them, and whatever the host picks she will be moving chairs. Merchant reads only.
The tables in this run. One id means a single table.
Their names, as the merchant wrote them, 'Decking 2', 'Decking 3'.
What they seat between them.
True when this is more than one table pushed together.
True when this token already had a request and it came back unchanged (idempotent replay).
curl -X POST "https://www.membber.com/api/v1/bookings/enquiries" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"party_size": 1,
"requested_date": "<requested_date>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/enquiries", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
party_size: 1,
requested_date: "<requested_date>"
},
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.createBookingEnquiry(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
partySize: 1,
requestedDate: "<requested_date>"
))
).ok.body.json
print(response){
"enquiry": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"enquiry_ref": "<enquiry_ref>",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"status": "pending",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"requested_date": "<requested_date>",
"requested_time": "<requested_time>",
"flexible_time": true,
"flexible_date": true,
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"preferred_area_id": "afa05c4f-0000-4000-8000-d0c5000000af",
"note": "Added at the front desk",
"guest_name": "<guest_name>",
"guest_email": "alex@example.com",
"guest_phone": "+44 7700 900123",
"decline_reason": "Added at the front desk",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"booking_ref": "<booking_ref>",
"expires_at": "<expires_at>",
"nudged_at": "<nudged_at>",
"responded_at": "<responded_at>",
"created_at": "<created_at>",
"seating_options": [
{
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"names": [
"Example name"
],
"seats_total": 1,
"table_count": 1,
"is_join": true
}
]
},
"reused": true
}Business-auth and scoped to the AUTHENTICATED store, never a client-supplied one. Returns the whole request, party, children, high chairs, the area they hoped for, their note, and crucially whether they are flexible, because the answer "not 19:00 but I can do 20:30" is only possible if the host can see that they said they could bend.
Default 'pending', the list somebody actually opens. 'all' includes answered, declined and expired requests for the window.
pendingallNarrow by the REQUESTED date, not by when it was asked. Omit for from today.
The day they want, in the STORE's timezone, YYYY-MM-DD.
Look one up by the code a caller is reading down the phone. Case-insensitive.
Soonest requested date first, then oldest asked first, the order a host would work them in.
The short code the diner quotes while this is still a request, six characters from the same alphabet a booking reference uses, and unique within the store across BOTH requests and bookings, so a host handed a code never has to ask which kind of thing it is. When the request is accepted the resulting booking gets its own booking reference, and THAT is the one the diner keeps.
'pending' asked, nobody has answered · 'accepted' the venue turned it into a real booking · 'declined' the venue said no, and `decline_reason` says why · 'expired' the day went by without an answer, which is the commonest outcome and a DESIGNED one, not a failure · 'withdrawn' the diner took it back.
pendingaccepteddeclinedexpiredwithdrawnOf the party, how many are children. Counted within party_size, never on top of it.
The day they want, in the STORE's timezone, YYYY-MM-DD.
The time they want, local to the store, 24 hour. Omit for 'any time that day'.
They would take another time that day.
They would take another day.
The area they asked for, carried forward from the booking flow. A preference, never a promise.
Anything the flow did not already know, in their own words.
Why the venue said no, in their words. Present only on a declined request, and it reaches the diner.
The booking this became. Present only when accepted.
The reference of the booking this became. Present only when accepted.
ISO instant this request stops being answerable. The end of the requested day in the store's zone, or seven days past it when they said they were flexible on the date. Stated to the diner at the moment they ask, because a request with no stated end is a promise nobody made.
When the venue was reminded. One reminder, ever, a second is nagging, and a nagged merchant mutes the channel.
Where this party could ACTUALLY sit: the runs of tables that would hold them, built from the merchant's own declared joins, the same column the customer availability search uses, so the two can never drift apart. Fewest tables first, then tightest fit, because burning two tables where one would do is how a floor plan quietly loses a sitting. EMPTY is a real answer and an important one: it means nothing on this floor holds them, and whatever the host picks she will be moving chairs. Merchant reads only.
The tables in this run. One id means a single table.
Their names, as the merchant wrote them, 'Decking 2', 'Decking 3'.
What they seat between them.
True when this is more than one table pushed together.
The account holder's name, for a signed-in diner who gave no guest name. Null for a guest, and null for a customer who has been erased.
Resolved name of the area they asked for, e.g. "Terrace".
'organic' a diner in the app · 'agent' a phone or WhatsApp agent asking on their behalf · 'merchant' the venue logging a request that arrived another way.
How many are still waiting on an answer, across every date. The badge number.
curl -G "https://www.membber.com/api/v1/bookings/enquiries/inbox" \
-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/bookings/enquiries/inbox");
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.getBookingEnquiryInbox().ok.body.json
print(response){
"enquiries": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"enquiry_ref": "<enquiry_ref>",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"status": "pending",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"requested_date": "<requested_date>",
"requested_time": "<requested_time>",
"flexible_time": true,
"flexible_date": true,
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"preferred_area_id": "afa05c4f-0000-4000-8000-d0c5000000af",
"note": "Added at the front desk",
"guest_name": "<guest_name>",
"guest_email": "alex@example.com",
"guest_phone": "+44 7700 900123",
"decline_reason": "Added at the front desk",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"booking_ref": "<booking_ref>",
"expires_at": "<expires_at>",
"nudged_at": "<nudged_at>",
"responded_at": "<responded_at>",
"created_at": "<created_at>",
"seating_options": [
{
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"names": [
"Example name"
],
"seats_total": 1,
"table_count": 1,
"is_join": true
}
],
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"customer_name": "Alex Example",
"preferred_area_name": "<preferred_area_name>",
"service_name": "<service_name>",
"source": "<source>"
}
],
"pending_count": 1
}ONE ACTION. Accept creates the appointment and closes the request against it inside a single transaction, so there is never a diner holding a booking nobody can find, nor a request marked answered with nothing behind it. It FORCE-BOOKS deliberately: a party of eight at a six-seat venue fits no table by construction, which is the entire reason the request exists, so an accept that respected the double-booking guard could never succeed for the case it was built for. The host is standing in the room and has decided. Both outcomes reach the diner. A party too big for one table is seated across a RUN of tables the merchant declared joinable (`unit_ids`), each genuinely blocked, so the second table cannot also be sold. Where they still do not fit, the answer says so, `seats_total` and `over_capacity` come home on every accept, rather than confirming in silence. Named error codes: TIME_REQUIRED (accept with no time), UNIT_NOT_FOUND, BAD_WINDOW, TOO_MANY_TABLES (more than six), PARTY_TOO_LARGE (they do not fit and the caller did not say `accept_over_capacity`), JOIN_NOT_ALLOWED (those tables are not declared as pushing together), STALE_SLOT (that table went while you were deciding, pick another).
acceptdeclineAccept only: the table the party is going on. Kept for callers that can only name ONE table (an installed binary that predates `unit_ids`); prefer `unit_ids`. Ignored when `unit_ids` is sent.
Accept only, and the one to use: EVERY table the party is going on. A party too big for any single table is seated across a run of tables the merchant declared joinable, each one is genuinely blocked, so the second table cannot also be sold to somebody else. The first id is the primary (the booking hangs off it); the rest ride as a join. `enquiry_seating_options` on each request already lists the runs that would hold this party, so a caller does not have to work out the combinations itself. One id behaves exactly like `unit_id`.
Accept only. Say true to mean "yes, this party is bigger than these tables seat, and I am putting them there anyway", the normal, allowed thing a host does when she is moving chairs. It is not required today, because the host is standing in the room and the app does not argue with her. It exists because a caller that is NOT in the room (a phone or WhatsApp agent) should have to say it out loud rather than promise a room something it cannot do. When it is required and missing, the answer is PARTY_TOO_LARGE with both numbers in it.
Accept only: ISO instant the table is for. This is the HOST's time, not the diner's, the whole value of "I am flexible" is that the answer may be a different time, or a different day, from the one asked for. Required to accept.
Accept only: ISO instant the sitting ends. Required to accept.
Decline only, and it REACHES THE DINER. A silent no is what makes people ring up, and a reason in the venue's own words is often an offer in disguise ("not Friday, but Saturday is wide open").
'pending' asked, nobody has answered · 'accepted' the venue turned it into a real booking · 'declined' the venue said no, and `decline_reason` says why · 'expired' the day went by without an answer, which is the commonest outcome and a DESIGNED one, not a failure · 'withdrawn' the diner took it back.
pendingaccepteddeclinedexpiredwithdrawnThe booking that now exists. Present on accept.
Its reference, the one the diner keeps and quotes at the door.
True when this request had already been answered and nothing changed. A second tap is never a second booking.
Every table the booking occupies. One id for a single table, more for a run pushed together.
How many people the request was for.
What those tables seat between them, from the merchant's own declared sizes. Null where the merchant has never said what a table seats, an undeclared table is not "too small", nobody has said.
True when the party is bigger than the tables seat. NOT an error, it is the allowed, deliberate thing a host does when she is moving chairs, and it is recorded rather than refused. It is here so no caller has to GUESS whether what it just confirmed actually fits.
curl -X POST "https://www.membber.com/api/v1/bookings/enquiries/respond" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"enquiry_id": "d7aafa17-0000-4000-8000-d0c5000000d7",
"action": "accept"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/enquiries/respond", {
body: {
enquiry_id: "d7aafa17-0000-4000-8000-d0c5000000d7",
action: "accept"
},
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.respondToBookingEnquiry(
body: .json(.init(
enquiryId: "d7aafa17-0000-4000-8000-d0c5000000d7",
action: .accept
))
).ok.body.json
print(response){
"enquiry_id": "d7aafa17-0000-4000-8000-d0c5000000d7",
"status": "pending",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"booking_ref": "<booking_ref>",
"noop": true,
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"party_size": 1,
"seats_total": 1,
"over_capacity": true
}The diner sorted their evening elsewhere. Withdrawing frees the venue from answering and frees the diner from the open-request cap. Idempotent: withdrawing an already-closed request returns it unchanged rather than erroring, and a request the venue has already ACCEPTED cannot be withdrawn this way, that is a real booking now, so it is cancelled, not un-asked.
The request to take back.
The short code the diner quotes while this is still a request, six characters from the same alphabet a booking reference uses, and unique within the store across BOTH requests and bookings, so a host handed a code never has to ask which kind of thing it is. When the request is accepted the resulting booking gets its own booking reference, and THAT is the one the diner keeps.
'pending' asked, nobody has answered · 'accepted' the venue turned it into a real booking · 'declined' the venue said no, and `decline_reason` says why · 'expired' the day went by without an answer, which is the commonest outcome and a DESIGNED one, not a failure · 'withdrawn' the diner took it back.
pendingaccepteddeclinedexpiredwithdrawnOf the party, how many are children. Counted within party_size, never on top of it.
The day they want, in the STORE's timezone, YYYY-MM-DD.
The time they want, local to the store, 24 hour. Omit for 'any time that day'.
They would take another time that day.
They would take another day.
The area they asked for, carried forward from the booking flow. A preference, never a promise.
Anything the flow did not already know, in their own words.
Why the venue said no, in their words. Present only on a declined request, and it reaches the diner.
The booking this became. Present only when accepted.
The reference of the booking this became. Present only when accepted.
ISO instant this request stops being answerable. The end of the requested day in the store's zone, or seven days past it when they said they were flexible on the date. Stated to the diner at the moment they ask, because a request with no stated end is a promise nobody made.
When the venue was reminded. One reminder, ever, a second is nagging, and a nagged merchant mutes the channel.
Where this party could ACTUALLY sit: the runs of tables that would hold them, built from the merchant's own declared joins, the same column the customer availability search uses, so the two can never drift apart. Fewest tables first, then tightest fit, because burning two tables where one would do is how a floor plan quietly loses a sitting. EMPTY is a real answer and an important one: it means nothing on this floor holds them, and whatever the host picks she will be moving chairs. Merchant reads only.
The tables in this run. One id means a single table.
Their names, as the merchant wrote them, 'Decking 2', 'Decking 3'.
What they seat between them.
True when this is more than one table pushed together.
curl -X POST "https://www.membber.com/api/v1/bookings/enquiries/withdraw" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"enquiry_id": "d7aafa17-0000-4000-8000-d0c5000000d7"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/enquiries/withdraw", {
body: {
enquiry_id: "d7aafa17-0000-4000-8000-d0c5000000d7"
},
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.withdrawBookingEnquiry(
body: .json(.init(
enquiryId: "d7aafa17-0000-4000-8000-d0c5000000d7"
))
).ok.body.json
print(response){
"enquiry": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"enquiry_ref": "<enquiry_ref>",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"status": "pending",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"requested_date": "<requested_date>",
"requested_time": "<requested_time>",
"flexible_time": true,
"flexible_date": true,
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"preferred_area_id": "afa05c4f-0000-4000-8000-d0c5000000af",
"note": "Added at the front desk",
"guest_name": "<guest_name>",
"guest_email": "alex@example.com",
"guest_phone": "+44 7700 900123",
"decline_reason": "Added at the front desk",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"booking_ref": "<booking_ref>",
"expires_at": "<expires_at>",
"nudged_at": "<nudged_at>",
"responded_at": "<responded_at>",
"created_at": "<created_at>",
"seating_options": [
{
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"names": [
"Example name"
],
"seats_total": 1,
"table_count": 1,
"is_join": true
}
]
}
}The merchant rail: every waiting + called walk-in for the AUTHENTICATED store, in join order, each waiting entry carrying its DERIVED position, honest wait, and the earliest-free chair the estimate would assign it (the default assign target). Store-wide and business-authenticated, the customer status read can only ever see one own entry. Business-auth + the paid can_use_bookings entitlement.
The store whose queue to read. Must be the authenticated store.
'appointments_only' | 'walk_ins_only' | 'mixed', whether the store even takes walk-ins.
The hold-after-call window, for the rail's countdown on called entries.
Every waiting + called entry in join order. Waiting entries carry a derived position + wait + suggested chair; called entries carry called_at.
Present on the merchant surface; omitted from the customer surface.
The service the walk-in asked for, shapes the wait estimate and the assigned duration.
A chair/stylist the walk-in asked for, if any.
'waiting' | 'called' | 'assigned' | 'completed' | 'left' | 'no_show' | 'converted_to_appointment'.
ISO instant they joined the queue (the join order that derives position).
ISO instant they were called, if they have been.
The wait we quoted AT JOIN, snapshotted, so a later dispute is answerable.
Set once assigned, the booking the entry converted into.
DERIVED 1-based place in line. Null once called/assigned/left, a stored position is a second source of truth that drifts.
DERIVED minutes until a chair is expected free for this person, from the live blocks. Null when not waiting.
DERIVED ISO instant a chair is expected free for this person. Null when not waiting.
The earliest-free chair the estimate would hand this person, the rail's default assign target. Null when not waiting.
curl -G "https://www.membber.com/api/v1/bookings/queue" \
-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/bookings/queue", {
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.listStoreWalkInQueue(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"walkin_mode": "<walkin_mode>",
"called_hold_minutes": 1,
"entries": [
{
"entry_id": "83353c08-0000-4000-8000-d0c500000083",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"customer_id": "96607d1c-0000-4000-8000-d0c500000096",
"guest_name": "<guest_name>",
"guest_phone": "+44 7700 900123",
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"preferred_unit_id": "cb0a9bf8-0000-4000-8000-d0c5000000cb",
"party_size": 1,
"status": "<status>",
"joined_at": "<joined_at>",
"called_at": "<called_at>",
"quoted_wait_min": 1,
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"queue_position": 1,
"expected_wait_min": 1,
"expected_start": "<expected_start>",
"suggested_unit_id": "28cd789a-0000-4000-8000-d0c500000028"
}
]
}Seats a walk-in at a chair/stylist/table starting now(), which BOOKS A REAL APPOINTMENT through the single booking authority, the queue never writes its own booking, so a walk-in and an online customer can never both take one chair. A lost race returns STALE_SLOT and the walk-in KEEPS THEIR PLACE (they are not dropped). Idempotent: a second assign returns the same appointment, never a double-book. With override: true the merchant force-seats a squeeze-in the authority would refuse. Both entry and unit must belong to the authenticated store. Business-auth + can_use_bookings. No money moves (deposits are Stage 6).
The authenticated store the entry + unit belong to.
The walk-in to seat. Must belong to the store.
The chair/stylist/table to seat them at, starting now(). Must belong to the store.
Override the assigned length. Defaults to the service duration (or 30 min).
FORCE-SEAT even where the availability authority would refuse (a squeeze-in). The merchant-only power, the block is exempt from the double-booking guard. Defaults to false.
The booking the walk-in became, created through the ONE booking authority, source "walk_in".
false when the entry was already assigned, an idempotent no-op returning the same appointment.
curl -X POST "https://www.membber.com/api/v1/bookings/queue/assign" \
-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",
"entry_id": "83353c08-0000-4000-8000-d0c500000083",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/queue/assign", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
entry_id: "83353c08-0000-4000-8000-d0c500000083",
unit_id: "eeebf9b6-0000-4000-8000-d0c5000000ee"
},
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.assignWalkInQueueEntry(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
entryId: "83353c08-0000-4000-8000-d0c500000083",
unitId: "eeebf9b6-0000-4000-8000-d0c5000000ee"
))
).ok.body.json
print(response){
"entry_id": "83353c08-0000-4000-8000-d0c500000083",
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"assigned": true
}Marks a WAITING walk-in as called and starts the hold-after-call countdown (they get "you're up", the shop holds their place for the policy window). "Call the next" is simply calling the first waiting entry the list returned. Single-winner and idempotent, a second call is a no-op returning the original called_at; an entry that is not waiting is refused. The entry must belong to the authenticated store. Business-auth + can_use_bookings.
The authenticated store the entry belongs to.
The entry to call. "Call the next" is the first waiting entry from the list. Must belong to the store.
false when it was already called, an idempotent no-op.
How long they are held before they lose their place.
ISO instant they were called (the existing time on a no-op re-call).
curl -X POST "https://www.membber.com/api/v1/bookings/queue/call" \
-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",
"entry_id": "83353c08-0000-4000-8000-d0c500000083"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/queue/call", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
entry_id: "83353c08-0000-4000-8000-d0c500000083"
},
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.callWalkInQueueEntry(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
entryId: "83353c08-0000-4000-8000-d0c500000083"
))
).ok.body.json
print(response){
"entry_id": "83353c08-0000-4000-8000-d0c500000083",
"called": true,
"hold_minutes": 1,
"called_at": "<called_at>"
}Marks a queue entry left or no_show, the merchant side of clearing the rail when a called walk-in does not appear or gives up. "left" and "no_show" are kept distinct for the record. Idempotent, removing an already-ended entry is a no-op. The entry must belong to the authenticated store. Business-auth + can_use_bookings. No fee (queue-side fees never exist in v1, no consent is captured at join).
The authenticated store the entry belongs to.
The entry to remove. Must belong to the store.
Why it is leaving the queue: 'left' (they gave up / no-answer) or 'no_show' (called and did not appear). Distinct outcomes for the record.
leftno_showThe terminal status the entry now holds.
true when the entry had already ended, an idempotent no-op.
curl -X POST "https://www.membber.com/api/v1/bookings/queue/remove" \
-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",
"entry_id": "83353c08-0000-4000-8000-d0c500000083",
"status": "left"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/queue/remove", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
entry_id: "83353c08-0000-4000-8000-d0c500000083",
status: "left"
},
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.removeWalkInQueueEntry(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
entryId: "83353c08-0000-4000-8000-d0c500000083",
status: .left
))
).ok.body.json
print(response){
"entry_id": "83353c08-0000-4000-8000-d0c500000083",
"status": "<status>",
"noop": true
}Every service period (Lunch/Dinner/…) for the authenticated store. Disabled periods are excluded unless asked for.
Include disabled periods. Defaults to active only.
The week this period is served. Omit a day to close it. e.g. {"friday":{"open":"18:00","close":"22:00"}}.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
The merchant's own menu sections served on this period, e.g. ["Breakfast","Breakfast Extras"]. Empty means no diner ever sees this period.
Every menu section this store actually has (distinct `menu_items.category`, live items only), alphabetical. These are the only values `PUT .../sections` accepts.
curl -G "https://www.membber.com/api/v1/bookings/service-periods" \
-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/bookings/service-periods", {
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.listServicePeriods(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"periods": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"sort": 1,
"week": {
"sunday": {
"open": "<open>",
"close": "<close>"
},
"monday": {
"open": "<open>",
"close": "<close>"
},
"tuesday": {
"open": "<open>",
"close": "<close>"
},
"wednesday": {
"open": "<open>",
"close": "<close>"
},
"thursday": {
"open": "<open>",
"close": "<close>"
},
"friday": {
"open": "<open>",
"close": "<close>"
},
"saturday": {
"open": "<open>",
"close": "<close>"
}
},
"last_seating_offset_min": 1,
"is_active": true,
"categories": [
"<categorie>"
]
}
],
"store_sections": [
"<store_section>"
]
}Adds a named service period to the authenticated store. Send a `client_token` to make the create idempotent.
Caller-generated idempotency key, a retry with the same token returns the SAME period, never a duplicate.
What the period is called, e.g. "Lunch" or "Dinner".
Display order among the store's periods.
The week this period is served. Omit a day to close it. e.g. {"friday":{"open":"18:00","close":"22:00"}}.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
Minutes before this period closes that the last table is seated. Overrides the store policy value for this period.
The week this period is served. Omit a day to close it. e.g. {"friday":{"open":"18:00","close":"22:00"}}.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
The merchant's own menu sections served on this period, e.g. ["Breakfast","Breakfast Extras"]. Empty means no diner ever sees this period.
curl -X POST "https://www.membber.com/api/v1/bookings/service-periods" \
-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"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/service-periods", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name"
},
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.createServicePeriod(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name"
))
).ok.body.json
print(response){
"period": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"sort": 1,
"week": {
"sunday": {
"open": "<open>",
"close": "<close>"
},
"monday": {
"open": "<open>",
"close": "<close>"
},
"tuesday": {
"open": "<open>",
"close": "<close>"
},
"wednesday": {
"open": "<open>",
"close": "<close>"
},
"thursday": {
"open": "<open>",
"close": "<close>"
},
"friday": {
"open": "<open>",
"close": "<close>"
},
"saturday": {
"open": "<open>",
"close": "<close>"
}
},
"last_seating_offset_min": 1,
"is_active": true,
"categories": [
"<categorie>"
]
}
}Hard-deletes a service period (config only, no appointment references it). Idempotent: deleting one already gone is a no-op. To keep it but stop serving it, PATCH is_active=false instead.
false when there was nothing to remove (idempotent).
curl -X DELETE "https://www.membber.com/api/v1/bookings/service-periods/19dd12bc-0000-4000-8000-d0c500000019?store_id=6659c139-0000-4000-8000-d0c500000066" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "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.DELETE("/api/v1/bookings/service-periods/{periodId}", {
params: { path: { periodId: "19dd12bc-0000-4000-8000-d0c500000019" }, query: { 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.deleteServicePeriod(
path: .init(periodId: "19dd12bc-0000-4000-8000-d0c500000019"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"period_id": "21c59859-0000-4000-8000-d0c500000021",
"removed": true
}Partial update of a service period the authenticated store owns. A window change shapes FUTURE availability only.
What the period is called, e.g. "Lunch" or "Dinner".
Display order among the store's periods.
The week this period is served. Omit a day to close it. e.g. {"friday":{"open":"18:00","close":"22:00"}}.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
Minutes before this period closes that the last table is seated. Overrides the store policy value for this period.
Set false to disable this period without deleting it; true to re-enable.
The week this period is served. Omit a day to close it. e.g. {"friday":{"open":"18:00","close":"22:00"}}.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
The merchant's own menu sections served on this period, e.g. ["Breakfast","Breakfast Extras"]. Empty means no diner ever sees this period.
curl -X PATCH "https://www.membber.com/api/v1/bookings/service-periods/19dd12bc-0000-4000-8000-d0c500000019" \
-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/bookings/service-periods/{periodId}", {
params: { path: { periodId: "19dd12bc-0000-4000-8000-d0c500000019" } },
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.updateServicePeriod(
path: .init(periodId: "19dd12bc-0000-4000-8000-d0c500000019"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"period": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"sort": 1,
"week": {
"sunday": {
"open": "<open>",
"close": "<close>"
},
"monday": {
"open": "<open>",
"close": "<close>"
},
"tuesday": {
"open": "<open>",
"close": "<close>"
},
"wednesday": {
"open": "<open>",
"close": "<close>"
},
"thursday": {
"open": "<open>",
"close": "<close>"
},
"friday": {
"open": "<open>",
"close": "<close>"
},
"saturday": {
"open": "<open>",
"close": "<close>"
}
},
"last_seating_offset_min": 1,
"is_active": true,
"categories": [
"<categorie>"
]
}
}Replaces the whole set of menu sections on one card in a single write, so the merchant can say what Breakfast actually is. Section names are the merchant's own `menu_items.category` values, matched case- and whitespace-insensitively; an unknown section is a 422 naming it. Idempotent (the same payload converges). Returns the period, so the client adopts the server's answer rather than what it sent.
Store the period belongs to (also authorises the merchant).
Every menu section served on this card, replacing what is there now. Must be sections the store actually has (see `store_sections` on the list response), an unknown one is refused by name rather than written as a link that serves nobody. An EMPTY array clears the card, which hides it from every diner; that is allowed and the merchant surface says so. A section may sit on more than one card (a café whose Hot Drinks runs all day), so this never touches other cards.
The week this period is served. Omit a day to close it. e.g. {"friday":{"open":"18:00","close":"22:00"}}.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
One serving window for one day. May cross midnight.
When this period opens on this day (24h HH:MM).
When this period closes on this day (24h HH:MM). A close EARLIER than the open means the service runs past midnight: "open":"21:00","close":"01:00" is a late bar serving until 1am the following morning. Only open == close is refused, because a zero-length window serves nobody.
The merchant's own menu sections served on this period, e.g. ["Breakfast","Breakfast Extras"]. Empty means no diner ever sees this period.
curl -X PUT "https://www.membber.com/api/v1/bookings/service-periods/19dd12bc-0000-4000-8000-d0c500000019/sections" \
-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",
"sections": [
"<section>"
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/bookings/service-periods/{periodId}/sections", {
params: { path: { periodId: "19dd12bc-0000-4000-8000-d0c500000019" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
sections: [
"<section>"
]
},
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.setServicePeriodSections(
path: .init(periodId: "19dd12bc-0000-4000-8000-d0c500000019"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
sections: ["<section>"]
))
).ok.body.json
print(response){
"period": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"name": "Example name",
"sort": 1,
"week": {
"sunday": {
"open": "<open>",
"close": "<close>"
},
"monday": {
"open": "<open>",
"close": "<close>"
},
"tuesday": {
"open": "<open>",
"close": "<close>"
},
"wednesday": {
"open": "<open>",
"close": "<close>"
},
"thursday": {
"open": "<open>",
"close": "<close>"
},
"friday": {
"open": "<open>",
"close": "<close>"
},
"saturday": {
"open": "<open>",
"close": "<close>"
}
},
"last_seating_offset_min": 1,
"is_active": true,
"categories": [
"<categorie>"
]
}
}Every service the store offers, each with its variants and add-ons. Scoped to the authenticated store; archived services are excluded unless asked for.
The store whose catalogue to read (must be the authenticated store).
Include archived services. Defaults to active only.
Image URLs the merchant uploaded for this service.
fixedfromper_personon_premisesonlinecustomer_sitePricing/duration variants for this service.
Optional paid add-ons that extend this service.
curl -G "https://www.membber.com/api/v1/bookings/services" \
-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/bookings/services", {
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.listBookingServices(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"services": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"category": "<category>",
"name": "Example name",
"description": "Added at the front desk",
"images": [
"<image>"
],
"duration_min": 1,
"price_pence": 1500,
"currency": "GBP",
"price_type": "fixed",
"party_min": 1,
"party_max": 1,
"buffer_before_min": 1,
"buffer_after_min": 1,
"processing_min": 1,
"location_type": "on_premises",
"rebook_interval_days": 1,
"online_visible": true,
"sort": 1,
"is_active": true,
"variants": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"duration_min": 1,
"price_pence": 1500,
"sort": 1,
"is_active": true
}
],
"addons": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"extra_min": 1,
"extra_pence": 1500,
"sort": 1,
"is_active": true
}
]
}
]
}Adds a service to the authenticated store. Send a `client_token` to make the create idempotent, a retry with the same token returns the same service rather than a duplicate.
Caller-generated idempotency key. Send the same token on a retry to get the SAME service back instead of a duplicate.
What the customer books.
Uploaded image URLs.
Appointment length in minutes.
Price in the smallest currency unit.
'fixed' | 'from' | 'per_person'.
fixedfromper_personDeposit policy JSON. WRITABLE but INERT until the money stage, no charge is wired from it here.
Prep time the unit is occupied before.
Clean-up time the unit is occupied after.
on_premisesonlinecustomer_siteWhether customers can see and book this online.
Image URLs the merchant uploaded for this service.
fixedfromper_personon_premisesonlinecustomer_sitePricing/duration variants for this service.
Optional paid add-ons that extend this service.
curl -X POST "https://www.membber.com/api/v1/bookings/services" \
-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",
"duration_min": 1
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/services", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name",
duration_min: 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.createBookingService(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name",
durationMin: 1
))
).ok.body.json
print(response){
"service": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"category": "<category>",
"name": "Example name",
"description": "Added at the front desk",
"images": [
"<image>"
],
"duration_min": 1,
"price_pence": 1500,
"currency": "GBP",
"price_type": "fixed",
"party_min": 1,
"party_max": 1,
"buffer_before_min": 1,
"buffer_after_min": 1,
"processing_min": 1,
"location_type": "on_premises",
"rebook_interval_days": 1,
"online_visible": true,
"sort": 1,
"is_active": true,
"variants": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"duration_min": 1,
"price_pence": 1500,
"sort": 1,
"is_active": true
}
],
"addons": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"extra_min": 1,
"extra_pence": 1500,
"sort": 1,
"is_active": true
}
]
}
}Partial update of a service the authenticated store owns. A price or duration change shapes FUTURE availability only, a booked appointment keeps the price and policy it was snapshotted with.
What the customer books.
Uploaded image URLs.
Appointment length in minutes.
Price in the smallest currency unit.
'fixed' | 'from' | 'per_person'.
fixedfromper_personDeposit policy JSON. WRITABLE but INERT until the money stage, no charge is wired from it here.
Prep time the unit is occupied before.
Clean-up time the unit is occupied after.
on_premisesonlinecustomer_siteWhether customers can see and book this online.
Set true to RESTORE an archived service (the inverse of /archive); false archives it. Archiving never deletes, existing appointments keep their snapshot either way.
Image URLs the merchant uploaded for this service.
fixedfromper_personon_premisesonlinecustomer_sitePricing/duration variants for this service.
Optional paid add-ons that extend this service.
curl -X PATCH "https://www.membber.com/api/v1/bookings/services/f46cf6b0-0000-4000-8000-d0c5000000f4" \
-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/bookings/services/{serviceId}", {
params: { path: { serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4" } },
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.updateBookingService(
path: .init(serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"service": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"category": "<category>",
"name": "Example name",
"description": "Added at the front desk",
"images": [
"<image>"
],
"duration_min": 1,
"price_pence": 1500,
"currency": "GBP",
"price_type": "fixed",
"party_min": 1,
"party_max": 1,
"buffer_before_min": 1,
"buffer_after_min": 1,
"processing_min": 1,
"location_type": "on_premises",
"rebook_interval_days": 1,
"online_visible": true,
"sort": 1,
"is_active": true,
"variants": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"duration_min": 1,
"price_pence": 1500,
"sort": 1,
"is_active": true
}
],
"addons": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"extra_min": 1,
"extra_pence": 1500,
"sort": 1,
"is_active": true
}
]
}
}Adds an add-on to a service the authenticated store owns. Idempotent per `client_token`.
Caller-generated idempotency key. Send the same token on a retry to get the SAME service back instead of a duplicate.
Extra minutes this add-on adds to the appointment.
Extra charge in the smallest currency unit (INERT until the money stage).
curl -X POST "https://www.membber.com/api/v1/bookings/services/f46cf6b0-0000-4000-8000-d0c5000000f4/addons" \
-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"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/services/{serviceId}/addons", {
params: { path: { serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name"
},
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.createBookingServiceAddon(
path: .init(serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name"
))
).ok.body.json
print(response){
"addon": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"extra_min": 1,
"extra_pence": 1500,
"sort": 1,
"is_active": true
}
}Partial update of an add-on belonging to a service the authenticated store owns.
Set false to archive this add-on.
curl -X PATCH "https://www.membber.com/api/v1/bookings/services/f46cf6b0-0000-4000-8000-d0c5000000f4/addons/bb965b7b-0000-4000-8000-d0c5000000bb" \
-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/bookings/services/{serviceId}/addons/{addonId}", {
params: { path: { serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4", addonId: "bb965b7b-0000-4000-8000-d0c5000000bb" } },
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.updateBookingServiceAddon(
path: .init(serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4", addonId: "bb965b7b-0000-4000-8000-d0c5000000bb"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"addon": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"extra_min": 1,
"extra_pence": 1500,
"sort": 1,
"is_active": true
}
}Sets the service inactive so it stops appearing in availability, but NEVER deletes it, every existing appointment keeps its snapshotted price/duration and its link to the service. Idempotent.
true once inactive (idempotent, archiving twice is a no-op).
curl -X POST "https://www.membber.com/api/v1/bookings/services/f46cf6b0-0000-4000-8000-d0c5000000f4/archive" \
-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/bookings/services/{serviceId}/archive", {
params: { path: { serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4" } },
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.archiveBookingService(
path: .init(serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"archived": true
}Adds a variant to a service the authenticated store owns. Idempotent per `client_token`.
Caller-generated idempotency key. Send the same token on a retry to get the SAME service back instead of a duplicate.
Overrides the service duration for this variant.
curl -X POST "https://www.membber.com/api/v1/bookings/services/f46cf6b0-0000-4000-8000-d0c5000000f4/variants" \
-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"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/services/{serviceId}/variants", {
params: { path: { serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name"
},
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.createBookingServiceVariant(
path: .init(serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
name: "Example name"
))
).ok.body.json
print(response){
"variant": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"duration_min": 1,
"price_pence": 1500,
"sort": 1,
"is_active": true
}
}Partial update of a variant belonging to a service the authenticated store owns.
Set false to archive this variant.
curl -X PATCH "https://www.membber.com/api/v1/bookings/services/f46cf6b0-0000-4000-8000-d0c5000000f4/variants/fb1b0c80-0000-4000-8000-d0c5000000fb" \
-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/bookings/services/{serviceId}/variants/{variantId}", {
params: { path: { serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4", variantId: "fb1b0c80-0000-4000-8000-d0c5000000fb" } },
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.updateBookingServiceVariant(
path: .init(serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4", variantId: "fb1b0c80-0000-4000-8000-d0c5000000fb"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"variant": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"duration_min": 1,
"price_pence": 1500,
"sort": 1,
"is_active": true
}
}Writes `stores.timezone` after validating it is a real IANA timezone. This is the anchor every day boundary, hour label and rollup in the bookings vertical reads, so an invalid value is refused rather than silently skewing a diary. Idempotent.
An IANA timezone name, e.g. "Europe/London". Validated against the runtime before it is written.
curl -X PUT "https://www.membber.com/api/v1/bookings/store-timezone" \
-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",
"timezone": "Europe/London"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/bookings/store-timezone", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
timezone: "Europe/London"
},
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.setStoreTimezone(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
timezone: "Europe/London"
))
).ok.body.json
print(response){
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"timezone": "Europe/London"
}The customer-facing view of a store's bookable services and people, the read the Book flow starts from, and the only way to obtain the `service_id` that availability requires. Public, because showing what a shop offers must not require an account. Returns only what is genuinely offered online: archived and staff-only services are absent, and so is anyone who is not taking online bookings.
The store whose bookable offering to read.
Bookable online, in the merchant's own order.
The merchant's own photos. Empty is a real state, the client draws its designed blank, never a stock photo.
What the customer is told the appointment lasts. Buffers are NEVER exposed, they are the shop's business, not the customer's.
'from' when performers price it differently, the exact figure locks at the person step.
fixedfromper_personSmallest party this service takes. 1 for a normal appointment.
Largest party bookable online. Above it, the client offers "message the restaurant". For a restaurant this rises to what the FLOOR can actually seat: declaring that two tables push together is the merchant saying "I will seat that party", so the join sets the ceiling and there is no second number to keep in step. It never falls below what the merchant set by hand.
Who picks the table for a tables store: 0 the house assigns, 1 the guest picks, 2 reserved. From booking_policies.table_choice_level (store-level).
Who can be booked, in the merchant's own order, the same order the diary columns use.
The bookable unit id, what `availability` and the booking call take as `unit_id`.
Per-person price/duration for a service, where it differs from the service default. Resolved so the confirm card can show the TRUE figure for this person.
The venue's dining areas, in the merchant's order. Empty unless it is a tables store with areas.
The area name the diner picks, e.g. "Window", "Main room", "Bar".
Optional one line, e.g. "Heated and covered".
False when the store is set up but PAUSED. The client shows one calm line rather than an empty slot grid pretending to be availability.
True when the shop has PUBLISHED a price list but nobody can perform any of it online, so `services` is what it charges and `people` is empty. The client renders the list as an untappable price list under one calm line instead of hiding the shop entirely. False whenever anything here is bookable, and then `services` holds only what is genuinely bookable, exactly as before.
curl -G "https://www.membber.com/api/v1/bookings/storefront" \
--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/bookings/storefront", {
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.getBookingStorefront(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"services": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"description": "Added at the front desk",
"images": [
"<image>"
],
"duration_min": 1,
"price_pence": 1500,
"currency": "GBP",
"price_type": "fixed",
"category": "<category>",
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"sort": 1,
"party_min": 1,
"party_max": 1,
"seat_choice_level": 1
}
],
"people": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"display_name": "<display_name>",
"photo_url": "https://example.com/image.jpg",
"sort": 1,
"overrides": [
{
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"duration_min": 1,
"price_pence": 1500
}
]
}
],
"areas": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"sort": 1,
"description": "Added at the front desk"
}
],
"accepting": true,
"catalogue_only": true
}Every bookable unit for the authenticated store, each carrying which services it offers. Archived units excluded unless asked for.
personspaceassetFree-text sub-type (e.g. "chair", "room").
The staff member this unit represents, when kind = person.
Diary column colour.
Whether the unit can hold appointments at all.
Whether customers can book it online (vs staff-only).
Which services this unit offers, with any per-unit overrides.
The dining AREA this table stands in. Null for every non-restaurant unit and for a table nobody has filed.
Resolved name of area_id ("Dining room", "Terrace"), so the joins screen can group a floor into its rooms.
Smallest party this table seats. Null for a person or an asset.
Largest party this table seats. Two of these added together is what makes a party of eight bookable at a six-seat restaurant.
The other tables this one physically pushes together with. SYMMETRIC, if Table 5 lists Table 6, Table 6 lists Table 5, and the server keeps it that way. Empty means "this table stands alone", which is the honest default: only the merchant knows their floor.
true when the joins above were DERIVED for a demo store rather than declared by the merchant, the client says so plainly above the list. Saving the screen makes them the merchant's own answer and this goes false.
curl -G "https://www.membber.com/api/v1/bookings/units" \
-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/bookings/units", {
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.listBookableUnits(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"units": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"kind": "person",
"unit_type": "<unit_type>",
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"display_name": "<display_name>",
"bio": "<bio>",
"photo_url": "https://example.com/image.jpg",
"colour": "<colour>",
"bookable": true,
"online_bookable": true,
"sort": 1,
"is_active": true,
"services": [
{
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"duration_override_min": 1,
"price_override_pence": 1500
}
],
"area_id": "d381ba0d-0000-4000-8000-d0c5000000d3",
"area_name": "<area_name>",
"seats_min": 1,
"seats_max": 1,
"joins_with": [
"<joins_with>"
],
"joins_are_assumed": true
}
]
}Adds a person / space / asset to the authenticated store. Idempotent per `client_token`.
Caller-generated idempotency key, a retry with the same token returns the SAME unit, never a duplicate.
'person' | 'space' | 'asset'. Defaults to 'person'.
personspaceassetOnly permitted when kind = person (the DB enforces this).
What the diary column is labelled.
personspaceassetFree-text sub-type (e.g. "chair", "room").
The staff member this unit represents, when kind = person.
Diary column colour.
Whether the unit can hold appointments at all.
Whether customers can book it online (vs staff-only).
Which services this unit offers, with any per-unit overrides.
The dining AREA this table stands in. Null for every non-restaurant unit and for a table nobody has filed.
Resolved name of area_id ("Dining room", "Terrace"), so the joins screen can group a floor into its rooms.
Smallest party this table seats. Null for a person or an asset.
Largest party this table seats. Two of these added together is what makes a party of eight bookable at a six-seat restaurant.
The other tables this one physically pushes together with. SYMMETRIC, if Table 5 lists Table 6, Table 6 lists Table 5, and the server keeps it that way. Empty means "this table stands alone", which is the honest default: only the merchant knows their floor.
true when the joins above were DERIVED for a demo store rather than declared by the merchant, the client says so plainly above the list. Saving the screen makes them the merchant's own answer and this goes false.
curl -X POST "https://www.membber.com/api/v1/bookings/units" \
-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",
"display_name": "<display_name>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/units", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
display_name: "<display_name>"
},
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.createBookableUnit(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
displayName: "<display_name>"
))
).ok.body.json
print(response){
"unit": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"kind": "person",
"unit_type": "<unit_type>",
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"display_name": "<display_name>",
"bio": "<bio>",
"photo_url": "https://example.com/image.jpg",
"colour": "<colour>",
"bookable": true,
"online_bookable": true,
"sort": 1,
"is_active": true,
"services": [
{
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"duration_override_min": 1,
"price_override_pence": 1500
}
],
"area_id": "d381ba0d-0000-4000-8000-d0c5000000d3",
"area_name": "<area_name>",
"seats_min": 1,
"seats_max": 1,
"joins_with": [
"<joins_with>"
],
"joins_are_assumed": true
}
}Partial update of a unit the authenticated store owns. Archiving (is_active=false) keeps existing appointments intact.
'person' | 'space' | 'asset'. Defaults to 'person'.
personspaceassetOnly permitted when kind = person (the DB enforces this).
What the diary column is labelled.
Set false to archive the unit (existing appointments are honoured).
personspaceassetFree-text sub-type (e.g. "chair", "room").
The staff member this unit represents, when kind = person.
Diary column colour.
Whether the unit can hold appointments at all.
Whether customers can book it online (vs staff-only).
Which services this unit offers, with any per-unit overrides.
The dining AREA this table stands in. Null for every non-restaurant unit and for a table nobody has filed.
Resolved name of area_id ("Dining room", "Terrace"), so the joins screen can group a floor into its rooms.
Smallest party this table seats. Null for a person or an asset.
Largest party this table seats. Two of these added together is what makes a party of eight bookable at a six-seat restaurant.
The other tables this one physically pushes together with. SYMMETRIC, if Table 5 lists Table 6, Table 6 lists Table 5, and the server keeps it that way. Empty means "this table stands alone", which is the honest default: only the merchant knows their floor.
true when the joins above were DERIVED for a demo store rather than declared by the merchant, the client says so plainly above the list. Saving the screen makes them the merchant's own answer and this goes false.
curl -X PATCH "https://www.membber.com/api/v1/bookings/units/cde691bf-0000-4000-8000-d0c5000000cd" \
-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/bookings/units/{unitId}", {
params: { path: { unitId: "cde691bf-0000-4000-8000-d0c5000000cd" } },
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.updateBookableUnit(
path: .init(unitId: "cde691bf-0000-4000-8000-d0c5000000cd"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066"
))
).ok.body.json
print(response){
"unit": {
"id": "00000d1b-0000-4000-8000-d0c500000000",
"store_id": "6659c139-0000-4000-8000-d0c500000066",
"kind": "person",
"unit_type": "<unit_type>",
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"display_name": "<display_name>",
"bio": "<bio>",
"photo_url": "https://example.com/image.jpg",
"colour": "<colour>",
"bookable": true,
"online_bookable": true,
"sort": 1,
"is_active": true,
"services": [
{
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"duration_override_min": 1,
"price_override_pence": 1500
}
],
"area_id": "d381ba0d-0000-4000-8000-d0c5000000d3",
"area_name": "<area_name>",
"seats_min": 1,
"seats_max": 1,
"joins_with": [
"<joins_with>"
],
"joins_are_assumed": true
}
}Records that this table can be pushed together with the tables you name, so a party too big for any single table can still be seated. Kept SYMMETRIC by the server: declaring Table 5 joins Table 6 also makes Table 6 join Table 5, and removing it removes both directions. Replaces the whole set atomically and is idempotent. Only a `space` unit can join another (joining two barbers is not a thing). Un-joining never disturbs a booking that already occupies both tables, that occupancy is recorded on the booking, not here; this only governs what may be offered next.
The COMPLETE set of tables this one pushes together with. Send an empty array to say it stands alone. Each must be an active space in the same store; a table can never join itself.
The set that now applies, on this table and symmetrically on each of theirs.
curl -X PUT "https://www.membber.com/api/v1/bookings/units/cde691bf-0000-4000-8000-d0c5000000cd/joins" \
-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",
"joins_with": [
"900d8ebc-0000-4000-8000-d0c500000090"
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/bookings/units/{unitId}/joins", {
params: { path: { unitId: "cde691bf-0000-4000-8000-d0c5000000cd" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
joins_with: [
"900d8ebc-0000-4000-8000-d0c500000090"
]
},
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.setBookableUnitJoins(
path: .init(unitId: "cde691bf-0000-4000-8000-d0c5000000cd"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
joinsWith: ["900d8ebc-0000-4000-8000-d0c500000090"]
))
).ok.body.json
print(response){
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"joins_with": [
"<joins_with>"
]
}Records that a unit offers a service (with optional per-unit duration/price overrides). Upsert on the (unit, service) pair, so it is idempotent, re-sending it converges rather than duplicating. Both the unit and the service must belong to the authenticated store.
The service this unit should offer (must belong to the same store).
Per-unit duration override.
Per-unit price override.
curl -X POST "https://www.membber.com/api/v1/bookings/units/cde691bf-0000-4000-8000-d0c5000000cd/services" \
-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",
"service_id": "993232e5-0000-4000-8000-d0c500000099"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/bookings/units/{unitId}/services", {
params: { path: { unitId: "cde691bf-0000-4000-8000-d0c5000000cd" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
service_id: "993232e5-0000-4000-8000-d0c500000099"
},
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.setBookableUnitService(
path: .init(unitId: "cde691bf-0000-4000-8000-d0c5000000cd"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
serviceId: "993232e5-0000-4000-8000-d0c500000099"
))
).ok.body.json
print(response){
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"service_id": "993232e5-0000-4000-8000-d0c500000099"
}Removes the (unit, service) mapping. Idempotent, removing one that is already gone is a no-op.
false when there was nothing to remove (idempotent).
curl -X DELETE "https://www.membber.com/api/v1/bookings/units/cde691bf-0000-4000-8000-d0c5000000cd/services/f46cf6b0-0000-4000-8000-d0c5000000f4?store_id=6659c139-0000-4000-8000-d0c500000066" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "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.DELETE("/api/v1/bookings/units/{unitId}/services/{serviceId}", {
params: { path: { unitId: "cde691bf-0000-4000-8000-d0c5000000cd", serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4" }, query: { 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.removeBookableUnitService(
path: .init(unitId: "cde691bf-0000-4000-8000-d0c5000000cd", serviceId: "f46cf6b0-0000-4000-8000-d0c5000000f4"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"service_id": "993232e5-0000-4000-8000-d0c500000099",
"removed": true
}The working intervals for a unit the authenticated store owns. Multiple rows for one weekday ARE the intervals; the gaps ARE the breaks.
0 = Sunday … 6 = Saturday.
Minutes past midnight the interval starts.
Minutes past midnight the interval ends (must be after start).
True for an interval that runs past midnight.
YYYY-MM-DD the interval starts applying (null = always).
YYYY-MM-DD the interval stops applying (null = open-ended).
curl -G "https://www.membber.com/api/v1/bookings/units/cde691bf-0000-4000-8000-d0c5000000cd/working-patterns" \
-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/bookings/units/{unitId}/working-patterns", {
params: { path: { unitId: "cde691bf-0000-4000-8000-d0c5000000cd" }, 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.listUnitWorkingPatterns(
path: .init(unitId: "cde691bf-0000-4000-8000-d0c5000000cd"),
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"patterns": [
{
"weekday": 0,
"start_minute": 0,
"end_minute": 1,
"spans_midnight": true,
"effective_from": "<effective_from>",
"effective_to": "<effective_to>",
"id": "00000d1b-0000-4000-8000-d0c500000000"
}
]
}Swaps the unit's ENTIRE working pattern for the set you send, atomically (the old rows and the new ones never coexist, and a half-applied pattern is impossible). Idempotent, replacing with the same set converges. The unit must belong to the authenticated store.
The COMPLETE new set of intervals for this unit. Send an empty array to clear the pattern.
0 = Sunday … 6 = Saturday.
Minutes past midnight the interval starts.
Minutes past midnight the interval ends (must be after start).
True for an interval that runs past midnight.
YYYY-MM-DD the interval starts applying (null = always).
YYYY-MM-DD the interval stops applying (null = open-ended).
0 = Sunday … 6 = Saturday.
Minutes past midnight the interval starts.
Minutes past midnight the interval ends (must be after start).
True for an interval that runs past midnight.
YYYY-MM-DD the interval starts applying (null = always).
YYYY-MM-DD the interval stops applying (null = open-ended).
curl -X PUT "https://www.membber.com/api/v1/bookings/units/cde691bf-0000-4000-8000-d0c5000000cd/working-patterns" \
-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",
"patterns": [
{
"weekday": 0,
"start_minute": 0,
"end_minute": 1
}
]
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.PUT("/api/v1/bookings/units/{unitId}/working-patterns", {
params: { path: { unitId: "cde691bf-0000-4000-8000-d0c5000000cd" } },
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
patterns: [
{
weekday: 0,
start_minute: 0,
end_minute: 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.replaceUnitWorkingPatterns(
path: .init(unitId: "cde691bf-0000-4000-8000-d0c5000000cd"),
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
patterns: [.init(
weekday: 0,
startMinute: 0,
endMinute: 1
)]
))
).ok.body.json
print(response){
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"patterns": [
{
"weekday": 0,
"start_minute": 0,
"end_minute": 1,
"spans_midnight": true,
"effective_from": "<effective_from>",
"effective_to": "<effective_to>",
"id": "00000d1b-0000-4000-8000-d0c500000000"
}
]
}