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.

💡
This guide covers the sandbox environment. Before going live, make sure you've read the Sandbox & Testing and Going Live guides.

Prerequisites

Before you begin, you’ll need:

RequirementDetails
Fruga credentialsThe four values below, provided by the Fruga team for each environment
A backendA server-side environment capable of signing JWTs and making outbound HTTPS calls to Fruga
A web frontendA 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
ValueWhat it isFrontend safe?
NEXT_PUBLIC_PARTNER_IDYour Partner Key. Passed to the widget, and used as ?partnerKey= when loading your configurationYes - it is a public identifier
PARTNER_SECRETThe HMAC key used to sign assertion JWTsNo - server-side only
PARTNER_KEY_IDGoes in the JWT header as kid, so Fruga knows which secret to verify withNo
ISSUERGoes in the JWT as the iss claimNo

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

⚠️
If you haven't received your credentials yet, contact us. Keep your 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:

1
Your frontend asks your backend for a token
When a user opens the page where Relay is embedded, the widget calls your onTokenRequired callback, which posts the current user's userRef to an endpoint on your own backend.
2
Your backend signs an assertion and exchanges it
Your server signs a short-lived JWT - the assertion - with your PARTNER_SECRET, carrying kid, iss and the audience Fruga advertises. It then posts that assertion to Fruga's token exchange endpoint itself.
3
Your backend returns the Fruga access token
The exchange returns a Fruga access token, which your endpoint passes back to the browser. The widget mounts with it and your user is authenticated - no separate login required. The assertion and your 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, ... }
});
⚠️
Always check that the user is authenticated in your own system before generating an assertion. If you issue an assertion for an unauthenticated request, an anonymous visitor could access another user's Relay data.
💡
If 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:

RequestWhat 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/tokenThe 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.

Once both requests return successfully, you have a working Relay integration. Your users can now see their cashback balance and browse offers inside the widget.

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.