Developer Documentation

Embed the Handbook Dashboard

imageWhite-label the dashboard inside your product
imageJWT-based authentication for seamless login
imageYour users manage handbooks without leaving your app
Prerequisites

Before You Begin

To embed the Handbooks.io dashboard, you'll need the following:

  • A Reseller account on Handbooks.io with an active reseller_id and shared_secret contact us to get set up
  • A backend server capable of generating HS256 JWT tokens
  • A frontend that can render an iframe
Step 1

Generate a JWT Token

On 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.

JWT Payload Fields

FieldTypeDescription
user_idrequiredstringA unique identifier for this user in your system. Used to map the user across sessions.
emailrequiredstringThe user's email address. Used for display and synced on each login.
namerequiredstringThe user's display name.
reseller_idrequiredstringYour reseller ID provided by Handbooks.io.
workspace_idrequiredstringA unique identifier for the workspace/organization in your system. Users with the same workspace_id share a workspace.
workspace_namerequiredstringDisplay name for the workspace. Synced on each login.
rolestringUser role in the workspace. Defaults to admin. Use member to restrict access.
exprequirednumberExpiration timestamp (Unix seconds). Recommended: 5–10 minutes from now.
javascript
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',
  });
}
python
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")
php
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');
}
Step 2

Create a Server Endpoint

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.

javascript
// 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.

Step 3

Embed the iframe

Fetch the widget URL from your server and set it as the iframe src.

jsx
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"
    />
  );
}
html
<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>
Step 4

Handle Iframe Events

The embedded dashboard communicates with your parent page via postMessage. Listen for these events to keep your app in sync.

Available Events

Event TypeDescription
handbooks:logoutThe user's session has expired or is invalid. Re-generate the widget URL and reload the iframe, or show a “session expired” message.
javascript
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.

Complete Example

Full Working Integration

Here's a complete example with widget URL generation, iframe rendering, and event handling.

javascript
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}` });
});
html
<!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>
Customization

Workspaces & Multi-Tenancy

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.

User Roles

RoleCapabilities
adminFull access: create/edit handbooks, manage members, workspace settings.
memberView handbooks, sign documents. Cannot invite members or change settings.

Deep Linking

Use the returnUrl query parameter on the widget URL to land users on a specific page:

javascript
// 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`;

Available Pages

PathDescription
/widget/Handbook list (default landing page)
/widget/edit/{handbookId}Handbook editor
/widget/teamTeam members
/widget/workspace/settingsWorkspace settings
/widget/accountUser profile
/widget/handbook/{handbookId}/signatureSignature tracking
/widget/handbook/{handbookId}/pdfPDF download
/widget/handbook-updatesHandbook update history

Replace {handbookId} with the numeric handbook ID.

Troubleshooting

Frequently Asked Questions

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.

Contact Us

Still have questions?

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