7 operations. Every schema and example on this page is generated from the platform contract.
Manager only (can_manage_staff). Returns the device secret once.
What the owner calls this iPad, e.g. "Front desk".
Shown ONCE and never retrievable: only its hash is stored. If it is lost the iPad is re-enrolled, which revokes the old row rather than leaving an un-revokable ghost.
curl -X POST "https://www.membber.com/api/v1/till/devices" \
-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",
"label": "<label>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/till/devices", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
label: "<label>"
},
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.tillEnrolDevice(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
label: "<label>"
))
).ok.body.json
print(response){
"device_id": "0180aba4-0000-4000-8000-d0c500000001",
"secret": "<secret>"
}/api/v1/till/pinActs on the CALLER, always. There is no path anywhere that lets one person choose or read another person’s PIN, because that would give every "Sara refunded £40" a second innocent explanation and the accountability would be worthless.
Six digits. Six rather than four because the lockout ladder allows 15 guesses before a manager must intervene: against 10^4 that is a 1-in-667 shot per person, against 10^6 it is 1-in-66,667. The cost is two extra taps. NEVER returned by any endpoint.
PIN_REJECTEDPIN_REUSEDMerchant-facing sentence. Never names another person’s PIN.
curl -X POST "https://www.membber.com/api/v1/till/pin" \
-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",
"pin": "<pin>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/till/pin", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
pin: "<pin>"
},
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.tillSetPin(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
pin: "<pin>"
))
).ok.body.json
print(response){
"ok": true,
"code": "PIN_REJECTED",
"message": "Added at the front desk"
}Manager only (can_manage_staff). Deliberately cannot set a PIN: a manager helps someone back in, they do not become them.
clear_lock releases a tier-3 lockout, which no timer will release on its own. revoke removes this person from the till and ends their live sessions. Neither can SET a PIN.
clear_lockrevokecurl -X POST "https://www.membber.com/api/v1/till/pin/manage" \
-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",
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"action": "clear_lock"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/till/pin/manage", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
user_id: "f73aee0f-0000-4000-8000-d0c5000000f7",
action: "clear_lock"
},
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.tillManagePin(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
userId: "f73aee0f-0000-4000-8000-d0c5000000f7",
action: .clearLock
))
).ok.body.json
print(response){
"ok": true
}People with a till PIN at this store. Authenticates on the device secret. Deliberately NOT built on the gym staff list, which is gated behind can_use_gym_management and would 403 for every coffee shop. Returns names only: this screen faces the counter.
What the tile shows. Names only: this screen faces the counter.
True when this person is inside a lockout window. The tile is shown but not tappable.
curl -G "https://www.membber.com/api/v1/till/roster"import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.GET("/api/v1/till/roster");
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.tillRoster().ok.body.json
print(response){
"store_name": "<store_name>",
"staff": [
{
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"display_name": "<display_name>",
"locked": true
}
]
}Same list as /till/roster, for callers that are signed in as the merchant rather than enrolled as a till. The gym kiosk needs it: a kiosk iPad runs the merchant session (that is how it shows the gym screens at all), so it cannot present a device secret. Names only.
What the tile shows. Names only: this screen faces the counter.
True when this person is inside a lockout window. The tile is shown but not tappable.
curl -G "https://www.membber.com/api/v1/till/staff" \
-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/till/staff", {
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.tillStaff(
query: .init(storeId: "6659c139-0000-4000-8000-d0c500000066")
).ok.body.json
print(response){
"staff": [
{
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"display_name": "<display_name>",
"locked": true
}
]
}Authenticates on the device secret (X-Till-Device header) plus the tapped person and their PIN. Returns a session that NAMES that person, so every action taken through it is attributable.
WHO is signing in, taken from the tile they tapped. The roster identifies and the PIN only proves, so the PIN is never a lookup key. That is what makes two staff sharing the same six digits a non-event, and what keeps a guesser at 1/keyspace however many people the store hires.
Six digits. Six rather than four because the lockout ladder allows 15 guesses before a manager must intervene: against 10^4 that is a 1-in-667 shot per person, against 10^6 it is 1-in-66,667. The cost is two extra taps. NEVER returned by any endpoint.
Till session token. 12 hours, a shift, not a remembered login.
What this person may do at the till, read from staff_permissions ONLY. There is deliberately no "they are in the organisation so they can do everything" fallback, which is how the Business app behaves today and is exactly what would let a Front desk tile issue a refund.
Idle window. Slides on writes only, never on reads.
Absolute. Never extended, however busy the till is.
INVALID is returned identically for a wrong PIN and for a person who is not on this roster, so the endpoint cannot be used to work out who works here.
DEVICE_UNKNOWNDEVICE_REVOKEDDEVICE_QUARANTINEDTILL_DISABLEDSTORE_PAUSEDINVALIDLOCKEDcurl -X POST "https://www.membber.com/api/v1/till/unlock" \
-H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
-H "Content-Type: application/json" \
-d '{
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"pin": "<pin>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/till/unlock", {
body: {
user_id: "f73aee0f-0000-4000-8000-d0c5000000f7",
pin: "<pin>"
},
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.tillUnlock(
body: .json(.init(
userId: "f73aee0f-0000-4000-8000-d0c5000000f7",
pin: "<pin>"
))
).ok.body.json
print(response){
"ok": true,
"token": "<token>",
"display_name": "<display_name>",
"permissions": [
"<permission>"
],
"hot_until": "<hot_until>",
"hot_ceiling_at": "<hot_ceiling_at>",
"failure": "DEVICE_UNKNOWN",
"locked_until": "<locked_until>",
"attempts_remaining": -9007199254740991
}For a gate that needs to know WHO is present but has no session to hand out: the kiosk exit. Runs the same bcrypt check and the same lockout ladder as an unlock, so a kiosk cannot be used as an unlimited PIN oracle that the till itself would have stopped. Mints nothing.
Six digits. Six rather than four because the lockout ladder allows 15 guesses before a manager must intervene: against 10^4 that is a 1-in-667 shot per person, against 10^6 it is 1-in-66,667. The cost is two extra taps. NEVER returned by any endpoint.
INVALIDLOCKEDcurl -X POST "https://www.membber.com/api/v1/till/verify" \
-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",
"user_id": "f73aee0f-0000-4000-8000-d0c5000000f7",
"pin": "<pin>"
}'import { createMembberClient } from "@membber/sdk-ts";
const membber = createMembberClient({
getAccessToken: () => process.env.MEMBBER_TOKEN,
});
const { data, error } = await membber.raw.POST("/api/v1/till/verify", {
body: {
store_id: "6659c139-0000-4000-8000-d0c500000066",
user_id: "f73aee0f-0000-4000-8000-d0c5000000f7",
pin: "<pin>"
},
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.tillVerify(
body: .json(.init(
storeId: "6659c139-0000-4000-8000-d0c500000066",
userId: "f73aee0f-0000-4000-8000-d0c5000000f7",
pin: "<pin>"
))
).ok.body.json
print(response){
"ok": true,
"display_name": "<display_name>",
"failure": "INVALID",
"locked_until": "<locked_until>",
"attempts_remaining": -9007199254740991
}