# MojoWare Kiosk Developer Guide Product scope: MojoWare Kiosks and their WebApps. This is not a reference for other MojoWare products. Canonical guide: https://mojoware.net/docs/kiosk/ Kiosk LLM index: https://mojoware.net/docs/kiosk/llms.txt # Kiosk WebApp integration Run your HTTPS WebApp on a managed MojoWare kiosk, identify the device, and request approved cards on its Advertising display. **Reviewed against the current implementation on 16 October 2026.** This guide describes the shipped `window.mojoware` bridge. It is intended for WebApp developers and the operators configuring their screens. [Download the complete Markdown guide](/docs/kiosk/llms-full.txt) · [LLM discovery index](/docs/kiosk/llms.txt) ## Start with the right interface | You want to… | Use | | --- | --- | | Identify the kiosk and sign it into your product | [Authentication](/docs/kiosk/authentication/) | | Pause ads or show a loading, jackpot, or service card | [Advertising cards](/docs/kiosk/advertising-cards/) | | Check a method, return value, or error | [Bridge reference](/docs/kiosk/bridge-reference/) | | Assign content, capture screenshots, inspect health, or restart a WebApp | The [Kiosk portal](https://kiosk.mojoware.net/), with operator permissions | The browser bridge is intentionally small. It does not expose native management credentials, Wi-Fi configuration, reboot commands, or a general remote-management API. ## Configure the WebApp 1. In the Kiosk portal, choose the correct group and open **WebApps**. Create or select your app using its name and HTTPS URL. 2. Open **Fleet**, click the kiosk row, and select **Monitors**. 3. Choose the intended screen and use **Change content** to assign the WebApp. Check that the kiosk has applied the change. 4. The portal creates an application audience automatically. Only override it under Advanced when coordinating an explicit audience with your backend. 5. Run the app as the top-level page on the kiosk. Its actual HTTPS origin must be allowed by its assigned configuration. An iframe or an ordinary desktop browser is not a managed kiosk context. Authentication and card permissions are separate. Assigning an app does not automatically let it interrupt Advertising. ## Detect the bridge safely: window.mojoware.ready No script tag or npm SDK is required. The managed browser injects the interface. ```js async function getKioskBridge() { const bridge = window.mojoware; if (!bridge) { throw new Error("This page is not running in a managed kiosk browser."); } await bridge.ready; return bridge; } try { const bridge = await getKioskBridge(); const deviceId = bridge.device_id; // Public identifier; not a password. // Continue with your product's kiosk login. } catch { // Show an appropriate non-kiosk or unavailable state. // Do not invent a device ID or repeatedly reload the page. } ``` `ready` is a Promise, not a function. Read `device_id` and `sn` after it resolves; both contain the same public device identifier. Readiness can reject after about five seconds when native access is unavailable. ## Where to investigate - **Monitors → selected screen:** applied content, captures, WebApp performance, and activity. - **Observations → Events:** operational events, including card transitions and reported WebApp errors. - **Observations → Activity:** who changed configuration or sent commands, when, and the recorded outcome. - **Network / Health:** saved reports and explicit diagnostic tools. **Online** describes the management WebSocket connection. **Healthy** describes the last reported health assessment; read its report age. **Seen** is the last recorded device contact. These timestamps are not interchangeable. ## Documentation scope Examples use placeholder app names and credentials. No real device, customer, credential, or fleet data is embedded here. The underlying contracts are maintained in the server repository at `docs/web_developer/kiosk_api.md`, the browser shim at `apps/mojoware/kiosk/pi/bridge/page.js`, and the native advertising control implementation. This guide does not introduce a new `devices/identify` endpoint or define a product's brand-membership, inventory-stocking, or session policy. Those decisions belong to the integrating product. --- # Identity & authentication Use the public device ID plus a signed product credential to sign a kiosk into **your own backend**. Your product decides which devices it accepts and issues its own application session. [Start here](/docs/kiosk/home/) · [Complete Markdown](/docs/kiosk/llms-full.txt) ## Get a credential: getAuthKey ```js async function loginKiosk() { const bridge = window.mojoware; if (!bridge) throw new Error("Managed kiosk bridge unavailable"); await bridge.ready; const device_id = bridge.device_id; const auth_key = await bridge.getAuthKey(); // Example route owned by YOUR product, not a MojoWare endpoint. const response = await fetch("/api/device/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ device_id, auth_key }) }); if (!response.ok) throw new Error("Kiosk login failed"); return response.json(); } ``` With no argument, `getAuthKey()` uses the current monitor's approved WebApp audience. This is preferred: the page does not need to guess or hard-code it. `getAuthKey("wmwx")` is also supported **only when `wmwx` is that assigned app's configured audience**. An explicit null, empty, invalid, or mismatched audience is rejected. The browser cannot request a different app's identity. ## What the credential means - Format: `mwk1..`. - The credential belongs to one **device + application audience + credential revision**. It is not a fleet-wide API key. - It is stable across reloads, reboots, and normal assignment/configuration changes for that same audience and revision. A credential reset or approved identity recovery invalidates the old revision. - It is a bearer credential: a copied token can be replayed. It does not by itself prove freshness, current connection, or physical presence. - Keep it out of logs, URLs, analytics, screenshots, and error reports. Send it only to your trusted backend over HTTPS; do not persist it unnecessarily in browser storage. Native management keys, kiosk management JWTs, and signing private keys are never returned by the bridge. ## Verify on your server For online verification, your backend calls the kiosk's regional MojoWare API: ```http POST /api/kiosk/identity/verify Authorization: apikey Content-Type: application/json { "device_id": "", "auth_key": "", "audience": "" } ``` Use the regional API assigned to your integration; the US API is `https://api.mojoware.net`. Keep the verifier API key on your server. It needs `verify_kiosk_attestation` for the kiosk's current group. Choose the expected audience from trusted server configuration, not an unvalidated value submitted by the client. Online verification checks the registered signing key, current tenant/assignment, business eligibility, current configured audience, and credential revision. Successful response: ```json { "status": true, "data": { "ok": true, "device_id": "", "sn": "", "assignment_version": 7, "state": "active", "group_id": 123, "credential_version": 1 } } ``` An invalid credential returns `data: { "ok": false, "reason": "invalid_auth_key" }`. Permission or transport failures can also occur. **Check both the HTTP outcome and `data.ok === true`; HTTP 200 or `status: true` alone is not verification.** After verification, your backend still checks its own authorized device association and issues its own session. Do not automatically enroll an unknown device merely because it supplies a valid-looking ID or token. A local verifier needs a trusted, previously associated public key or an authorized credential pin. Never trust a public key supplied alongside the login request. Local signature verification alone cannot discover a cloud reset, revocation, or reassignment; your product also owns revocation of sessions it has already issued. ## Fresh challenge proofs: getAuthProof Use this flow when your backend needs a fresh, single-use challenge rather than the stable bearer: ```js const bridge = await getKioskBridge(); // Helper from Start here. const proof = await bridge.getAuthProof({ nonce: challengeFromYourBackend, audience: expectedConfiguredAudience }); // Send proof to your backend over HTTPS. ``` The nonce is a server-generated string of 16–256 characters. The audience must match the assigned app. The native proof expires after 30 seconds and returns `{ payload, signature }`. Your backend calls the same verifier using the separate proof mode: ```js const verificationBody = { proof, // The complete { payload, signature } returned by the bridge. expected_nonce: issuedNonce, audience: expectedConfiguredAudience, origin: "https://your-app.example" }; ``` Send this body from your backend with its tenant-scoped verifier API key. Do not mix `auth_key` or `device_id` fields into this proof-mode request. Check `data.ok`, atomically consume the issued nonce on your backend, and reject reuse. The public device ID alone is never authentication. ## Token format for implementers The stable token payload has exactly `v`, `device_id`, `aud`, `key_id`, and `credential_version`. It is sorted, compact UTF-8 JSON encoded as canonical unpadded base64url. Ed25519 signs the ASCII bytes `mwk1.`, including the prefix. The signature is also canonical unpadded base64url. Challenge payloads instead contain `v, device_id, sn, key_id, assignment_version, nonce, aud, origin, iat, exp`; their Ed25519 signature covers sorted compact UTF-8 JSON. Do not use the stable-token signing format for a challenge proof. --- # Advertising cards A WebApp can temporarily replace the playlist on an **approved Advertising screen on the same kiosk**. The operator chooses the media and allowed keys; the WebApp requests a key. [Start here](/docs/kiosk/home/) · [Bridge reference](/docs/kiosk/bridge-reference/) · [Complete Markdown](/docs/kiosk/llms-full.txt) ## Configure the receiving screen first For example, a lower touchscreen runs your game while the upper display runs Advertising. 1. Assign your external WebApp to its screen and an Advertising playlist to the receiving screen. 2. In **Monitors**, select the **Advertising screen**, then its **Advertising** tab. 3. Open **Playback → Edit playback → WebApp cards**. 4. Under **Allow a WebApp to show cards**, select the assigned source WebApp. Default is **Off**. 5. Select the **Allowed cards** keys and save. 6. In the receiving screen's **Cards** view, configure the fleet default or screen override media for a custom key such as `jackpot`. Wait for the kiosk to download/apply that content before testing. The source WebApp must run on another monitor. Its audience/origin approval and the target's card permission are separate requirements. Standard keys are `loading`, `webapp_error`, `offline`, `needs_service`, and `paused`; they have built-in fallback visuals. Custom keys need configured media. Keys must match `[a-z][a-z0-9_]{1,31}`: 2–32 characters, starting with a lowercase letter, then lowercase letters, digits, or underscores. ## Show and clear a card: showCard / clearCard ```js const bridge = await getKioskBridge(); // Helper from Start here. const result = await bridge.showCard({ key: "jackpot", seconds: 60 }); // result: { screens: [""], seconds: 60, key: "jackpot" } // When this WebApp's card is no longer needed: await bridge.clearCard(); ``` `seconds` defaults to 300 and must be an integer from **1 to 300**. Zero, fractions, larger durations, and extra option fields are rejected. `key` is required. The page cannot pass a target monitor, URL, file, HTML, or image. Native code derives the source and targets from the live browser session and operator configuration. These are local bridge calls, not cloud requests. `clearCard()` releases only this runtime's own card lease. It does not clear an operator's portal override or another runtime's lease. Success returns `{ screens: [...], seconds: 0, key: null }`; an empty screen list can mean there was no owned lease left to clear. ## Pause and resume the playlist: pauseAds / resumeAds ```js await bridge.pauseAds(); // "paused" card for 300 seconds await bridge.pauseAds({ seconds: 120 }); // Explicit bounded duration await bridge.resumeAds(); // Clear this runtime's lease ``` `pauseAds()` is the `paused` card; the receiving screen must allow that key. `resumeAds()` and `clearCard()` clear the same owned lease. There is one active WebApp card lease per target, not a stack of independent pause and card requests. ## Keep a card up during a longer activity Renew before expiry while the activity is still running. This example uses one serialized renewal loop and a single cleanup path: ```js async function holdCardWhile(key, runActivity, onCardError) { const bridge = await getKioskBridge(); let active = true; let timer; let pending = Promise.resolve(); const renew = () => { pending = bridge.showCard({ key, seconds: 300 }) .then(() => { if (active) timer = setTimeout(renew, 60_000); }) .catch(error => { active = false; // Stop renewing; the bounded lease will expire. onCardError(error); }); }; // Fail before starting the activity if the initial request is denied. await bridge.showCard({ key, seconds: 300 }); timer = setTimeout(renew, 60_000); try { return await runActivity(); } finally { active = false; clearTimeout(timer); await pending; // Finish any renewal before clearing. try { await bridge.clearCard(); } catch (error) { onCardError(error); // Do not silently claim that playback resumed. } } } ``` Use one owner/controller in your app so two activities do not clear each other's lease. Handle cancellation by ending `runActivity` and running cleanup. Do not use a rapid retry loop when native access is denied. ## Expiry, priority, and confirmation - The lease uses the kiosk's monotonic clock and expires without renewal. - It does not survive an agent restart or reboot. Configuration, assignment, or runtime revocation can remove it sooner. - Same-origin navigation can retain the browser runtime; stopping renewal leaves the old card until clear or bounded expiry. Do not depend on an unload handler completing an async clear. - An operator's portal card has priority over a WebApp card. Clearing your lease may therefore leave the operator's card visible. - Offline bridge access remains subject to the source WebApp's valid business lease. It does not grant indefinite offline authority. - A resolved Promise confirms that native control accepted the request. It is not proof of physical pixels. Use the target's reported card state or an explicit screenshot when checking actual display. - Card transitions appear as `kiosk:card_shown` and `kiosk:card_cleared` in **Observations → Events**, with the screen, key, and source. General show/clear controls belong to Advertising. The system's own WebApp error/recovery pages are separate and do not grant arbitrary card control over external WebApp screens. --- # Bridge reference & troubleshooting The managed browser exposes an immutable `window.mojoware` object to top-level documents. No interface is provided inside an iframe. Successful readiness and every privileged call require a valid native context. [Start here](/docs/kiosk/home/) · [Complete Markdown](/docs/kiosk/llms-full.txt) ## Public interface | Member | Input | Result | | --- | --- | --- | | `ready` | Await the Promise; do not call it | Resolves when identity is ready; rejects when unavailable | | `device_id` | Read after readiness | Public device ID string | | `sn` | Read after readiness | Alias of `device_id` | | `getAuthKey(audience?)` | Omit audience to use the assigned app; explicit nonempty string must match | Promise of stable `mwk1` bearer string | | `getAuthProof({nonce, audience})` | Nonce 16–256 characters; matching audience up to 128 characters | Promise of `{payload, signature}` | | `showCard({key, seconds?})` | Allowed key; integer duration 1–300, default 300 | Promise of `{screens, seconds, key}` | | `clearCard()` | None | Promise of `{screens, seconds: 0, key: null}` | | `pauseAds({seconds?})` | Integer duration 1–300, default 300 | Promise of `{screens, seconds, key: "paused"}` | | `resumeAds()` | None | Same owned-lease clear as `clearCard()` | `getAuthKey` is async. There is no API to change the assigned audience or select another monitor. The page never receives a management token, private signing key, or general command interface. ## Troubleshooting | Symptom | Check | | --- | --- | | `window.mojoware` is undefined | Ordinary browser, unsupported installation, or iframe. Test in the assigned top-level kiosk WebApp. | | `ready` rejects / “Native bridge unavailable” | Current managed browser/native host, approved origin, assigned app, business eligibility, and live runtime. The bridge intentionally hides detailed denial reasons. | | “Invalid audience” | Use `getAuthKey()`, or supply a nonempty string matching the app's configured audience. Do not guess `wmwx` for an unrelated app. | | “Invalid proof request” | Pass an object containing a 16–256-character nonce and a valid configured audience. Your server must issue the nonce. | | “Invalid card request” / “Invalid advertising pause” | Check key syntax and integer seconds 1–300; omit unknown option fields. | | “Advertising control unavailable” | Check the receiving Advertising screen's selected source WebApp and allowed keys, both live monitor contexts, and business authorization. Rejection or the roughly five-second timeout does not identify one unique cause. | | Card accepted but not visible | Check operator override priority, downloaded media, target card status, and a fresh capture. Acceptance alone is not physical playback confirmation. | | Clear accepted with `screens: []` | No lease owned by this runtime remained to clear; inspect target state rather than assuming an operator override was removed. | | Verifier returns HTTP 200 but login must fail | Require `data.ok === true`. The response envelope can have `status: true` while the credential is invalid. | | Auth works but cards fail | App authentication and Advertising permission are separate. Enable WebApp cards on the receiving screen. | ## Integration checklist - Test the app in its real assigned top-level HTTPS context. - Await readiness and use `getAuthKey()` unless an explicit audience is coordinated. - Verify credentials server-side and check authorized product/device association. - Keep product bearers and verifier API keys out of browser logs and URLs. - Configure the receiving Advertising screen, source app, allowed keys, and media. - Exercise show, renewal, explicit clear, expiry, and operator override priority. - Surface failures to the operator without a request storm. - Confirm visible behavior using monitor status or a capture when it matters. ## Source and version Reviewed 16 October 2026 against server main `78bd60203b68cb00da448111fd8417602db8e6c3`; the bridge/API implementation is unchanged from the previously deployed kiosk work. This is documentation verification, not a new physical-device test. Maintainer sources: - `docs/web_developer/kiosk_api.md`: device identity bridge, local advertising pause, and screen cards. - `apps/mojoware/kiosk/pi/bridge/page.js`: public methods, input validation, and timeouts. - `apps/mojoware/kiosk/pi/agent/agent/bridge.py`: native origin, audience, assignment, and lease checks. - `apps/mojoware/kiosk/pi/agent/agent/advertising.py`: target selection, lease ownership, return values, and card priority. - `apps/mojoware/kiosk/rest/identity.py` and `utils/identity.py`: server verification contract. - Portal `AdvertisingPanel.tsx`: current operator setup labels. The task for the original authentication/identity bridge is [Maestro #7015](https://maestromojo.com/app/#/board/21?item=7015); task access requires workspace membership.