Require current native and presentation authority
An authorized operator must enable the venue's native QR policy, activate current-session public access and provide a physical-table locator through a valid typed presentation resource. The public endpoints cannot configure policy, issue a session PIN, approve a guest or choose an operational guest slot. Admission policy and later order-confirmation policy are separate.
The locator is a nonsecret physical reference, not a bill credential. Resolution requires the actual browser Origin and current Storefront binding. A supplied connectionId is a nonsecret locator for a configured connection, not permission to use it. Browser requests use credentials: 'omit' and let the browser supply Origin; do not add a fabricated Origin header or account-token fallback.
In the venue workspace, open Floor (/venues/:venueId/floor), select the table and open QR table access. Configure the venue policy and Admission of guest order requests, then activate Session guest access for the current session. DIRECT and STAFF_CONFIRM govern order admission separately from PIN or staff-approved joining. These QR controls require existing venue floor-operation authority. Published custom-host access separately requires the current QR resource and verified domain connection in Storefront settings (/venues/:venueId/settings/storefront), using its venueSettings gate and owning Storefront permissions. The guest API grants no administrative authority.
Resolve and request admission
Resolution returns public labels, currently permitted join methods and an opaque joinContext valid for 60 seconds. It does not reveal a private membership or table bill. Do not parse that context or derive a session identifier from it.
SESSION_PIN requires the current six-digit service-session PIN. STAFF_APPROVAL accepts no PIN, client-selected slot or staff decision. The example chooses STAFF_APPROVAL only after checking the returned policy. All identifiers and the idempotency key below are synthetic; the documentation does not send these requests. A real client creates one key per command and retains it for exact retries.
- Resolve the actual 24-character locator from the restaurant's QR with the configured connection when one is required.
- Check joinAvailable, the permitted method and a non-null joinContext. Stop if the current service session cannot admit a guest.
- Create a join with its original context, permitted method and one stable Idempotency-Key. Include a PIN only for SESSION_PIN.
- Keep the returned requestToken in the private guest flow. It authorizes only reading and claiming that exact join request.
- Read the join decision when needed. Claim only an approved join, using the join credential and a separate stable key for the claim command.
const locator = 'qr_demo_locator_00000001';
const connectionId = '0604de3a-da74-4ad5-89f6-2dfe000d5391';
const url = new URL(
`/public/table-locators/${encodeURIComponent(locator)}`,
'https://api.hallify.co'
);
url.searchParams.set('connectionId', connectionId);
const resolved = await fetch(url, { credentials: 'omit', cache: 'no-store' });
if (!resolved.ok) {
throw new Error(`HTTP ${resolved.status}`);
}
const context = await resolved.json();
if (
!context.joinAvailable ||
!context.allowStaffApprovalJoin ||
!context.joinContext
) {
throw new Error('Staff-approved admission is unavailable.');
}
const response = await fetch(`${url.origin}${url.pathname}/joins`, {
method: 'POST',
credentials: 'omit',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': 'docs-table-admission-001',
},
body: JSON.stringify({
joinContext: context.joinContext,
method: 'STAFF_APPROVAL',
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const join = await response.json();Claim the original membership and respect revocation
PENDING_APPROVAL is not membership. APPROVED allows the first claim before expiresAt. Claiming creates one native TableSessionGuest membership and returns its separate session-scoped capability. The join credential never becomes a general restaurant or bill key. TABLE_SESSION observation uses the claimed membership's READ authority, not the join request token.
After a lost response, retry the same claim command with its original key and payload. The original capability is disclosed again only while that original membership and presentation authority remain current. A fresh key does not issue another membership for an already claimed join.
Membership reads recheck the current venue, session, guest slot, authority epochs and exact Origin binding. Closing, reseating, transfer/merge, access disablement or explicit revocation can fence stale authority. A general configuration revision or host-proof renewal is not by itself a new membership grant.
On 429, respect Retry-After. Wrong-PIN and join-attempt limits are native admission controls; changing keys is not a way around them. On 409, distinguish an idempotency conflict from stale revision, changed join state or unavailable guest slot using the documented machine code. Preserve the original command while checking its outcome.
Use the same table flow on a verified domain
Hallify serves the canonical /table/:locator page. A published verified restaurant or brand domain can serve the same path with its connection derived from the current host publication. An explicit connectionId must match that binding. There is no fallback to another connection or to canonical-origin admission.
The page preserves the actual verified origin instead of redirecting a table flow to a different canonical host. The published snapshot identifies a presentation connection only: the locator, native membership, resource scope and current approved Origin still authorize every guest request. An unpublished QR-only custom page is outside this hosted route; a separately configured own-site frontend uses the same native API authority.
Configure the current native menu and review a quote
After membership is claimed, read order-menu with its membership capability. Use that exact catalog menuVersionId and graphHash. Submit 1–100 uniquely correlated lines, each selecting exactly one menuItemId or menuVariantId with a whole quantity from 1 to 999. Modifiers and structured component choices must come from that published configurator; the server validates eligibility and every amount.
Quote creation creates no Order, kitchen demand or payable amount. The response includes the exact native quote, DIRECT or STAFF_CONFIRM mode, effective expiresAt and an opaque proof. The deadline is at most five minutes and can be shorter when source authority expires. Catalog prices support display; server quote amounts remain authoritative.
This browser example accepts the already claimed session/capability, the actual catalog and selections produced by your configurator. The documentation does not call it. It constructs a local submission containing the original selection, quote and proof, and retains the server's mode and effective expiresAt alongside it for review. POST only submission: mode and the outer deadline are response metadata, not submit-body fields. Use that outer expiresAt for the review countdown rather than the possibly later native quote.expiresAt. Preserve the original submission verbatim; do not rebuild the quote or choose another tenant, channel, guest slot or staff identity.
async function quoteTableOrder({ sessionId, capability, catalog, items }) {
const selection = {
items,
clientMenuVersionId: catalog.menuVersionId,
clientGraphHash: catalog.graphHash,
};
const response = await fetch(
`https://api.hallify.co/public/table-sessions/${encodeURIComponent(sessionId)}/order-requests/quote`,
{
method: 'POST',
credentials: 'omit',
cache: 'no-store',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${capability}`,
},
body: JSON.stringify(selection),
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const quoted = await response.json();
return {
submission: {
selection,
quote: quoted.quote,
proof: quoted.proof,
},
mode: quoted.mode,
expiresAt: quoted.expiresAt,
};
}Submit once and recover the original result
Before POST order-requests, persist the exact returned selection/quote/proof body and one new Idempotency-Key in your source-bound private guest storage. DIRECT atomically admits one canonical native Order for this batch and returns ACCEPTED. STAFF_CONFIRM returns PENDING without an Order, kitchen demand or payable amount; authorized staff review and approve or reject it in the current session access panel.
On UNKNOWN, network loss, reload or a lost response, retain the original source, body and key and retry that same command. An authenticated exact replay returns the original immutable receipt; it does not create another logical order or debit the fresh submit budget. Quote expiry does not justify replacing an uncertain command with a fresh key or quote. Changed membership or revoked authority must be resolved through native access, not an account-token fallback.
An original PENDING receipt remains PENDING even after staff has decided. Read your owned request or bounded request history for the current ACCEPTED, REJECTED or INVALIDATED state and nullable frozen admission outcome. Pending requests can be invalidated by expiry, changed ordering policy or native source changes. Editing an unsent cart requires a fresh quote; a submitted unknown command stays frozen until its outcome is known.
Refresh native status and enter separate checkout
A TABLE_SESSION observer uses current membership READ authority and signals that native data changed. Its empty publicGuest.resourceChanged payload is not order or financial truth. Refetch only the caller-owned request summaries/detail; dispose the old observer when its source changes or access expires. Do not create an unbounded polling loop.
Read, quote and fresh submit have separate native attempt budgets. Respect 429 and Retry-After; failed fresh work still consumes its attempt. An exact original submit replay uses the recovery path. New keys are not a way around limits, source checks or a terminal request decision.
ACCEPTED means native order admission, not payment. The original quote and outcome remain historical. Open the existing separate guest-payments flow for the current assigned portions or permitted session bill, explicit consent, tips and checkout. Ordering does not modify another guest’s cart or allocations, and it does not submit the staff active draft.
Keep membership separate from ordering and payment
Membership admission, native ordering and payment remain separate authorities. A join request cannot add an order or expose a bill. A claimed current membership authorizes only its own native guest ordering; separate QR_MEMBERSHIP payment admission uses that membership capability and the table-session UUID. Neither step selects another guest’s operational slot.
An authorized operator explicitly enables paymentScope in the staff QR policy: DISABLED is the default; ASSIGNED_ITEMS limits collection to current assigned portions; SESSION_BILL permits the current session bill within native authority. Then use the separate guest-payments bill, offer and explicit consent flow. Payment policy does not change admission or order-confirmation policy.
Keep join credentials, PIN and membership capability outside URLs, analytics, logs, Redux and account authentication. TableSessionGuest is an operational slot, not a CRM identity. Do not build a polling loop or treat a join as an observer resource. The SDK exposes the native methods; table widgets start through the exact verified QR attachment and optional public locator.
Widget locator resolution and new join demand additionally check signed context at the actual frame Origin. Claim and existing membership use retained parent/connection/session provenance; a renewed proof cannot revive revoked membership. Isolate each stable widget slot's original join/order/payment journal and never transfer PIN, native capability or payment credentials in parent events.
Associate only your original QR history
After native membership admission, read portal-association-statement at the actual original Origin and show the exact localized termsVersion/termsDigest. Explicit initial consent freezes commandId, expectedRevision 0, original session/membership/seat and a server-selected target proof. prepare-portal-booking-proof and associate-portal-history accept tableGuestToken alone or together with guestPortalToken; table-only real BOOKING verification reserves the exact future Portal continuation.
prepare-portal-reentry-proof requires BOTH the original tableGuestToken AND current guestPortalToken plus the actual consumed PORTAL_REENTRY challenge for that session/contact version. Recent login time is not proof. Keep OTP private, use the real entered code and retain original key/body before sending; wrong codes consume budget. Exact lost-response replay recovers the original association decision and nullable reserved continuation without a new proof, seat or key.
Portal QR_VISIT and own order receipts remain attached to the original immutable membership/session/seat after close, transfer or merge. They do not expose another member’s receipts, SESSION_BILL or a native table/payment grant. Portal consent reads/withdrawal need current ownership; private history also needs fresh contact assurance and latest ACCEPT. Reconsent requires a fresh target proof and cannot undo hard revocation. Follow the Portal guide; do not pass a gpc bearer in a cross-tab URL or widget parent event.