Configuration
Sensible defaults, and every option when you need it.
The CopilotKit integration is configured through the AdzenAsyncConfig object passed to the AdzenAsyncMiddleware constructor, or AdzenStreamConfig passed to AdzenStreamMiddleware. Values can be sourced from environment variables, explicit code, or a combination.
Configuration precedence
- Explicit values passed to
new AdzenAsyncMiddleware({ ... }) - Environment variables read by your application
- SDK defaults for optional fields
There is no automatic environment variable binding — your application code reads environment variables and passes them to the constructor.
Where the middleware runs
Run AdzenAsyncMiddleware server-side — in the browser it would expose your API key. AdzenStreamMiddleware is server-side only (Node 18+).
Streaming proxies the LLM response stream into the Adzen /stream endpoint by passing a ReadableStream as the fetch request body. Only Chromium-based browsers implement fetch upload streams, and they additionally require HTTP/2. Safari and Firefox silently coerce a stream body to the string "[object ReadableStream]", which the API cannot process.
The SDK feature-detects this. When the runtime cannot send stream bodies, AdzenStreamClient emits an adzen_error with code request_streams_unsupported and skips the request rather than sending a corrupted body.
Since the Adzen API uses key auth, running server-side also keeps your API key out of the client bundle.
Environment variables
These are recommended environment variable names. Your application reads them and passes them to the middleware.
Required
| Variable | Purpose |
|---|---|
ADZEN_API_KEY | Adzen API key for your publisher account |
Optional
| Variable | Default | Purpose |
|---|---|---|
ADZEN_API_URL | https://api.adzen.ai/v1/ai | Adzen API base URL. The SDK appends /process and /stream. The default targets the hosted Adzen API, whose AI endpoints live under the /ai path prefix (so /process resolves to https://api.adzen.ai/v1/ai/process). Override only if Adzen gives you a different base URL. |
ADZEN_TIMEOUT_MS | 3000 | Maximum time (ms) to wait for the Adzen API before abandoning the ad fetch |
ADZEN_AD_UNIT_POSITION | "chin" | Ad unit position label used in tracking |
ADZEN_LOCATION | — | DMA location code (e.g. "US-CA-803") sent with requests for geo-targeting |
ADZEN_PROFILE_ID | — | Publisher profile ID, sent as the X-Profile-Id header. Only needed if Adzen support asks you to set it. |
Example .env
ADZEN_API_KEY=your_api_key
ADZEN_TIMEOUT_MS=3000
ADZEN_AD_UNIT_POSITION=chin
ADZEN_LOCATION=US-CA-803Adzen API response format
The Adzen /process API returns:
{
"message_id": "msg_abc123",
"ads": [
{
"ad_id": 1071,
"title": "Local Coffee Co",
"cta": "Learn More",
"click_through_url": "https://api.adzen.ai/…/redirect/abc1234",
"advertiser_name": "Acme Corp",
"sponsor_logo_url": "https://example.com/logo.png",
"creative_url": "https://example.com/banner.jpg",
"description": "A great product",
"placement": "after_message",
"pixel_trackers": [],
"render_impression_url": "https://api.adzen.ai/…/impressions/render",
"view_impression_url": "https://api.adzen.ai/…/impressions/view"
}
]
}The middleware consumes this format directly and maps it to AdzenPlacement internally:
| API field | SDK field |
|---|---|
ads[0].ad_id | ad_id (coerced to string) |
ads[0].title | headline |
ads[0].cta | cta_text |
ads[0].click_through_url | destination_url |
ads[0].advertiser_name | advertiser_name |
ads[0].sponsor_logo_url | advertiser_image_url |
ads[0].creative_url | creative_url |
ads[0].description | description |
ads[0].placement | adUnitPosition |
ads[0].render_impression_url | render_impression_url |
ads[0].view_impression_url | view_impression_url |
ads.length === 0 | No placement event emitted |
AdzenAsyncConfig options
Passed to new AdzenAsyncMiddleware(config):
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
apiKey | string | Yes | — | Adzen API key. Sent as the X-API-Key header. |
endpointUrl | string | No | "https://api.adzen.ai/v1/ai" | Adzen API base URL. The middleware appends /process when calling the ad matching endpoint. The default targets the hosted API (AI endpoints under the /ai prefix); override only if Adzen gives you a different base URL. |
timeoutMs | number | No | 3000 | Maximum time (ms) to wait for the /process API response. Uses AbortController to cancel the fetch on timeout. |
adUnitPosition | string | No | "chin" | Ad unit position label for this placement. |
location | string | No | — | DMA location code sent as a top-level field on the /process request for geo-targeting. |
conversationId | string | No | — | Conversation identifier sent as conversation_id on the /process request. Links impressions to conversation context. If a RUN_STARTED event has a threadId, it overrides this value. |
profileId | string | No | — | Publisher profile ID, sent as the X-Profile-Id header. Only needed if Adzen support asks you to set it. |
Recommended setup
import { AdzenAsyncMiddleware } from "@adzenai/ai/copilotkit";
const adzen = new AdzenAsyncMiddleware({
apiKey: process.env.ADZEN_API_KEY!,
location: process.env.ADZEN_LOCATION,
});With conversation tracking
const adzen = new AdzenAsyncMiddleware({
apiKey: process.env.ADZEN_API_KEY!,
location: "US-CA-803",
conversationId: session.conversationId,
});If you pass AG-UI RUN_STARTED events through the middleware and they contain a threadId field, the middleware automatically uses that as the conversation_id — overriding any value set in the config.
AdzenStreamConfig options
Coming soon. Streaming enrichment is not yet available in production. The async middleware is the supported path today; this section describes the streaming mode so you can plan for it.
Passed to new AdzenStreamMiddleware(config) (server-side only). AdzenStreamMiddlewareConfig extends AdzenStreamConfig with adUnitPosition.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
apiKey | string | Yes | — | Adzen API key. Sent as the X-API-Key header. |
endpointUrl | string | No | "https://api.adzen.ai/v1/ai" | Adzen API base URL. The SDK appends /process and /stream. The default targets the hosted API (AI endpoints under the /ai prefix); override only if Adzen gives you a different base URL. |
timeoutMs | number | No | 3000 | Maximum time (ms) to wait for the Adzen API before aborting via AbortController. |
location | string | No | — | DMA location code. Sent as the X-Location header on /stream and a top-level field on /process. |
conversationId | string | No | auto (UUID) | Conversation identifier. Overridden by an AG-UI RUN_STARTED threadId when present. |
profileId | string | No | — | Publisher profile ID, sent as the X-Profile-Id header. |
placementPollIntervalMs | number | No | 2000 | Interval (ms) for polling the sidecar placement endpoint. |
prefetch | boolean | No | false | When true, fires POST /process with the prompt before opening the stream. |
sidecar | boolean | No | true | Whether to poll the sidecar placement endpoint for out-of-stream ads. Set to false for inline-only. |
adUnitPosition | string | No | "chin" | Ad unit position label for this placement. |
AdzenCardProps options
Passed to <AdzenCard>:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
messageId | string | Yes | — | The assistant message ID to look up an ad placement for. |
adUnitPosition | string | No | "chin" | Ad unit position label for this placement. |
viewabilityThresholdMs | number | No | 1000 | Milliseconds the ad must be continuously visible before a view impression fires. |
className | string | No | — | CSS class applied to the card’s root <div>. |
Example
<AdzenCard
messageId={msg.id}
adUnitPosition="chin"
viewabilityThresholdMs={2000}
className="my-ad-card"
/>useAdzenPlacement return type
interface UseAdzenPlacementReturn {
getAdForMessage(messageId: string): AdzenPlacement | null;
placements: Map<string, AdzenPlacement>;
}| Property | Description |
|---|---|
getAdForMessage | Returns the AdzenPlacement for a given messageId, or null if no ad was matched. |
placements | The full Map of all received placements, keyed by messageId. |
Bridge functions
dispatchPlacementEvent(event)
Checks whether a single AG-UI event is an adzen_placement custom event and dispatches it as a browser CustomEvent on window (picked up by useAdzenPlacement). Returns true if the event was dispatched, false otherwise.
dispatchInlineAdEvent(event)
Checks whether a single AG-UI event is an adzen_inline_ad custom event and dispatches it as a browser CustomEvent on window (picked up by useAdzenInlineAds). Returns true if the event was dispatched, false otherwise. These events are emitted only by AdzenStreamMiddleware.
dispatchPlacementEvents(events)
Convenience wrapper that iterates an array of AG-UI events and calls both dispatchPlacementEvent and dispatchInlineAdEvent for each — so it bridges both adzen_placement cards and adzen_inline_ad in-message creatives. Safe to call on every batch of events from processEvent() or processStream(); events that are neither are silently ignored.
All three functions require a browser environment (window must be defined). They are no-ops in server-side contexts.
Viewability configuration
Viewability tracking uses IntersectionObserver via @adzenai/core’s observeViewability():
| Setting | Default | Description |
|---|---|---|
| Intersection threshold | 1.0 | The ad must be 100% visible in the viewport. |
| Time threshold | 1000ms | The ad must be continuously visible for this duration. |
| Firing behavior | Once | The impression fires once per ad instance. |
The intersection threshold is not configurable via AdzenCard — it always requires full visibility. The time threshold is configurable via viewabilityThresholdMs.
Impression beacons
AdzenCard fires two impression beacons as client-side GET requests directly to the delivery API. No server-side proxy or authentication is required. The impression URLs are pre-built by the API and fired as-is.
Render impression — fired on mount (when the ad first becomes available). Sent as a GET to the pre-built render_impression_url.
View impression — fired after the viewability threshold is met. Sent as a GET to the pre-built view_impression_url.
Null impression URLs
If render_impression_url or view_impression_url is null (impression tracking not configured for this ad), the corresponding beacon is skipped entirely.
Error handling and silent degradation
Adzen never throws errors into the AG-UI event stream or the React rendering tree. All failures are silent:
| Scenario | Behavior |
|---|---|
/process API returns non-200 | The middleware emits no placement event |
/process API times out | AbortController cancels the request → no placement event |
| Network error during ad fetch | The middleware emits no placement event |
/process API returns empty ads array | No placement event emitted |
| Impression beacon fails (render or view) | Single retry after 1 second; failure is swallowed |
window undefined (SSR) | dispatchPlacementEvent returns false — no-op |
Timeout and retry policy
Ad fetch
- Timeout: Controlled by
timeoutMs(default3000ms). UsesAbortControllerto cancel thefetch. - Retries: None. A timed-out or failed ad fetch is abandoned. The ad slot for that message is forfeited.
Impression beacon
- Timeout: No explicit timeout on the impression beacon
fetch. - Retries: One automatic retry after 1 second on failure. If the retry also fails, the error is swallowed.
RUN_FINISHED hold
RUN_FINISHED events are held until all pending ad fetches settle via Promise.allSettled. This ensures the stream does not terminate before ad placements are emitted. The hold duration is bounded by the timeoutMs of the slowest pending fetch.
Stale buffer eviction
On each TEXT_MESSAGE_START, the middleware evicts any buffer entries older than timeoutMs * 2. This prevents memory leaks from incomplete message sequences (e.g., a TEXT_MESSAGE_START without a corresponding TEXT_MESSAGE_END).
Next
- Integration walkthrough — end-to-end wiring guide
- How it works — architecture and full API reference
- Quick start — quick-start for existing apps