Pull LinkJolt stats into your own admin UI — useful for internal tools, white-label portals, or embedded views in your SaaS product.
30 min read
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.
/api/dashboardServer-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,
});
}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>;
}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.
/stats response for 30-60 seconds to stay well below rate limitsPromise.all for parallel calls — each endpoint is independentX-RateLimit-Remaining to back off gracefullyRetry-After headerpage, limit) and cache pagesFull reference: GET /stats · GET /conversions · GET /affiliates