10 operations. Every schema and example on this page is generated from the platform contract.
Always scoped to the signed-in customer, ownership is part of the query, never a filter applied after fetching. There is no way to ask for somebody else's bookings.
Narrow to one store. Omit for every store this customer has booked with.
Include appointments that have already happened. Defaults to upcoming only.
The short reference the customer quotes at the shop.
What was booked, for a card that can say so. null when the service was deleted.
The merchant's own photograph of that service. null when they have not added one.
Who or which table it is with. null when the unit was archived.
The person's own portrait. null when they have not added one.
Every amount carries its currency, never inferred from the caller.
How many of the party are children (age 12 and under). 0 for a normal appointment.
High chairs requested (a request, not a seat).
How late you have said you will be. null when nothing has been said. THE BOOKING IS UNCHANGED, running late never moves a time or releases a table.
ISO instant you said it.
What you have told the shop about this booking, oldest first. Empty when you have told them nothing.
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.
curl -G "https://www.membber.com/api/v1/appointments" \
-H "Authorization: Bearer $MEMBBER_TOKEN"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.listAppointments();
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.listAppointments().ok.body.json
print(response){
"appointments": [
{
"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",
"service_name": "<service_name>",
"service_image_url": "https://example.com/image.jpg",
"unit_name": "<unit_name>",
"unit_photo_url": "https://example.com/image.jpg",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"status": "<status>",
"payment_status": "<payment_status>",
"price_pence": 1500,
"deposit_pence": 1500,
"currency": "GBP",
"party_size": 1,
"children": 1,
"high_chairs": 1,
"running_late_minutes": 1,
"running_late_at": "<running_late_at>",
"diner_notes": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"at": "<at>",
"text": "<text>"
}
],
"dietary_requirements": [
"Celery"
],
"dietary_note": "Added at the front desk",
"dietary_recorded_at": "<dietary_recorded_at>",
"dietary_recorded_by": "customer",
"dietary_has_allergen": true
}
]
}Converts a hold into a booking IN PLACE, so the slot is never free for an instant between paying and being booked. Double-booking is prevented by a database constraint rather than a check, so a lost race returns STALE_SLOT and nothing is written. Price and policy are SNAPSHOTTED onto the appointment: a later change to either never rewrites what the customer agreed to. Send an Idempotency-Key, retrying is safe and will not produce a second booking. No sign-in required: a guest books by giving guest_email (and ideally guest_name), and is filed as a customer of that store from those details. A booking that takes money up front is the exception, a deposit or a table premium needs an account to refund, and is refused with SIGN_IN_REQUIRED_FOR_DEPOSIT.
The store being booked.
The staff member / room / table.
The service booked. Its price and duration are SNAPSHOTTED onto the appointment.
ISO instant the appointment starts.
ISO instant the appointment ends.
The hold taken for this slot. Strongly recommended: without it the slot is unprotected between the availability read and this call.
The OTHER tables this booking occupies, when the party needs tables pushed together, send the whole `unit_ids` set from the availability slot. Must match the hold exactly: a hold on one table can never become a booking on two, and vice versa (STALE_SLOT). Each table gets its own occupancy row, so cancelling releases every one of them. Omitting it for a party that does not fit the single table you named is refused with PARTY_TOO_LARGE, the server checks the seats so no client can seat eight at a four.
Required when booking without a signed-in customer, somebody has to be reachable.
Who the appointment is for, when there is no customer record.
People in the party (adults + children). 1 for a normal appointment. This is the occupancy that seats the table.
Of the party, how many are children (age 12 and under). Counted within party_size, not on top of it. The venue reads this to prepare.
High chairs requested. A request, not a seat, never changes the party size. At most one per child.
The dining AREA the guest asked for (Level-1 seating), e.g. a window table. A preference the host tries to honour, never the assigned table, that stays unit_id. Null for no-preference and every non-restaurant booking; a stale or foreign area is dropped to null server-side.
Agreed price in the smallest currency unit. Snapshotted, a later price change never rewrites it.
ASSERTION of the flat premium-table upgrade the client showed (e.g. 800 = "+£8"). The server resolves the true figure from the table and refuses a mismatch (PREMIUM_CHANGED), then charges it UPFRONT with the deposit. Omit or 0 for an ordinary table.
ASSERTION of the minimum spend the diner agreed to (credited to the bill). Recorded as the agreement; NEVER charged. The server refuses a mismatch (PREMIUM_CHANGED) so the diner always agrees to the figure they saw.
Deposit due now. Greater than zero leaves the appointment pending_payment until it is settled.
ISO currency for the amounts above. Defaults to the store's.
What the party cannot eat, stated IN the booking call, one call, not two, the shape an agent taking a table end to end needs. OMITTING the field means nothing was said and the booking stays in the "nobody has been asked" state; SENDING it, even empty, is a statement, stamped with a timestamp exactly as a later dietary write stamps one. Each entry comes from the SAME fixed vocabulary as POST /api/v1/appointments/dietary (a tag outside it is refused), de-duplicated and stored as data the kitchen can match against a dish; detail the list cannot carry belongs in dietary_note.
CeleryGlutenCrustaceansEggsFishLupinMilkMolluscsMustardPeanutsSesameSoyaSulphitesTree nutsVegetarianVeganHalalKosherPescatarianDairy-freeGluten-freeThe dietary detail one tap in, "severe, airborne". At most 280 characters. Sent alone it still counts as a dietary statement.
The booking.
The SHORT reference the customer quotes and an agent reads back, six characters from 0123456789ACDEFGHJKMNPRTVWXY. There is no O, Q, I, L, S, Z, B or U in it, because each of those is the twin of a character that stayed, and a code that has to survive being said down a phone line cannot contain both halves of a pair. Server-issued, unique within the store, and fixed for the life of the booking, a reschedule moves the time, not the reference. Look one up again with getAppointmentByReference.
'confirmed', or 'pending_payment' when a deposit is still due.
ISO instant it starts.
ISO instant it ends, what the customer was told, not how long the unit is occupied.
'collect_deposit' when a deposit must be taken before this booking is safe; null when nothing is owed now.
Every table the booking occupies. Two when tables were pushed together, one otherwise.
curl -X POST "https://www.membber.com/api/v1/appointments" \
-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.createAppointment({
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.createAppointment(
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": "0d727dbb-0000-4000-8000-d0c50000000d",
"booking_ref": "<booking_ref>",
"status": "<status>",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"payment_next_action": "<payment_next_action>",
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
]
}Computed from the unit's working patterns in the STORE's timezone, at the store's own slot increment, honouring lead time and booking horizon. Every slot returned has been checked against the same single authority the booking call uses, so a slot listed here is genuinely bookable, but it is still a snapshot: another customer can take one between this read and your booking, which is exactly what STALE_SLOT reports. For a restaurant, a party too big for any single table is offered a PAIR the merchant declared joinable, read `unit_ids`, not just `unit_id`. Joins are a last resort: they are searched only when no single table seats the party, and exactly one pair (the tightest fit) is offered per time, because the house assigns a join rather than asking the guest which two tables to push together.
The store to read availability for.
The service being booked, its duration and buffers shape the slots.
First date to search, YYYY-MM-DD, in the STORE's timezone.
Last date to search, YYYY-MM-DD. Clamped to the store's booking horizon.
Restrict to one staff member / room / table. Omit to see every unit that offers the service.
People in the party. For a restaurant this filters tables to the party's seat band and scales the turn time. Omit or 1 for a normal appointment. (Coerced: it arrives as a query string.)
The booking being MOVED. Its own reservation stops counting against it, so the SAME table can be offered for a time overlapping where it already sits, which is what makes carry-forward-the-same-table possible on a reschedule. Omit when booking something new. Scoped to store_id and ignored for a booking that is already finished or cancelled; it can only ever hand back that booking's own slots, and anything taken is re-checked atomically.
Every slot that is genuinely bookable right now, soonest first.
Which staff member / room / table this slot belongs to.
ISO instant the APPOINTMENT starts (what the customer is told).
ISO instant the APPOINTMENT ends. The unit may be occupied longer if the service has buffers.
A flat upgrade to guarantee this exact table (e.g. 800 = "+£8"). 0/null when the table is not premium this way.
A minimum spend to hold this table, credited to the bill (e.g. 4000 = "£40 min spend"). Never charged upfront.
Smallest party the table seats, lets the client label it.
Largest party the table seats.
Every table this slot occupies. One id for an ordinary slot; two when the party needs tables pushed together. Send the whole array back on the hold and the booking, a join that reserves only one of its tables is worse than no join at all.
true when this slot is two tables pushed together. The house assigns which two; a guest is never asked to choose them.
Seats across every table in this slot. For a join it is the pair total, which is why a party of 8 fits two fours.
The floor plan, when the merchant has positioned their tables. Null means show the area list instead.
Rooms within the venue, in the merchant's own order.
Decking, garden, pavement, lets a client warn about weather.
Room width relative to the venue's biggest room. Null = unknown.
Room depth, on the same scale as width_rel.
Every positioned table. A table with no coordinates is omitted rather than drawn at the origin.
Degrees clockwise. 0 for round tables and anything bolted to a wall.
square · round · booth · bar. Drives how the table is drawn.
true = tapping this table genuinely reserves it. false = a preference the house tries to keep.
When this list was computed. A slot list is a snapshot of a moving thing, compare this before committing, and expect STALE_SLOT if it is old.
curl -G "https://www.membber.com/api/v1/appointments/availability" \
--data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066" \
--data-urlencode "service_id=993232e5-0000-4000-8000-d0c500000099" \
--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.getAppointmentAvailability({
params: { query: { store_id: "6659c139-0000-4000-8000-d0c500000066", service_id: "993232e5-0000-4000-8000-d0c500000099", 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.getAppointmentAvailability(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066", serviceId: "993232e5-0000-4000-8000-d0c500000099", fromDate: "<from_date>", toDate: "<to_date>")
).ok.body.json
print(response){
"slots": [
{
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"area_id": "d381ba0d-0000-4000-8000-d0c5000000d3",
"area_name": "<area_name>",
"area_sort": 1,
"premium_price_pence": 1500,
"min_spend_pence": 1500,
"seats_min": 1,
"seats_max": 1,
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
],
"joined": true,
"seats_total": 1
}
],
"room": {
"areas": [
{
"id": "00000d1b-0000-4000-8000-d0c500000000",
"name": "Example name",
"sort": 1,
"outdoor": true,
"width_rel": 1,
"height_rel": 1
}
],
"tables": [
{
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"display_name": "<display_name>",
"area_id": "d381ba0d-0000-4000-8000-d0c5000000d3",
"x": 1,
"y": 1,
"rotation": 1,
"shape": "<shape>",
"seats_min": 1,
"seats_max": 1,
"premium": true,
"premium_price_pence": 1500,
"min_spend_pence": 1500
}
]
},
"generated_at": "<generated_at>"
}Call without `confirm` to see exactly what cancelling costs before committing to it, then again with `confirm: true`. The fee is evaluated against the policy SNAPSHOTTED on the booking, so a shop tightening its terms next week cannot retroactively charge someone who booked under the old ones. Idempotent, cancelling twice is a no-op, never a second charge.
The appointment to cancel.
Why, for the record and the merchant.
false/omitted returns a PREVIEW of what cancelling costs and changes nothing. true performs it. A fee discovered AFTER the fact is the most complained-about thing in booking apps.
false when this was a preview.
What cancelling costs, from the policy the customer AGREED to, not the shop's current one.
What comes back of anything already paid. Never more than was taken.
true when it is still early enough to cancel free.
The window that applied to THIS booking.
curl -X POST "https://www.membber.com/api/v1/appointments/cancel" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"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.cancelAppointment({
body: {
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.cancelAppointment(
body: .json(.init(
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d"
))
).ok.body.json
print(response){
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"cancelled": true,
"fee_pence": 1500,
"refund_pence": 1500,
"currency": "GBP",
"within_free_window": true,
"cancel_window_hours": 1
}Creates a Stripe PaymentIntent on the store's connected account for the deposit a pending_payment booking owes, carrying the uniform 1.5% platform fee every member transaction does. IDEMPOTENT per appointment, retrying returns the SAME PaymentIntent, never a second charge. The booking flips to confirmed only when the deposit succeeds, reconciled server-side by the Connect webhook; a deposit that lands after the booking has lapsed is refunded automatically rather than kept.
The pending_payment appointment whose deposit is being collected.
The Stripe PaymentIntent collecting the deposit.
Confirm this PaymentIntent client-side (Apple Pay / card) to pay the deposit.
Short-lived key so the payment sheet can show the customer's saved cards. Null if one could not be minted, the sheet still works without it.
The platform-account customer the deposit is billed to.
The store's connected account the funds settle to.
The deposit due now, in the smallest currency unit.
ISO currency of the deposit.
curl -X POST "https://www.membber.com/api/v1/appointments/deposit-intent" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"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.createAppointmentDepositIntent({
body: {
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.createAppointmentDepositIntent(
body: .json(.init(
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d"
))
).ok.body.json
print(response){
"payment_intent_id": "0b6642a5-0000-4000-8000-d0c50000000b",
"client_secret": "<client_secret>",
"ephemeral_key": "<ephemeral_key>",
"stripe_customer_id": "847b302a-0000-4000-8000-d0c500000084",
"connected_account_id": "231b6f63-0000-4000-8000-d0c500000023",
"amount_pence": 1500,
"currency": "GBP"
}The structured half of "anything we should know". A note passes a SENTENCE; this passes DATA, a set of named requirements the kitchen can match against its own dishes and the host card can flag at check-in. REPLACES the standing set rather than appending to it, because front of house needs the current fact, not an archaeology exercise; every previous value is kept in the booking ledger. An EMPTY set with this call is a statement, "we asked, there is nothing", and is stored as one, distinct from a booking nobody has asked. Deliberately buzzes nobody: an allergy belongs on the booking and on the card at check-in, not in a push at twenty past six about an eight o'clock table. Errors: APPOINTMENT_NOT_FOUND (not yours, or gone), INVALID_TRANSITION (already cancelled, finished or marked a no-show).
The booking. Must belong to the caller.
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/appointments/dietary" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"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/appointments/dietary", {
body: {
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.setAppointmentDietary(
body: .json(.init(
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
}Holds a slot so nobody else can take it mid-payment. Ten minutes by default, long enough for a card security check, short enough that an abandoned checkout does not sterilise a busy slot. Idempotent per hold_token. Returns STALE_SLOT if the slot went first, or HOLD_LIMIT (with Retry-After) if this unit already has too many live holds for that day, which is the anti-squatting guard. No sign-in required: a stranger arriving from a QR code or a shared link holds a slot the same way a member does. Send a session token if you have one and the hold is recorded against that customer.
The staff member / room / table to reserve.
ISO instant the appointment would start.
ISO instant the appointment would end.
Caller-generated idempotency key. The SAME token returns the SAME hold rather than taking a second slot, so a retry or a double tap can never consume two times.
The OTHER tables of a join, when the slot needs tables pushed together, every id from the availability slot's `unit_ids` except `unit_id`. All of them are held under this one token, all or nothing: a hold that got one table and missed the other would sell a party of eight a table for four. The merchant must have declared the join, or the call is refused with JOIN_NOT_ALLOWED.
The reservation.
ISO instant the hold lapses, 10 minutes by default (founder decision D10). Show this to the customer rather than guessing it.
true when this token already had a live hold and it was returned unchanged (idempotent replay).
Every table this hold reserves. Two for a join, one otherwise.
curl -X POST "https://www.membber.com/api/v1/appointments/holds" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"hold_token": "<hold_token>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.createAppointmentHold({
body: {
unit_id: "eeebf9b6-0000-4000-8000-d0c5000000ee",
starts_at: "<starts_at>",
ends_at: "<ends_at>",
hold_token: "<hold_token>"
},
headers: { "Idempotency-Key": crypto.randomUUID() },
});
if (error) {
// Typed error envelope: { error: { code, message, requestId } }
throw new Error(`${error.error.code}: ${error.error.message}`);
}
console.log(data);import MembberSwift
let client = MembberClient(
serverURL: MembberClient.productionServerURL,
tokenProvider: { session.accessToken }
)
let response = try await client.api.createAppointmentHold(
body: .json(.init(
unitId: "eeebf9b6-0000-4000-8000-d0c5000000ee",
startsAt: "<starts_at>",
endsAt: "<ends_at>",
holdToken: "<hold_token>"
))
).ok.body.json
print(response){
"hold_id": "4112d09b-0000-4000-8000-d0c500000041",
"expires_at": "<expires_at>",
"reused": true,
"unit_ids": [
"eeebf9b6-0000-4000-8000-d0c5000000ee"
]
}The half of a booking nobody builds: everything that happens AFTER the yes. Running late reaches the diary immediately and moves nothing, no slot shifts, no table is re-let, the host simply knows. A note lands on the booking and on the card at check-in, and deliberately does NOT buzz anybody: a push at twenty past six about an eight o'clock table is something the host will have forgotten by eight. Errors: APPOINTMENT_NOT_FOUND (not yours, or gone), INVALID_TRANSITION (already cancelled, finished or marked a no-show), TOO_LATE_TO_SAY (the booking has already ended), TOO_MANY_MESSAGES (a booking is not a messaging channel).
The booking this is about. Must belong to the caller.
'running_late' says how many minutes behind you are, and changes nothing about the booking. 'note' passes a sentence to the restaurant, an allergy, a high chair, a party that grew.
running_latenoteRequired when kind is running_late. Minutes behind the booked time. A later message replaces an earlier one.
Required when kind is note. What the restaurant needs to know, in the diner's own words.
Your own id for this message. The same token twice is one message, so a retried tap, or an offline queue flushed twice, never says it twice.
true when the message was stored. false is never returned, a refusal is an error with a code.
true when this client_token had already been used, so nothing new was written. Still a success.
How late the booking now stands at, after this call. null when nothing has been said.
ISO instant the running-late claim was made.
curl -X POST "https://www.membber.com/api/v1/appointments/message" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"kind": "running_late"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/appointments/message", {
body: {
appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d",
kind: "running_late"
},
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.messageAppointment(
body: .json(.init(
appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d",
kind: .runningLate
))
).ok.body.json
print(response){
"appointment_id": "0d727dbb-0000-4000-8000-d0c50000000d",
"accepted": true,
"duplicate": true,
"running_late_minutes": 1,
"running_late_at": "<running_late_at>"
}Returns the signed .pkpass for one of the caller's own appointments, base64-encoded. Scoped to the signed-in customer: ownership is checked against the row, and a booking that is not yours is indistinguishable from one that does not exist. A cancelled booking answers GONE rather than a pass, the card it had has been evicted.
The booking to make a pass for. Must belong to the caller.
The signed .pkpass file, base64-encoded. Decode to bytes and hand to PassKit.
The pass serial. Stable per appointment, so a reschedule updates the SAME card rather than adding a second one.
curl -G "https://www.membber.com/api/v1/appointments/pass" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
--data-urlencode "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.getAppointmentWalletPass({
params: { query: { appointment_id: "0d727dbb-0000-4000-8000-d0c50000000d" } },
});
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.getAppointmentWalletPass(
query: .init(appointmentId: "0d727dbb-0000-4000-8000-d0c50000000d")
).ok.body.json
print(response){
"pkpass_base64": "<pkpass_base64>",
"serial_number": "<serial_number>"
}Secures the NEW slot before releasing the OLD one, so if the destination has gone the original booking survives untouched, you are never left with nothing. Returns STALE_SLOT if the new time went first, or OUTSIDE_POLICY_WINDOW if it is too close to the appointment to change online.
The appointment to move.
Where it is moving to, may be the same staff member / room / table.
ISO instant of the new start.
ISO instant of the new end.
A hold taken on the NEW slot. Recommended for the same reason as booking: without it the destination is unprotected.
true when the booking was nudged within the same unit (the block moved rather than being re-taken).
curl -X POST "https://www.membber.com/api/v1/appointments/reschedule" \
-H "Authorization: Bearer $MEMBBER_TOKEN" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"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.rescheduleAppointment({
body: {
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.rescheduleAppointment(
body: .json(.init(
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",
"starts_at": "<starts_at>",
"ends_at": "<ends_at>",
"unit_id": "eeebf9b6-0000-4000-8000-d0c5000000ee",
"moved_in_place": true
}