Identity Verification
Prove which shop is using your app by signing a short-lived identity with your Relay Secret.
Relay needs to know which Shopify shop is loading the embed or widget. Instead of handing Relay your Shopify app secret, your own server signs each shop's identity with a Relay Secret that Relay issues to you. Relay verifies the signature with the same secret. Your Shopify client secret never leaves your infrastructure.
How it works
- Relay generates a per-app Relay Secret (
rlys_…). You store it on your server asRELAY_SECRET. - On each page load, your server — which already knows the authenticated shop — computes an HMAC signature over the shop domain and a timestamp.
- Your page exposes that signature to the embed/widget as
window.RelayEmbed.identity. - Relay verifies the signature and issues a short-lived session.
The signature is only valid for 5 minutes, so sign it fresh on every page load. The embed exchanges it for a longer-lived Relay session immediately on mount.
Get your Relay Secret
In the Relay admin, open Settings → Apps → your app → Relay Secret and click Generate Relay Secret. Copy the value 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 derived signature is safe to put on the page.
The signature
The canonical message is:
v1:{shop}:{ts}shop— the*.myshopify.comdomain (e.g.acme.myshopify.com)ts— current Unix time in seconds
signature = hex( HMAC_SHA256(RELAY_SECRET, "v1:{shop}:{ts}") )Expose the result as a global the embed reads:
<script>
window.RelayEmbed = {
identity: { shop: "acme.myshopify.com", ts: 1781190000, signature: "a1b2c3…" },
}
</script>React Router / Remix (Shopify app template)
If you use the Shopify React Router app template,
sign the identity right in your app route loader — no extra API endpoint
needed. The loader already authenticates the shop, so session.shop is verified.
// app/routes/app.tsx (your authenticated app layout route)
import { createHmac } from "node:crypto"
import type { LoaderFunctionArgs } from "react-router"
import { authenticate } from "../shopify.server"
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { session } = await authenticate.admin(request)
const ts = Math.floor(Date.now() / 1000)
const signature = createHmac("sha256", process.env.RELAY_SECRET!)
.update(`v1:${session.shop}:${ts}`)
.digest("hex")
return {
relay: {
embedToken: process.env.RELAY_EMBED_TOKEN, // public
identity: { shop: session.shop, ts, signature },
},
}
}Then render the identity into the page before embed.js runs:
import { useLoaderData } from "react-router"
export default function App() {
const { relay } = useLoaderData<typeof loader>()
return (
<>
<script
// biome-ignore lint: trusted, server-generated values
dangerouslySetInnerHTML={{
__html: `window.RelayEmbed = ${JSON.stringify({ identity: relay.identity })}`,
}}
/>
<script src="https://app.superrelay.ai/embed/embed.js" data-token={relay.embedToken} />
<div id="relay-helpdesk" />
<div id="relay-feature-requests" />
{/* …your app… */}
</>
)
}Because the loader runs on every navigation to the app, the signature is always fresh.
PHP
<?php
// $shop comes from your verified Shopify session.
$shop = $session->getShop(); // e.g. "acme.myshopify.com"
$ts = time();
$signature = hash_hmac('sha256', "v1:{$shop}:{$ts}", getenv('RELAY_SECRET'));
$identity = json_encode([
'shop' => $shop,
'ts' => $ts,
'signature' => $signature,
]);
?>
<script>
window.RelayEmbed = { identity: <?php echo $identity; ?> };
</script>
<script src="https://app.superrelay.ai/embed/embed.js" data-token="<?php echo getenv('RELAY_EMBED_TOKEN'); ?>"></script>
<div id="relay-helpdesk"></div>Any language
The algorithm is plain HMAC-SHA256 — implement it with your standard library:
| Step | Value |
|---|---|
| Message | v1:{shop}:{ts} |
| Key | your RELAY_SECRET |
| Hash | SHA-256 |
| Output | lowercase hex |
Set window.RelayEmbed.identity = { shop, ts, signature } before the Relay
script loads, and you're done.
Rotating the secret
You can rotate the Relay Secret any time from Settings → Apps. Rotation
invalidates the previous secret immediately, so update RELAY_SECRET on your
server before the next request.
Troubleshooting
Signature verification failed— the message or secret don't match. Check the exact formatv1:{shop}:{ts}and thatRELAY_SECRETmatches the admin.Signature timestamp is outside the allowed window—tsmust be within 5 minutes of now, in seconds (not milliseconds). Sign on each page load.Relay Secret is not configured— generate one in the Relay admin first.- Read-only embed —
window.RelayEmbed.identityis missing or malformed; the embed still shows public surfaces (like changelog) but can't authenticate.