Integration walkthrough
Everything between “we have a key” and “ads are live”, in order.
This guide walks through the full Adzen integration for a plain React or Next.js app — from access to live verification — including custom rendering and viewability. No CopilotKit or AG-UI.
Skip ahead:
- Need the short path? Start with the Overview.
- Already have an app? See the Quick start.
Prerequisites
- Node.js
>=18(for the server-side fetch) - React
>=18 - An Adzen API key
Part 1: Get your API key
- Contact your Adzen account representative to be onboarded. Adzen sets up your publisher profile.
- You are issued an API key. Store it where only your server can read it.
Part 2: Install and configure
npm install @adzenai/ai @adzenai/corePut your key where only the server can read it:
ADZEN_API_KEY=your_api_key
# optional
ADZEN_LOCATION=US-CA-803Part 3: Fetch a placement on the server
The API key must never reach the browser, so the fetch runs on the server. Pick whichever fits your app.
Option A — Next.js route handler:
// app/api/ad/route.ts
import { fetchAdzenAd } from "@adzenai/ai";
export async function POST(req: Request) {
const { content, messageId } = await req.json();
const placement = await fetchAdzenAd({
apiKey: process.env.ADZEN_API_KEY!,
content,
messageId: messageId ?? crypto.randomUUID(),
location: process.env.ADZEN_LOCATION,
});
return Response.json({ placement });
}Option B — server action:
"use server";
import { fetchAdzenAd } from "@adzenai/ai";
import type { AdzenPlacement } from "@adzenai/ai";
export async function getAd(content: string): Promise<AdzenPlacement | null> {
return fetchAdzenAd({
apiKey: process.env.ADZEN_API_KEY!,
content,
messageId: crypto.randomUUID(),
});
}Option C — React Server Component: call fetchAdzenAd directly in an async server component and pass the result to a client child.
Key behaviors:
- Returns an
AdzenPlacementon a match, ornullon no-match / non-200 / timeout / error. It never throws. - Sends
content+message_id(and optionallocation/conversation_id) to/processwith the requiredIdempotency-Key. - Aborts after
timeoutMs(default 3000) unless you pass your ownsignal.
Part 4: Render with AdzenCard
<AdzenCard> is a client component. Give it the placement your server returned:
"use client";
import { useEffect, useState } from "react";
import { AdzenCard } from "@adzenai/ai/react";
import type { AdzenPlacement } from "@adzenai/ai";
export function SponsoredSlot({ content }: { content: string }) {
const [ad, setAd] = useState<AdzenPlacement | null>(null);
useEffect(() => {
let cancelled = false;
fetch("/api/ad", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
})
.then((r) => r.json())
.then((d) => !cancelled && setAd(d.placement))
.catch(() => {});
return () => { cancelled = true; };
}, [content]);
// AdzenCard's `ad` prop doesn't accept null, so render it only once there's a placement.
return ad ? <AdzenCard ad={ad} /> : null;
}AdzenCard returns null while ad is null, so no empty DOM is rendered. When an ad is present the card shows the advertiser avatar, advertiser name, a “Relevant Ad” disclosure, the headline, and the CTA link.
Part 5: Custom ad rendering
For full control over markup, render your own and let useAdImpressions handle tracking. Attach the returned ref to the element that represents the ad.
"use client";
import { useAdImpressions } from "@adzenai/ai/react";
import type { AdzenPlacement } from "@adzenai/ai";
export function CustomAd({ ad }: { ad: AdzenPlacement | null }) {
const { ref } = useAdImpressions(ad, { viewabilityThresholdMs: 2000 });
if (!ad) return null;
return (
<aside ref={ref} className="my-custom-ad">
{ad.creative_url && <img src={ad.creative_url} alt={ad.headline} />}
<div className="label">Sponsored · {ad.advertiser_name}</div>
<h3>{ad.headline}</h3>
{ad.description && <p>{ad.description}</p>}
<a href={ad.destination_url} target="_blank" rel="noopener sponsored">
{ad.cta_text}
</a>
</aside>
);
}You render the markup; useAdImpressions fires the render beacon on mount and the view beacon after the threshold — the same tracking AdzenCard uses internally.
Part 6: Impression tracking and viewability
Both AdzenCard and useAdImpressions fire two beacons as client-side GET requests directly to the delivery API. No server proxy or auth headers are required — the URLs are pre-built by the API.
| Beacon | When | URL |
|---|---|---|
| Render | On mount | render_impression_url |
| View | After the ad is continuously visible for the threshold (default 1000ms) | view_impression_url |
- Viewability requires 100% visibility (
IntersectionObserverthreshold1.0); the time threshold isviewabilityThresholdMs. - Each beacon fires at most once per ad instance.
- A
nullimpression URL skips that beacon; a failed beacon retries once after 1 second, then is dropped.
Tune the time threshold per placement:
{placement && (
<AdzenCard ad={placement} viewabilityThresholdMs={2000} adUnitPosition="sidebar" />
)}Part 7: Verify a live run
- Fetch — trigger a request and confirm your server route returns a placement (or
null). - Render — the card / custom markup appears when there’s a match, and nothing renders otherwise.
- Render impression — Network tab shows a
GETto the render impression URL when the ad mounts. - View impression — keep the ad visible for the threshold; a
GETto the view impression URL fires. - Click-through — clicking the CTA opens the destination URL.
- Recorded — your Adzen contact can confirm the impression and click events were recorded.
If the Adzen API is unreachable or returns no match:
- No ad is rendered.
- No error is shown to the user.
- Your app is completely unaffected.
Test the empty path too. An empty response is a normal, expected outcome — it means we declined to fill the slot rather than show something irrelevant. Make sure your default state looks right before you go live.
Next
- Configuration — all options, props, and beacon parameters
- How it works — architecture and full API reference
- Streaming or CopilotKit agents? See the CopilotKit walkthrough.