Get started
Complete example: Next.js
A session route, a widget with token refresh, and a signed webhook receiver for a Next.js app.
On this page
Prerequisites
- A Next.js app with its own sign-in. getSignedInUser below stands for your existing session lookup.
- The values from Keys and environments in .env.local, and the webhook signing secret as ADQUEST_WEBHOOK_SECRET.
- npm install @adquest/sdk
Step 1: The session route
app/api/adquest/session/route.ts
export async function POST(request: Request) {
const user = await getSignedInUser(request); // your own sign-in
if (!user) return new Response(null, { status: 401 });
const { locale } = (await request.json().catch(() => ({}))) as { locale?: string };
const response = await fetch(process.env.ADQUEST_API_URL + "/platform/participant-sessions", {
method: "POST",
headers: { "Content-Type": "application/json", "X-AdQuest-Platform-Secret": process.env.ADQUEST_PLATFORM_SECRET! },
body: JSON.stringify({ external_user_id: user.id, display_name: user.name, locale }),
cache: "no-store",
});
const result = await response.json();
if (!response.ok || !result.status) return new Response("Could not create an AdQuest session", { status: 502 });
return Response.json(
{ externalUserId: user.id, participantToken: result.data.participant_token, expiresAt: result.data.expires_at },
{ headers: { "Cache-Control": "no-store" } },
);
}The route answers 401 when nobody is signed in, never returns the Secret Key, and tells the browser not to cache the token.
Step 2: The widget, with refresh
app/rewards/Rewards.tsx
"use client";
import { useCallback, useEffect, useState } from "react";
import { AdQuestWidget } from "@adquest/sdk/react";
import "@adquest/sdk/styles.css";
type Session = { externalUserId: string; participantToken: string; expiresAt: string };
export default function Rewards() {
const [session, setSession] = useState<Session | null>(null);
const [failed, setFailed] = useState(false);
const refresh = useCallback(async () => {
const response = await fetch("/api/adquest/session", { method: "POST", body: JSON.stringify({ locale: "en" }) });
if (!response.ok) return setFailed(true);
setFailed(false);
setSession(await response.json());
}, []);
useEffect(() => { void refresh(); }, [refresh]);
useEffect(() => {
if (!session) return;
const wait = Math.max(1000, Date.parse(session.expiresAt) - Date.now() - 60_000);
const timer = setTimeout(() => void refresh(), wait);
return () => clearTimeout(timer);
}, [session, refresh]);
if (failed) return <button onClick={() => void refresh()}>Retry</button>;
if (!session) return <p>Loading rewards…</p>;
return <AdQuestWidget
key={session.externalUserId}
apiKey={process.env.NEXT_PUBLIC_ADQUEST_PLATFORM_API_KEY!}
baseUrl={process.env.NEXT_PUBLIC_ADQUEST_API_URL!}
externalUserId={session.externalUserId}
participantToken={session.participantToken}
locale="en"
onAuthRequired={() => void refresh()}
/>;
}The token lasts 15 minutes, so the component asks for a new one a minute before it ends. The same locale goes to the session route and to the widget.
Step 3: The webhook receiver
app/api/adquest/webhook/route.ts
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string) {
const match = /^t=(d+),v1=([0-9a-f]{64})$/.exec(header);
if (!match || Math.abs(Date.now() / 1000 - Number(match[1])) > 300) return false;
const expected = createHmac("sha256", secret).update(match[1] + "." + rawBody).digest("hex");
return timingSafeEqual(Buffer.from(match[2]), Buffer.from(expected));
}
export async function POST(request: Request) {
const rawBody = await request.text(); // before JSON parsing
if (!verify(rawBody, request.headers.get("X-AdQuest-Signature-V2") ?? "", process.env.ADQUEST_WEBHOOK_SECRET!)) {
return new Response("Invalid signature", { status: 401 });
}
const eventId = request.headers.get("X-AdQuest-Event-Id") ?? "";
if (await alreadyProcessed(eventId)) return new Response("OK"); // duplicates are normal
const event = JSON.parse(rawBody) as { external_user_id?: string; tokens?: number };
if (request.headers.get("X-AdQuest-Event") === "quest.completed") {
await grantInYourProduct(event.external_user_id ?? "", event.tokens ?? 0);
}
await markProcessed(eventId);
return new Response("OK");
}Check that it works
- Sign in, open the page and confirm the widget shows quests and a token balance.
- Complete a quest. The balance changes and your webhook receives quest.completed.
- Replay the delivery from the dashboard. Your receiver answers OK and grants nothing a second time.