Appearance
Embedded pages
An embedded app page gives your app a UI surface inside the host panel — a full tab with your web app rendered in a sandboxed iframe. Use it for dashboards, configuration screens, or any interactive UI that goes beyond what settings and metafields alone can provide.
Declare an app page
Add app_page.url to your manifest:
jsonc
"app_page": {
"url": "https://app.example.com/stayblox"
}When the app is installed, Stayblox adds a {App Name} entry to the host's panel navigation. Clicking it renders your URL in a sandboxed iframe, with a signed session token appended as a query parameter:
https://app.example.com/stayblox?session=eyJhbGciOiJIUzI1NiJ9...The session token
The session query parameter is a HS256-signed JWT. Verify it on your server to confirm the request comes from Stayblox and to identify the installing host.
Token structure
json
{
"iss": "stayblox",
"aud": "your_client_id",
"team_id": 42,
"user_id": 7,
"install_id": 19,
"iat": 1749812734,
"exp": 1749813034
}| Claim | Description |
|---|---|
iss | Always "stayblox". |
aud | Your app's client_id. Verify this matches your own credential. |
team_id | The host's team ID. Use this to load team-specific data from your own store, or scope API calls. |
user_id | The panel user who opened the app page. |
install_id | The install's ID. Useful for associating your own session with the Stayblox install record. |
iat | Issued-at unix timestamp. |
exp | Expiry unix timestamp. 5 minutes from issuance. |
Signing key
The token is signed with your app's client_secret using HS256. Verify it with the same key.
Verification examples
Node.js (using jsonwebtoken)
js
import jwt from 'jsonwebtoken'
function verifySession(token) {
// Throws if the token is invalid, expired, or signed with the wrong key.
const payload = jwt.verify(token, process.env.STAYBLOX_CLIENT_SECRET, {
algorithms: ['HS256'],
issuer: 'stayblox',
audience: process.env.STAYBLOX_CLIENT_ID,
})
return payload // { team_id, user_id, install_id, ... }
}PHP (using firebase/php-jwt)
php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
function verifySession(string $token): object
{
// Throws on invalid signature, expiry, wrong issuer/audience.
return JWT::decode(
$token,
new Key($_ENV['STAYBLOX_CLIENT_SECRET'], 'HS256'),
);
// Verify aud separately if needed:
// assert($payload->aud === $_ENV['STAYBLOX_CLIENT_ID']);
}Always:
- Verify the signature before trusting any claim.
- Check
expto reject expired tokens (most JWT libraries do this automatically). - Check
audmatches your ownclient_id.
Token refresh
The session token has a 5-minute TTL. For long-lived app pages (dashboards the host keeps open), use the postMessage token-refresh bridge to get a fresh token without a full page reload.
How it works
Your iframe Stayblox parent frame
│ │
│── postMessage({type:'stayblox:refresh-token'}) ──▶│
│ │ mints a new JWT
│◀── postMessage({type:'stayblox:token', token: '...'}) ──│
│ │Your page posts a message to the parent; Stayblox mints a fresh token and replies to your declared origin only.
Client-side implementation
js
// Request a new token from the Stayblox parent frame.
function refreshSession() {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Token refresh timed out')), 5000)
function handler(event) {
// Only accept messages from the Stayblox app domain.
if (event.origin !== 'https://app.stayblox.com') return
if (event.data?.type !== 'stayblox:token') return
clearTimeout(timeout)
window.removeEventListener('message', handler)
resolve(event.data.token)
}
window.addEventListener('message', handler)
window.parent.postMessage({ type: 'stayblox:refresh-token' }, 'https://app.stayblox.com')
})
}
// Example: refresh before an API call when you know the token is near expiry.
async function callMyApi(endpoint) {
const token = await refreshSession()
return fetch(endpoint, {
headers: { 'X-Stayblox-Session': token }
})
}Proactive refresh
Rather than waiting for a 401 from your backend, decode the exp claim from the initial token on page load and schedule a refresh 30–60 seconds before expiry with setTimeout. This avoids any window where the host triggers an action with an expired session.
Sandbox and security
The iframe is rendered with sandbox attributes that restrict what it can do:
html
<iframe
src="https://app.example.com/stayblox?session=..."
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe>In addition, the platform's Content Security Policy pins frame-src to your declared app_page.url origin, so the iframe URL cannot be substituted.
Design your app page to work within these constraints:
- JavaScript execution is allowed (
allow-scripts). - Popups for OAuth sub-flows are allowed (
allow-popups). - Form submission is allowed (
allow-forms). - Top-level navigation from within the iframe is not allowed. If you need to send the user somewhere else, open it in a new tab with
target="_blank".
Loading and error states
Your page is responsible for its own loading and error UI. The iframe is rendered at full panel width; Stayblox doesn't inject any loading overlay.
A good first-render pattern:
- Parse
?session=...from the URL on load. - Verify the JWT on your server (exchange it for your own session token if needed).
- Render a spinner while the exchange is in flight.
- On verification failure (bad signature, expired token), show a clear error and a "Reload" button that re-opens the panel page (which mints a fresh JWT).
Checklist
- [ ]
app_page.urlis anhttps://URL in the manifest. - [ ] Server verifies the
sessionJWT signature andexpbefore acting on it. - [ ] Server checks
audmatches your ownclient_id. - [ ] Client-side implements token refresh via the postMessage bridge for long-lived pages.
- [ ] UI degrades gracefully when the token is missing or expired.