Getting Started
This guide walks you through everything needed to get the Relay widget running in your platform for the first time - from receiving your credentials to a fully authenticated widget loading for a real user.
Prerequisites
Before you begin, you’ll need:
| Requirement | Details |
|---|---|
| Fruga credentials | The four values below, provided by the Fruga team for each environment |
| A backend | A server-side environment capable of signing JWTs and making outbound HTTPS calls to Fruga |
| A web frontend | A page where the widget will be embedded |
Fruga issues four values per environment. Add them to your server environment, along with the API base URL for the environment you are integrating against:
NEXT_PUBLIC_PARTNER_ID=pk_dev_123
PARTNER_SECRET=sk_dev_xxxxxxxxxxxxxxxxxxxxxxxx
PARTNER_KEY_ID=key_1
ISSUER=partner:1
FRUGA_API_URL=https://api-dev.fruga.co.uk
| Value | What it is | Frontend safe? |
|---|---|---|
NEXT_PUBLIC_PARTNER_ID | Your Partner Key. Passed to the widget, and used as ?partnerKey= when loading your configuration | Yes - it is a public identifier |
PARTNER_SECRET | The HMAC key used to sign assertion JWTs | No - server-side only |
PARTNER_KEY_ID | Goes in the JWT header as kid, so Fruga knows which secret to verify with | No |
ISSUER | Goes in the JWT as the iss claim | No |
FRUGA_API_URL is not a credential - it is the base URL of the Fruga API for the environment you are pointing at. Development is https://api-dev.fruga.co.uk; you will be given the production domain when you go live. Throughout these docs it appears as the placeholder {FRUGA_API_URL}.
PARTNER_SECRET secure - store it in your secrets manager and never expose it in client-side code or commit it to source control. Only NEXT_PUBLIC_PARTNER_ID is safe to ship to the browser.Overview of the integration
A Relay integration has two distinct parts that work together: a small amount of backend code that signs and exchanges tokens, and the widget loaded on your frontend. Here’s how they connect:
onTokenRequired callback, which posts the current user's userRef to an endpoint on your own backend.PARTNER_SECRET, carrying kid, iss and the audience Fruga advertises. It then posts that assertion to Fruga's token exchange endpoint itself.PARTNER_SECRET never reach the browser.Step 1 - Install the Relay SDK
The Fruga SDK is published on the public npm registry — no registry configuration or authentication needed. Install it using your preferred package manager:
pnpm add @fruga/sdk
Then add a container element on your page where the widget will mount:
<div id="fruga-relay"></div>
Step 2 - Create a backend endpoint that returns an access token
Before you can initialise the widget, you need a server endpoint that does three things for the currently authenticated user: read the audience from your partner configuration, sign an assertion JWT, and exchange that assertion for a Fruga access token. The widget calls this endpoint whenever it needs a token.
The following example uses Node.js with jose, but the same approach applies in any backend language that supports HMAC-signed JWTs.
import express from 'express';
import * as jose from 'jose';
const app = express();
app.use(express.json());
app.post('/fruga/token', async (req, res) => {
// Ensure the user is authenticated in your own system first
if (!req.user) return res.status(401).end();
const { userRef } = req.body;
if (!userRef) return res.status(400).json({ error: 'userRef is required' });
const apiUrl = process.env.FRUGA_API_URL;
const partnerKey = process.env.NEXT_PUBLIC_PARTNER_ID;
// 1. Read the audience Fruga expects for your partner environment
const bootstrap = await fetch(
`${apiUrl}/widget/bootstrap?partnerKey=${partnerKey}`
).then(r => r.json());
const { audience } = bootstrap.auth;
// 2. Sign the assertion — HMAC over the raw UTF-8 bytes of PARTNER_SECRET
const secret = new TextEncoder().encode(process.env.PARTNER_SECRET);
const assertion = await new jose.SignJWT({ userRef })
.setProtectedHeader({
alg: 'HS256',
typ: 'JWT',
kid: process.env.PARTNER_KEY_ID, // tells Fruga which secret to verify with
})
.setIssuedAt()
.setIssuer(process.env.ISSUER)
.setAudience(audience)
.setExpirationTime('16s') // keep this short — 120s is the maximum
.setJti(crypto.randomUUID()) // unique ID — prevents replay attacks
.sign(secret);
// 3. Exchange it for a Fruga access token
const tokenResponse = await fetch(`${apiUrl}/auth/external/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ assertion }),
});
if (!tokenResponse.ok) {
return res.status(tokenResponse.status).json({ error: 'Token exchange failed' });
}
res.json(await tokenResponse.json()); // { accessToken, ... }
});
PARTNER_KEY_ID is unset, most JWT libraries silently omit the kid header rather than erroring - and the exchange then fails with a 401 that looks like a bad secret. This is the most common setup mistake, so check kid first when the exchange rejects a token you believe is correct.Step 3 - Initialise the widget
With the package installed and your backend endpoint ready, initialise the widget on your frontend. Import init from the SDK’s loader and pass an onTokenRequired callback that fetches a token from your backend — the SDK calls it whenever it needs a fresh one, including on first load.
import { init } from '@fruga/sdk/loader';
await init({
partnerKey: process.env.NEXT_PUBLIC_PARTNER_ID, // your Partner Key
containerId: 'fruga-relay', // the container element on your page
onTokenRequired: async () => {
// Fetch a fresh access token from your own backend (Step 2)
const res = await fetch('/fruga/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userRef: currentUser.id }),
});
if (!res.ok) throw new Error('Failed to fetch Fruga token');
const { accessToken } = await res.json();
return accessToken;
},
});
The callback returns the access token, not the assertion — the exchange already happened on your server in Step 2.
Using React? The FrugaWidget component from @fruga/sdk/loader/react takes the same partnerKey and onTokenRequired props — see the SDK installation reference.
If the token is valid and the Partner Key is recognised, the widget will mount inside your container and your user will be authenticated automatically.
Verifying it works
In your browser’s developer tools, the Network tab should show a single successful POST /fruga/token to your own backend returning an accessToken — the calls to Fruga happen server-side, so the browser never sees them.
Behind that one request, your backend makes two calls to Fruga:
| Request | What it means |
|---|---|
GET {FRUGA_API_URL}/widget/bootstrap?partnerKey=... | Your partner configuration loaded successfully, and you have the audience to sign with |
POST {FRUGA_API_URL}/auth/external/token | The assertion was verified and a Fruga access token was issued |
If you see a 401 on the token exchange, the most common causes are a missing or unknown kid header, an iss that does not match your issued ISSUER, an expired assertion (check that your server clock is NTP-synced and that exp is set correctly), or an incorrect PARTNER_SECRET.
Next steps
With the widget running, the next thing to set up is cashback claims - the server-to-server call that tells Fruga when a user is entitled to a payout. Head to the Authentication guide to go deeper on how the token flow works, or jump straight to Partner API → Cashback Claim if you’re ready to set up payouts.