LinkJoltDevelopers

Manage API Keys

← Back to LinkJolt.io

← All guides

Build a custom dashboard

Pull LinkJolt stats into your own admin UI — useful for internal tools, white-label portals, or embedded views in your SaaS product.

Advanced

30 min read

What you'll build

A single-page dashboard with three widgets: aggregate stats, a recent conversions table, and a top-affiliates leaderboard. Works on Pro (read-only) or Ultimate. All data fetched server-side for security.

Architecture

  • Server: Next.js API route holds your LinkJolt API key (never expose to browser)
  • Client: React component calls your own /api/dashboard
  • Polling: refresh every 60s, or integrate webhooks for real-time updates

Step 1 — Server route

Server-side helper that calls LinkJolt and returns aggregated data to your frontend.

// app/api/dashboard/route.ts
const LINKJOLT = 'https://linkjolt.io/api/v1';
const KEY = process.env.LINKJOLT_API_KEY!;

async function lj(path: string) {
  const res = await fetch(`${LINKJOLT}${path}`, {
    headers: { Authorization: `Bearer ${KEY}` },
    cache: 'no-store',
  });
  if (!res.ok) throw new Error(`LinkJolt ${res.status}`);
  return res.json();
}

export async function GET(req: Request) {
  // Default to current month
  const from = new URL(req.url).searchParams.get('from')
    || new Date(new Date().getFullYear(), new Date().getMonth(), 1).toISOString();

  const [stats, conversions, affiliates] = await Promise.all([
    lj(`/stats?from=${from}`),
    lj(`/conversions?limit=10&sort=createdAt:desc`),
    lj(`/affiliates?limit=100&status=approved`),
  ]);

  return Response.json({
    stats: stats.data,
    recentConversions: conversions.data,
    affiliates: affiliates.data,
  });
}

Step 2 — Client component

Fetches your server route every 60 seconds and renders a simple dashboard.

'use client';
import { useEffect, useState } from 'react';

interface DashData {
  stats: { totalRevenue: number; totalCommissions: number; totalConversions: number; totalAffiliates: number };
  recentConversions: Array<{ id: string; amount: number; commission: number; status: string; createdAt: string }>;
  affiliates: Array<{ id: string; name: string; email: string }>;
}

export function Dashboard() {
  const [data, setData] = useState<DashData | null>(null);

  useEffect(() => {
    const load = () => fetch('/api/dashboard').then(r => r.json()).then(setData);
    load();
    const id = setInterval(load, 60_000);
    return () => clearInterval(id);
  }, []);

  if (!data) return <div>Loading...</div>;

  return (
    <div>
      <div className="stats-grid">
        <Stat label="Revenue" value={`$${data.stats.totalRevenue.toLocaleString()}`} />
        <Stat label="Commissions" value={`$${data.stats.totalCommissions.toLocaleString()}`} />
        <Stat label="Conversions" value={data.stats.totalConversions} />
        <Stat label="Affiliates" value={data.stats.totalAffiliates} />
      </div>

      <h2>Recent conversions</h2>
      <table>
        <thead>
          <tr><th>ID</th><th>Amount</th><th>Commission</th><th>Status</th></tr>
        </thead>
        <tbody>
          {data.recentConversions.map(c => (
            <tr key={c.id}>
              <td>{c.id}</td>
              <td>${c.amount}</td>
              <td>${c.commission}</td>
              <td>{c.status}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function Stat({ label, value }: { label: string; value: any }) {
  return <div className="stat"><div>{label}</div><div>{value}</div></div>;
}

Step 3 — Upgrade to real-time (Ultimate)

Instead of polling, subscribe to outbound webhooks. Your server receives conversion.created and conversion.updated events — write them to your DB and push to connected clients via SSE/WebSocket.

See the webhook setup guide for signature verification and retry handling.

Production tips

  • Cache the /stats response for 30-60 seconds to stay well below rate limits
  • Use Promise.all for parallel calls — each endpoint is independent
  • Check response headers X-RateLimit-Remaining to back off gracefully
  • On 429, respect the Retry-After header
  • For large affiliate counts, paginate server-side (page, limit) and cache pages

Full reference: GET /stats · GET /conversions · GET /affiliates