Server recipes
Sign the JWT in any language.
HS256 with your tenant Embed Secret. The four required claims — iss, sub, copilot_id, exp — are the same regardless of language. Pick a JWT library and go.
ONPILOT_TENANT_ID, ONPILOT_EMBED_SECRET, ONPILOT_COPILOT_ID. The embed secret stays on your server — never ship it to the browser.Node
Node.js — @onpilot/node
The tiny helper writes the four required claims and HS256-signs locally. Works in Express, Next.js, Hono, anywhere.
npm install @onpilot/nodeimport { OnPilot } from "@onpilot/node";
const onpilot = new OnPilot({
tenantId: process.env.ONPILOT_TENANT_ID!,
embedSecret: process.env.ONPILOT_EMBED_SECRET!,
});
const identityToken = onpilot.signIdentityToken({
copilotId: process.env.ONPILOT_COPILOT_ID!,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.isAdmin ? "admin" : "user",
},
expiresIn: "1h",
});Node — raw
Node.js — any JWT library
If you don't want our helper, sign with jsonwebtoken (or any HS256 library) directly. Same claims.
import jwt from "jsonwebtoken";
const identityToken = jwt.sign(
{
iss: process.env.ONPILOT_TENANT_ID,
sub: user.id,
copilot_id: process.env.ONPILOT_COPILOT_ID,
role: user.isAdmin ? "admin" : "user",
name: user.name,
email: user.email,
},
process.env.ONPILOT_EMBED_SECRET!,
{ algorithm: "HS256", expiresIn: "1h" },
);PHP
PHP — Laravel, Symfony, WordPress
Use firebase/php-jwt — the de-facto JWT library for PHP. Works with any framework or plain PHP.
composer require firebase/php-jwtuse Firebase\JWT\JWT;
$now = time();
$payload = [
'iss' => $_ENV['ONPILOT_TENANT_ID'],
'sub' => (string) $user->getId(),
'copilot_id' => $_ENV['ONPILOT_COPILOT_ID'],
'role' => $user->isAdmin() ? 'admin' : 'user',
'name' => $user->getDisplayName(),
'email' => $user->getEmail(),
'iat' => $now,
'exp' => $now + 3600, // 1 hour
];
$identityToken = JWT::encode(
$payload,
$_ENV['ONPILOT_EMBED_SECRET'],
'HS256'
);Render into your template:
<script
src="https://chat.onpilot.ai/embed.js"
data-identity-token="<?= htmlspecialchars($identityToken, ENT_QUOTES) ?>"
></script>Python
Python — Django, Flask, FastAPI
Use PyJWT — same claim shape across frameworks. Wrap in a helper your route handlers call.
pip install PyJWTimport jwt, time, os
def sign_identity_token(user, copilot_id: str) -> str:
now = int(time.time())
payload = {
"iss": os.environ["ONPILOT_TENANT_ID"],
"sub": str(user.id),
"copilot_id": copilot_id,
"role": "admin" if user.is_staff else "user",
"name": user.get_full_name(),
"email": user.email,
"iat": now,
"exp": now + 3600,
}
return jwt.encode(
payload,
os.environ["ONPILOT_EMBED_SECRET"],
algorithm="HS256",
)Ruby
Ruby on Rails
The jwt gem ships HS256 out of the box. Wrap signing in a service object.
# Gemfile
gem "jwt"# app/services/onpilot_token.rb
class OnpilotToken
def self.for(user, copilot_id)
now = Time.now.to_i
payload = {
iss: ENV.fetch("ONPILOT_TENANT_ID"),
sub: user.id.to_s,
copilot_id: copilot_id,
role: user.admin? ? "admin" : "user",
name: user.name,
email: user.email,
iat: now,
exp: now + 3600,
}
JWT.encode(payload, ENV.fetch("ONPILOT_EMBED_SECRET"), "HS256")
end
endGo
Go
github.com/golang-jwt/jwt v5 is the maintained fork. MapClaims keeps the snippet short.
go get github.com/golang-jwt/jwt/v5import (
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
func signIdentityToken(userID, copilotID, name, email, role string) (string, error) {
now := time.Now()
claims := jwt.MapClaims{
"iss": os.Getenv("ONPILOT_TENANT_ID"),
"sub": userID,
"copilot_id": copilotID,
"role": role,
"name": name,
"email": email,
"iat": now.Unix(),
"exp": now.Add(time.Hour).Unix(),
}
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return tok.SignedString([]byte(os.Getenv("ONPILOT_EMBED_SECRET")))
}Static / Jamstack
No server? Use a serverless function
If your frontend is fully static, you cannot safely sign tokens — the embed secret can't be shipped to the browser. The minimum viable integration is one serverless function (Cloudflare Worker, Vercel Edge Function, AWS Lambda) that reads ONPILOT_EMBED_SECRET and returns a signed JWT to your page.