To embed the Handbooks.io dashboard, you'll need the following:
reseller_id and shared_secret — contact us to get set upOn your server, create a signed JWT containing the user and workspace information. The token must be signed with your shared_secret using the HS256 algorithm.
| Field | Type | Description |
|---|---|---|
user_idrequired | string | A unique identifier for this user in your system. Used to map the user across sessions. |
emailrequired | string | The user's email address. Used for display and synced on each login. |
namerequired | string | The user's display name. |
reseller_idrequired | string | Your reseller ID provided by Handbooks.io. |
workspace_idrequired | string | A unique identifier for the workspace/organization in your system. Users with the same workspace_id share a workspace. |
workspace_namerequired | string | Display name for the workspace. Synced on each login. |
role | string | User role in the workspace. Defaults to admin. Use member to restrict access. |
exprequired | number | Expiration timestamp (Unix seconds). Recommended: 5–10 minutes from now. |
const jwt = require('jsonwebtoken');
function generateEmbedToken(user, workspace) {
const payload = {
user_id: user.id,
email: user.email,
name: user.name,
reseller_id: process.env.HANDBOOKS_RESELLER_ID,
workspace_id: workspace.id,
workspace_name: workspace.name,
role: 'admin',
};
return jwt.sign(payload, process.env.HANDBOOKS_SHARED_SECRET, {
algorithm: 'HS256',
expiresIn: '10m',
});
}import jwt, time, os
def generate_embed_token(user, workspace):
payload = {
"user_id": user["id"],
"email": user["email"],
"name": user["name"],
"reseller_id": os.environ["HANDBOOKS_RESELLER_ID"],
"workspace_id": workspace["id"],
"workspace_name": workspace["name"],
"role": "admin",
"exp": int(time.time()) + 600, # 10 minutes
}
return jwt.encode(payload, os.environ["HANDBOOKS_SHARED_SECRET"], algorithm="HS256")use Firebase\JWT\JWT;
function generateEmbedToken($user, $workspace) {
$payload = [
'user_id' => $user['id'],
'email' => $user['email'],
'name' => $user['name'],
'reseller_id' => getenv('HANDBOOKS_RESELLER_ID'),
'workspace_id' => $workspace['id'],
'workspace_name' => $workspace['name'],
'role' => 'admin',
'exp' => time() + 600,
];
return JWT::encode($payload, getenv('HANDBOOKS_SHARED_SECRET'), 'HS256');
}Create an API endpoint that your frontend calls to get a signed widget URL. This keeps your shared_secret on the server. Each URL is a single-use login credential: fetch a new URL immediately before assigning the iframe src, including after a failed load or session-expiry retry.
// Express.js example
app.get('/api/handbooks/widget', authenticate, (req, res) => {
const token = generateEmbedToken(req.user, req.user.workspace);
const widgetUrl = `https://www.handbooks.io/api/sso/${token}`;
res.json({ url: widgetUrl });
});Security: Never expose your shared_secret to the client. Always generate the JWT on your server. Tokens may be valid for at most ten minutes and are consumed atomically on first use, so do not cache or reuse a previously returned widget URL.
Fetch the widget URL from your server and set it as the iframe src.
import { useState, useEffect } from 'react';
function HandbooksDashboard() {
const [widgetUrl, setSsoUrl] = useState(null);
useEffect(() => {
fetch('/api/handbooks/widget')
.then(res => res.json())
.then(data => setSsoUrl(data.url));
}, []);
if (!widgetUrl) return <div>Loading...</div>;
return (
<iframe
src={widgetUrl}
style={{ width: '100%', height: '100vh', border: 'none' }}
allow="clipboard-write"
/>
);
}<iframe
id="handbooks-embed"
style="width: 100%; height: 100vh; border: none;"
allow="clipboard-write"
></iframe>
<script>
fetch('/api/handbooks/widget')
.then(res => res.json())
.then(data => {
document.getElementById('handbooks-embed').src = data.url;
});
</script>The embedded dashboard communicates with your parent page via postMessage. Listen for these events to keep your app in sync.
| Event Type | Description |
|---|---|
handbooks:logout | The user's session has expired or is invalid. Re-generate the widget URL and reload the iframe, or show a “session expired” message. |
window.addEventListener('message', (event) => {
// Verify origin for security
if (event.origin !== 'https://www.handbooks.io') return;
if (event.data?.type === 'handbooks:logout') {
// Session expired — re-authenticate and reload iframe
console.log('Handbooks session expired, re-authenticating...');
refreshHandbooksEmbed();
}
});Tip: Always verify event.origin matches your Handbooks.io domain to prevent cross-origin attacks.
Here's a complete example with widget URL generation, iframe rendering, and event handling.
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const RESELLER_ID = process.env.HANDBOOKS_RESELLER_ID;
const SHARED_SECRET = process.env.HANDBOOKS_SHARED_SECRET;
const HANDBOOKS_URL = 'https://www.handbooks.io';
app.get('/api/handbooks/widget', authenticate, (req, res) => {
const token = jwt.sign(
{
user_id: req.user.id,
email: req.user.email,
name: req.user.name,
reseller_id: RESELLER_ID,
workspace_id: req.user.organizationId,
workspace_name: req.user.organizationName,
role: 'admin',
},
SHARED_SECRET,
{ algorithm: 'HS256', expiresIn: '10m' }
);
res.json({ url: `${HANDBOOKS_URL}/api/sso/${token}` });
});<!DOCTYPE html>
<html>
<head>
<title>Employee Handbook</title>
<style>
body { margin: 0; font-family: sans-serif; }
#handbooks-embed { width: 100%; height: 100vh; border: none; }
#loading { display: flex; align-items: center;
justify-content: center; height: 100vh; color: #666; }
</style>
</head>
<body>
<div id="loading">Loading handbook dashboard...</div>
<iframe id="handbooks-embed" style="display:none"
allow="clipboard-write"></iframe>
<script>
const iframe = document.getElementById('handbooks-embed');
const loading = document.getElementById('loading');
async function loadHandbooks() {
const res = await fetch('/api/handbooks/widget');
const { url } = await res.json();
iframe.src = url;
iframe.onload = () => {
loading.style.display = 'none';
iframe.style.display = 'block';
};
}
window.addEventListener('message', (event) => {
if (event.origin !== 'https://www.handbooks.io') return;
if (event.data?.type === 'handbooks:logout') {
loading.style.display = 'flex';
iframe.style.display = 'none';
loadHandbooks(); // Re-authenticate
}
});
loadHandbooks();
</script>
</body>
</html>Each unique workspace_id in your JWT creates a separate workspace with its own handbooks and team members. Users who share the same workspace_id will see the same handbooks and can collaborate.
| Role | Capabilities |
|---|---|
admin | Full access: create/edit handbooks, manage members, workspace settings. |
member | View handbooks, sign documents. Cannot invite members or change settings. |
Use the returnUrl query parameter on the widget URL to land users on a specific page:
// Open directly to the handbook editor
const widgetUrl = `https://www.handbooks.io/api/sso/${token}?returnUrl=/widget/edit/${handbookId}`;
// Open to team members page
const widgetUrl = `https://www.handbooks.io/api/sso/${token}?returnUrl=/widget/team`;| Path | Description |
|---|---|
/widget/ | Handbook list (default landing page) |
/widget/edit/{handbookId} | Handbook editor |
/widget/team | Team members |
/widget/workspace/settings | Workspace settings |
/widget/account | User profile |
/widget/handbook/{handbookId}/signature | Signature tracking |
/widget/handbook/{handbookId}/pdf | PDF download |
/widget/handbook-updates | Handbook update history |
Replace {handbookId} with the numeric handbook ID.
Common issues and how to resolve them.
Ensure your token is valid and not expired. Check the browser console for CORS or X-Frame-Options errors. The widget endpoint removes X-Frame-Options automatically.
Third-party cookies must be allowed. Our widget endpoint sets cookies with SameSite=None and Secure flags. Ensure your page is served over HTTPS.
Verify your reseller_id is correct and your reseller account is active. Contact support if the issue persists.
Check that your server clock is synchronized (NTP), the shared_secret matches, and the token has not expired or already been used. Tokens must be short-lived (no more than 10 minutes). Request a fresh widget URL for every iframe load or retry.
Each unique reseller_id + workspace_id combination maps to one workspace. Ensure you are passing the correct workspace_id for the logged-in user.

Can’t find the answer you’re looking for? Please chat with our friendly team.