Configuration
The CopilotKit integration is configured through the AdzenAsyncConfig object passed to the AdzenAsyncMiddleware constructor. 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.
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. Override only if using a non-production environment. |
ADZEN_TIMEOUT_MS | 3000 | Maximum time (ms) to wait for the Adzen /process 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 /process requests for geo-targeting |
Example .env
ADZEN_API_KEY=adz_live_abc123
ADZEN_TIMEOUT_MS=3000
ADZEN_AD_UNIT_POSITION=chin
ADZEN_LOCATION=US-CA-803Demo-Only Variables
These variables are used only by the built-in demo app (pnpm demo). They are not part of the SDK's runtime configuration.
| Variable | Required | Default | Purpose |
|---|---|---|---|
OPENAI_KEY | No | — | When set, the demo uses OpenAI gpt-4.1-mini for streaming agent responses instead of canned mock responses. |
ADZEN_KEY | No | — | When set, the demo calls the real Adzen API at https://api.adzen.ai/v1/ai/process instead of the mock ad API. The publisher profile is inferred from the key. |
Create a .env file in packages/ai/src/copilotkit/demo/ to set these. See Run the Demo for the full behavior matrix.
Adzen 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/dea/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",
"pricing_model": "CPC",
"relevancy_score": 0.95,
"placement": "after_message",
"pixel_trackers": [],
"render_impression_url": "https://delivery.adzen.ai/v1/ai/impressions/render?ad_id=1071&message_id=msg_abc123&conversation_id=conv_7f3a9b2e",
"view_impression_url": "https://delivery.adzen.ai/v1/ai/impressions/view?ad_id=1071&message_id=msg_abc123&conversation_id=conv_7f3a9b2e"
}
]
}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].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. |
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 included in impression beacons. |
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. |
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.
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 appended to impression beacon URLs. |
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. Returns true if the event was dispatched, false otherwise.
dispatchPlacementEvents(events)
Convenience wrapper that iterates an array of AG-UI events and calls dispatchPlacementEvent for each. Safe to call on every batch of events from processEvent() — non-placement events are silently ignored.
Both 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.
Render impression — fired on mount (when the ad first becomes available). Sent as a GET to the pre-built render_impression_url with additional query parameters:
GET {render_impression_url}&ad_unit_position=chin×tamp=2026-07-06T20:30:00.000ZView impression — fired after the viewability threshold is met. Sent as a GET to the pre-built view_impression_url with additional query parameters:
GET {view_impression_url}&ad_unit_position=chin&viewability_ms=1000×tamp=2026-07-06T20:30:01.000ZParameters Appended by the SDK
| Parameter | Type | Beacons | Description |
|---|---|---|---|
ad_unit_position | string | Both | Where the ad is rendered in the UI (e.g. "chin") |
timestamp | string | Both | ISO 8601 timestamp of the event |
viewability_ms | number | View only | The viewability threshold in milliseconds |
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 | fetchAd returns null → no placement event |
/process API times out | AbortController cancels the request → no placement event |
| Network error during ad fetch | catch block returns null → 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 Steps
- Integration Walkthrough — end-to-end wiring guide
- Developer Guide — architecture and full API reference
- Add Adzen to CopilotKit — quick-start for existing apps