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.

Required env vars on every server: 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.

bash
npm install @onpilot/node
ts
import { 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.

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

bash
composer require firebase/php-jwt
php
use 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:

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

bash
pip install PyJWT
python
import 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.

ruby
# Gemfile
gem "jwt"
ruby
# 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
end

Go

Go

github.com/golang-jwt/jwt v5 is the maintained fork. MapClaims keeps the snippet short.

bash
go get github.com/golang-jwt/jwt/v5
go
import (
    "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.