Identity verification
Tell Relay who your signed-in user is by signing a short-lived token with your Relay Secret.
Chat needs no identity. The helpdesk (tickets), feature requests (voting, commenting) and per-user data do: Relay has to know that the person in the widget really is customer 123 of your product before it shows them their tickets. Rather than a login inside the widget, your own server — which already knows who is signed in — signs a short-lived identity token with a Relay Secret, and Relay verifies it.
How it works
- Relay issues a Relay Secret per product (
rlys_…). You store it on your server asRELAY_SECRET. It never goes to the browser. - On each page load, your server mints a JWT for the signed-in user, signed
with
RELAY_SECRET(HS256). - Your page exposes it as
window.RelayEmbed.identitybefore the Relay script loads. - The widget (or embed) sends it to Relay, Relay verifies the signature and claims, resolves the user to a contact, and issues a session.
Tokens are short-lived (5 minutes is plenty; 24 hours is the maximum), so mint one fresh on every page load. The widget exchanges it for a longer-lived Relay session immediately.
Get your Relay Secret
Open Settings → Products → your product, and in the Identity section click Generate next to Relay Secret. Copy it into your server environment:
RELAY_SECRET=rlys_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThe Relay Secret is server-only. Never expose it to the browser, commit it, or send it to Relay. Only the signed token goes on the page.
The token
A standard HS256 JWT signed with your Relay Secret:
// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{
"aud": "superrelay",
"sub": "customer-123",
"email": "[email protected]",
"name": "Ada Lovelace",
"organization": { "externalId": "acct_456", "name": "Acme" },
"iat": 1781190000,
"exp": 1781190300
}| Claim | Required | Meaning |
|---|---|---|
aud | yes | Exactly "superrelay". |
sub | yes | The user's stable id in your system. This is what Relay keys the contact on — never reuse it for another person. |
email | no | Lets Relay attach the identity to a contact you already have with that email, and shows the email to agents. |
name | no | Display name; fills in a contact that has none. |
organization | no | { externalId, name? } — the account/company/store the user belongs to. Relay resolves it to a company (creating one on first sight, keyed by externalId) and links the contact. Everyone with the same organization can share the same helpdesk, if you turn that on. |
iat | yes | Issued-at, unix seconds. |
exp | yes | Expiry, unix seconds. At most 24 h after iat. |
iss | no | Free-form issuer string; stored, not interpreted. |
Put it on the page before the Relay script:
<script>
window.RelayEmbed = { identity: { token: "eyJhbGciOiJIUzI1NiIs…" } }
</script>
<script src="https://app.superrelay.ai/widget/widget.js" data-relay-token="YOUR_WIDGET_TOKEN" defer></script>Node.js / Express
import jwt from "jsonwebtoken"
app.get("/app", (req, res) => {
const user = req.user // your session
const now = Math.floor(Date.now() / 1000)
const token = jwt.sign(
{
aud: "superrelay",
sub: user.id,
email: user.email,
name: user.name,
organization: { externalId: user.accountId, name: user.accountName },
iat: now,
exp: now + 300,
},
process.env.RELAY_SECRET!,
{ algorithm: "HS256" }
)
res.render("app", { relayIdentity: token })
})<script>
window.RelayEmbed = { identity: { token: "<%= relayIdentity %>" } }
</script>Next.js (App Router)
// app/layout.tsx
import { SignJWT } from "jose"
import { getSession } from "@/lib/auth"
async function relayIdentity() {
const user = await getSession()
if (!user) return null
return new SignJWT({
aud: "superrelay",
sub: user.id,
email: user.email,
name: user.name,
organization: { externalId: user.orgId, name: user.orgName },
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("5m")
.sign(new TextEncoder().encode(process.env.RELAY_SECRET!))
}
export default async function RootLayout({ children }) {
const token = await relayIdentity()
return (
<html>
<body>
{token ? (
<script
dangerouslySetInnerHTML={{
__html: `window.RelayEmbed=${JSON.stringify({ identity: { token } })}`,
}}
/>
) : null}
{children}
</body>
</html>
)
}PHP
<?php
use Firebase\JWT\JWT;
$now = time();
$token = JWT::encode([
'aud' => 'superrelay',
'sub' => $user->id,
'email' => $user->email,
'name' => $user->name,
'organization' => ['externalId' => $user->account_id, 'name' => $user->account_name],
'iat' => $now,
'exp' => $now + 300,
], getenv('RELAY_SECRET'), 'HS256');
?>
<script>
window.RelayEmbed = { identity: { token: <?php echo json_encode($token); ?> } };
</script>Any language
Any JWT library that does HS256 works. Without one: base64url-encode the header
and payload JSON, join them with ., HMAC-SHA256 that string with
RELAY_SECRET, base64url-encode the digest, and append it with another ..
What the identity unlocks
- Widget helpdesk — the customer sees and raises tickets. Without a verified identity the helpdesk tab signs in with an error; nothing is shown.
- Embed (Embed) — the helpdesk surface, and creating, voting and commenting on feature requests.
- Contact resolution — Relay maps
subto a contact once and keeps using it;emailonly helps the first time. Agents see the verified identity on the contact. - Organizations — with
organization, the contact is linked to a company. In Settings → Products → your product → Identity, Share tickets across an organization decides whether that company's people see each other's tickets (on by default) or only their own.
Rotating the secret
Settings → Products → your product → Identity → Rotate. Rotation
invalidates the previous secret immediately; update RELAY_SECRET on your
server first, then rotate.
Troubleshooting
Signature verification failed— the token was signed with a different secret, or with an algorithm other than HS256. CheckRELAY_SECRET.aud must be "superrelay"/sub is required— the payload is missing a required claim.Identity token has expired/Token lifetime exceeds 24 hours—iat/expare in seconds, andexp - iatmust be ≤ 86400. Mint on each page load.Relay Secret is not configured— generate one in the product's Identity section first.- Helpdesk tab says identity is missing —
window.RelayEmbed.identitywas set after the widget loaded, or is malformed. Set it before the script tag.
Shopify apps
Shopify apps use the same Relay Secret. The signed-shop format
({ shop, ts, signature }, an HMAC of v1:{shop}:{ts}) that existing apps
ship keeps working unchanged; a token as above with the shop as organization
gives you per-staff identities on top. See
Shopify identity verification.