Skip to Content

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:

Prerequisites

  • Node.js >=18 (for the server-side fetch)
  • React >=18
  • An Adzen API key

Part 1: Get your API key

  1. Contact your Adzen account representative to be onboarded. Adzen sets up your publisher profile.
  2. 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/core

Put your key where only the server can read it:

ADZEN_API_KEY=your_api_key # optional ADZEN_LOCATION=US-CA-803

Part 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 AdzenPlacement on a match, or null on no-match / non-200 / timeout / error. It never throws.
  • Sends content + message_id (and optional location / conversation_id) to /process with the required Idempotency-Key.
  • Aborts after timeoutMs (default 3000) unless you pass your own signal.

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.

BeaconWhenURL
RenderOn mountrender_impression_url
ViewAfter the ad is continuously visible for the threshold (default 1000ms)view_impression_url
  • Viewability requires 100% visibility (IntersectionObserver threshold 1.0); the time threshold is viewabilityThresholdMs.
  • Each beacon fires at most once per ad instance.
  • A null impression 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

  1. Fetch — trigger a request and confirm your server route returns a placement (or null).
  2. Render — the card / custom markup appears when there’s a match, and nothing renders otherwise.
  3. Render impression — Network tab shows a GET to the render impression URL when the ad mounts.
  4. View impression — keep the ad visible for the threshold; a GET to the view impression URL fires.
  5. Click-through — clicking the CTA opens the destination URL.
  6. 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

Last updated on