# wavebird
> wavebird is ad infrastructure for consumer AI apps, founded by Mario von Bassen and operated by MC Squared UG in Munich, Germany. It connects applications to global ad demand through one integration, with labeled placements kept separate from model instructions and generated output.
## Product and business context
- [How wavebird works](https://wavebird.ai/how-it-works): Placement, data boundaries, measurement and settlement.
- [Consumer AI use cases](https://wavebird.ai/solutions): Chat, music and audio tools, image, video, 3D, app builders, agents and other user-facing AI experiences.
- [Ad revenue](https://wavebird.ai/guides/ad-revenue): Pricing assumptions, publisher share and net revenue.
- [Company and principles](https://wavebird.ai/glossary/wavebird): European origin, global ad demand and the intermediary role between publishers and advertisers.
- [Engineering evidence](https://wavebird.ai/resources/evidence): Ad-path benchmarks, eight-hour memory measurements and sustained load results.
Endpoint-specific developer status: API v1 is stable, `/v1/consent` is beta, and `/v1/jobs` plus `/v1/decisions/{slot_id}` are advanced; see [API stability](https://wavebird.ai/api/reference/stability). These labels describe individual API surfaces, not the commercial phase of the overall product.
## API quickstart
# Quickstart
Create a Server Test Key, request a placement from your backend, and render a filled response with /v1/render.js and wavebird.renderPlacement().
## Start in the dashboard
Create a workspace, open Dashboard API Keys, and create a Server Test Key. Copy the full sk_test_ value when it is created or rotated; masked previews cannot authenticate requests. Copy the Project ID (client_id, WAVEBIRD_CLIENT_ID), formatted like wbproj_..., and configure local allowed origins before browser testing.
Keep the server key on your backend. Browser activation uses a separate publishable key. See [key classes and authentication](/api/reference/authentication) for production credentials and rotation.
```bash
WAVEBIRD_API_BASE_URL=https://api.wavebird.ai
WAVEBIRD_SECRET_KEY=sk_test_your_server_test_key
WAVEBIRD_CLIENT_ID=wbproj_your_project_id
NEXT_PUBLIC_WAVEBIRD_PUBLISHABLE_KEY=pk_publishable_your_browser_key
```
[Open dashboard](https://dashboard.wavebird.ai/wavebird)
## Server API placement request
Recommended backend path. For dashboard Test projects using wavebird_consent, first record the actual session decision through POST /v1/consent with the same client_id and session_id; inline flags alone return 403 consent_not_current.
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/placements?wait_ms=1500 \
-H "Authorization: Bearer sk_test_wavebird_demo_secret" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_demo_8jK42",
"session_id": "sess_demo_123",
"job_type": "chat",
"slots_requested": 1,
"slot_hint": {
"position": "below",
"max_width": 728,
"max_height": 90
},
"overrides": {
"allowed_formats": ["banner", "clip", "native"],
"timing": "during"
},
"consent": {
"semantic_targeting": false,
"prompt_shared": false,
"gdpr_applies": false,
"consent_source": "wavebird_consent"
}
}'
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/placements?wait_ms=1500", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
job_type: "chat",
slots_requested: 1,
slot_hint: {
position: "below",
max_width: 728,
max_height: 90
},
overrides: {
allowed_formats: ["banner", "clip", "native"],
timing: "during"
},
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: false,
consent_source: "wavebird_consent"
}
})
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
payload = json.loads("{\"client_id\":\"wbproj_demo_8jK42\",\"session_id\":\"sess_demo_123\",\"job_type\":\"chat\",\"slots_requested\":1,\"consent\":{\"semantic_targeting\":false,\"prompt_shared\":false,\"gdpr_applies\":true,\"consent_source\":\"wavebird_consent\"}}")
request = urllib.request.Request(
"https://api.wavebird.ai/v1/placements?wait_ms=1500",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
## REST response and hosted renderer
Pass the server-owned placement response to renderPlacement in the browser.
## HTML
```html
```
## JavaScript
```javascript
let cleanupPlacement;
function classifyWavebirdPlacementResponse(payload) {
if (!payload || typeof payload !== "object") return "invalid_response";
if (payload.placement && payload.decision?.fill === true) return "filled";
if (!payload.placement && payload.decision?.fill === false) return "no_fill";
if (!payload.placement && (payload.decision == null || payload.status === "pending")) return "not_ready";
return "invalid_response";
}
async function requestSponsor() {
cleanupPlacement?.();
const slot = document.querySelector("#wavebird-slot");
const response = await fetch("/api/sponsor-slot", { method: "POST" });
const payload = await response.json();
if (!response.ok) {
slot.replaceChildren();
slot.hidden = true;
return { status: "request_rejected" };
}
const placementState = classifyWavebirdPlacementResponse(payload);
if (placementState !== "filled") {
slot.replaceChildren();
slot.hidden = true;
return { status: placementState };
}
slot.hidden = false;
cleanupPlacement = await window.wavebird.renderPlacement({
target: slot,
placement: payload.placement,
decision: payload.decision,
authoritative_consent: payload.authoritative_consent
});
if (typeof cleanupPlacement !== "function") {
slot.hidden = true;
return { status: "render_failed" };
}
return { status: "rendered" };
}
```
## Script Tag install
Browser-first rendering with built-in activation, consent, and beacons.
## HTML
```html
```
## Advanced SDK
Compatibility package for TypeScript teams that intentionally choose a wrapper.
## Node
```typescript
import { WavebirdClient } from "wavebird";
const client = new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getApiKey: () => process.env.WAVEBIRD_SECRET_KEY ?? "",
});
const job = await client.createJob({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
job_type: "chat",
slots_requested: 1,
});
```
## Validate the result
- Check filled, pending, and no-fill paths before live traffic.
- Dashboard-created Test projects require current lifecycle consent. Pass consent with /v1/placements or synchronize the session through /v1/consent.
- Confirm that the hosted renderer creates one frame and reports frame_error without positive signals when media cannot load.
Continue with the [REST and renderer guide](/api/patterns/server-api) and the [testing checklist](/api/testing). Other request examples are available in the [endpoint reference](/api/reference/endpoints).
## REST API reference
# Activate browser key
Exchanges a publishable key and origin for a short-lived browser activation token.
Use this from browser integrations before creating jobs. Send the publishable key in the JSON body; the API validates the Origin header against the key's allowed-origins list.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| POST | /v1/browser/activate | No Authorization header; publishable key in JSON body with allowed Origin | stable |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| publishable_key | string | Yes | Publishable key shown once in the dashboard. |
| Origin header | string | Yes | Browser origin that must match an allowed origin for the publishable key. |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| ok | boolean | Yes | True when activation succeeds. |
| activation_token | string | Yes | Short-lived token bound to the project and origin. |
| expires_at_ms | integer | Yes | Unix epoch milliseconds for token expiry. |
| key_id | string | Yes | Activated publishable key identifier. |
| wrapper_tenant_id | string | Yes | Wrapper tenant bound to the token. |
| project_id | string | Yes | Project bound to the token. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing or unknown publishable key. |
| 403 | forbidden | Origin is not allowed for this key. |
| 400 | validation_error | publishable_key is missing or malformed, or the Origin header is absent. |
## Response example
```json
{
"ok": true,
"activation_token": "wbat_demo_eyJhbGciOi...",
"expires_at_ms": 1777032600000,
"key_id": "key_demo_123",
"wrapper_tenant_id": "wbt_demo_123",
"project_id": "wbproj_demo_8jK42"
}
```
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/browser/activate \
-H "Origin: https://publisher.example" \
-H "Content-Type: application/json" \
-d '{
"publishable_key": "pk_publishable_wavebird_demo_publishable"
}'
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/browser/activate", {
method: "POST",
headers: {
Origin: "https://publisher.example",
"Content-Type": "application/json"
},
body: JSON.stringify({ publishable_key: "pk_publishable_wavebird_demo_publishable" })
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
request = urllib.request.Request(
"https://api.wavebird.ai/v1/browser/activate",
data=json.dumps({"publishable_key": "pk_publishable_wavebird_demo_publishable"}).encode("utf-8"),
headers={
"Origin": "https://publisher.example",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
# Create placement
Creates one job, waits for the first slot decision, and returns the canonical placement response.
Use this as the default Server API entry point. It combines job creation and first-decision wait so your frontend can hand the response to the hosted renderer instead of manually building media DOM.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| POST | /v1/placements | Secret key or browser activation token | stable |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| wait_ms | integer | No | Optional long-poll wait time in milliseconds. Defaults should stay short, for example 1500. |
| test_outcome | no_fill | No | Optional deterministic no-fill control for Test requests made with a Server Test Key. Never use it with Production keys. |
| client_id | string | Yes | Project ID (client_id, WAVEBIRD_CLIENT_ID), formatted like wbproj_.... |
| session_id | string | Yes | Stable publisher session identifier. |
| job_type | string: chat, code, image, voice, agent | Yes | Workload category: chat, code, image, voice, agent. |
| prompt | object \| string | No | Optional context. Supplied text reaches the Wavebird API; targeting requires the applicable semantic_targeting and prompt_shared permissions. Prefer a controlled topic and omit raw text when it should not be transmitted. Standard SSP builders use the partner signal projection. |
| slot_hint | object | No | Placement position and maximum dimensions. |
| overrides | object | No | Advanced request-level controls: allowed_formats (banner, clip, native), native_template_id, timing, bidfloor, bidfloor_currency, publisher, blocked_categories, and related policy settings. |
| consent | object | No | Projects using wavebird_consent require a current stored consent record for the authenticated tenant, client_id, and session_id. Record the actual user decision through /v1/consent before placement; inline flags cannot replace or expand that permission. |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| slot_id | string | Yes | Slot identifier for diagnostics and advanced compatibility. |
| status | string | Yes | Decision state, usually pending or ready. |
| placement | object \| null | Yes | Hosted-render placement descriptor when a fill is ready. |
| placement.render | object | No | Hosted frame descriptor with frame_url, script_url, media_type (image, video, or native), dimensions, label, sponsor, click URL, and native_template_id for native fills. |
| decision | object \| null | Yes | Canonical decision payload retained for compatibility and debugging. |
| decision.format | banner \| clip \| native | No | Filled creative format. Native fills return structured assets; clip fills return video media. |
| decision.assets | object \| null | No | Native-only assets: title, image_url, optional description, cta_text, and icon_url. Null for banner and clip. |
| decision.delivery_url | string \| null | No | Banner image or clip video URL. Null for native fills because native assets are returned as structured fields. |
| authoritative_consent | object | No | Decision-bound stored or request-level consent snapshot used by render.js. Rendering and beacons fail closed unless it is granted, current, and unexpired. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 403 | consent_not_current | Required stored session consent is missing, stale, denied, revoked, or outside the authenticated scope. Inline flags cannot substitute for it. |
| 403 | consent_state_unavailable | Required consent storage could not be read. Keep advertising off and preserve the primary app flow. |
| 403 | privacy_signal_policy_required | A positive Sec-GPC signal was observed. Do not strip it or retry advertising without it. |
| 401 | unauthorized | Missing or inactive secret key or browser activation token. |
| 403 | forbidden | Wrong key type for the request. |
| 429 | rate_limited | Key exceeded its rate limit; retry after the Retry-After header. |
| 415 | unsupported_media_type | Non-empty request bodies must use Content-Type: application/json. |
| 400 | validation_error | The request body failed validation. TCF/GPP inputs, including structurally valid strings without an authoritative policy mapping, are currently rejected. |
## Response example
```json
{
"slot_id": "slot_demo_123",
"status": "ready",
"placement": {
"image_url": "https://cdn.example.com/native-main.png",
"video_url": null,
"click_url": "https://sponsor.example",
"sponsor_name": "Demo Sponsor",
"width": 300,
"height": 250,
"format": "native",
"asset_token": "wbat_asset_demo",
"ad_label_text": "Sponsored",
"render": {
"strategy": "hosted_frame",
"frame_url": "https://api.wavebird.ai/v1/render/wbat_asset_demo",
"script_url": "https://api.wavebird.ai/v1/render.js",
"media_type": "native",
"width": 300,
"height": 250,
"aspect_ratio": "300/250",
"label_text": "Sponsored",
"sponsor_name": "Demo Sponsor",
"click_url": "https://sponsor.example",
"native_template_id": "card"
}
},
"authoritative_consent": {
"lifecycle_state": "granted",
"revision": 1,
"updated_at_ms": 1777032000000,
"expires_at_ms": 1777032300000,
"source": "wrapper_cmp",
"purposes": { "semantic_targeting": true, "prompt_shared": false }
},
"decision": {
"fill": true,
"format": "native",
"asset_token": "wbat_asset_demo",
"delivery_url": null,
"assets": {
"title": "Explore weekend travel deals",
"image_url": "https://cdn.example.com/native-main.png",
"description": "Save on hand-picked city breaks.",
"cta_text": "Book now",
"icon_url": "https://cdn.example.com/native-icon.png"
}
}
}
```
## Format response shapes
Banner fills expose placement.image_url and decision.delivery_url, and placement.render.media_type is image.
Clip fills expose placement.video_url and decision.delivery_url, and placement.render.media_type is video. Hosted clip rendering emits play_started and play_completed beacons automatically.
Native fills expose structured decision.assets and placement.image_url from assets.image_url. decision.delivery_url is null, and placement.render.media_type is native.
Test traffic is non-billable. Test substitute assets currently cover banner and clip media; native fills still use the normal structured native asset shape.
## Per-format placement bodies
Use overrides.allowed_formats to test one format at a time in Test mode. Empty arrays and unsupported values are rejected.
```json
[
{
"client_id": "wbproj_...",
"session_id": "sess_...",
"job_type": "chat",
"overrides": { "allowed_formats": ["banner"] }
},
{
"client_id": "wbproj_...",
"session_id": "sess_...",
"job_type": "chat",
"overrides": { "allowed_formats": ["clip"] }
},
{
"client_id": "wbproj_...",
"session_id": "sess_...",
"job_type": "chat",
"overrides": {
"allowed_formats": ["native"],
"native_template_id": "card"
}
}
]
```
## Common request errors
Malformed JSON returns validation_error with status 400.
Wrong Content-Type returns unsupported_media_type with status 415.
Invalid formats, unsupported format combinations, stale beacon timestamps, and invalid asset tokens are rejected before billing.
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/placements?wait_ms=1500 \
-H "Authorization: Bearer sk_test_wavebird_demo_secret" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_demo_8jK42",
"session_id": "sess_demo_123",
"job_type": "chat",
"slots_requested": 1,
"slot_hint": {
"position": "below",
"max_width": 728,
"max_height": 90
},
"overrides": {
"allowed_formats": ["banner", "clip", "native"],
"timing": "during"
},
"consent": {
"semantic_targeting": false,
"prompt_shared": false,
"gdpr_applies": false,
"consent_source": "wavebird_consent"
}
}'
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/placements?wait_ms=1500", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
job_type: "chat",
slots_requested: 1,
slot_hint: {
position: "below",
max_width: 728,
max_height: 90
},
overrides: {
allowed_formats: ["banner", "clip", "native"],
timing: "during"
},
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: false,
consent_source: "wavebird_consent"
}
})
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
payload = json.loads("{\"client_id\":\"wbproj_demo_8jK42\",\"session_id\":\"sess_demo_123\",\"job_type\":\"chat\",\"slots_requested\":1,\"consent\":{\"semantic_targeting\":false,\"prompt_shared\":false,\"gdpr_applies\":true,\"consent_source\":\"wavebird_consent\"}}")
request = urllib.request.Request(
"https://api.wavebird.ai/v1/placements?wait_ms=1500",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
# Create job
Advanced compatibility route that creates a sponsorship job and returns one or more slot IDs.
Use this when you intentionally need job-plus-decision orchestration. New Server API integrations should start with POST /v1/placements. Browser-activation requests use a separate public-contract branch with contract_version and job.job_type.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| POST | /v1/jobs | Secret key, or activation token for browser flows | advanced |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| client_id | string | Yes | Project ID (client_id, WAVEBIRD_CLIENT_ID), formatted like wbproj_.... |
| session_id | string | No | Optional stable publisher session identifier. |
| job_type | string: chat, code, image, voice, agent | No | Optional workload category: chat, code, image, voice, agent. Missing values normalize to chat. |
| slots_requested | integer | No | Number of ad slots to request. Defaults to one. |
| prompt | object \| string | No | Optional context. Supplied text reaches the Wavebird API; targeting requires the applicable semantic_targeting and prompt_shared permissions. Prefer a controlled topic and omit raw text when it should not be transmitted. Standard SSP builders use the partner signal projection. |
| consent | object | No | Request-level privacy flags. Projects using wavebird_consent still require a current stored record for the authenticated project and session; inline flags cannot substitute for it. |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| job_id | string | Yes | Created job identifier. |
| slot_ids | string[] | Yes | Slots to render or poll for decisions. |
| poll_path_template | string | Yes | Canonical decision polling path. |
| request_id | string | Yes | Support identifier. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 403 | consent_not_current | Required stored session consent is missing, stale, denied, revoked, or outside the authenticated scope. Inline flags cannot substitute for it. |
| 403 | consent_state_unavailable | Required consent storage could not be read. Keep advertising off and preserve the primary app flow. |
| 403 | privacy_signal_policy_required | A positive Sec-GPC signal was observed. Do not strip it or retry advertising without it. |
| 401 | unauthorized | Missing key or activation token. |
| 403 | forbidden | Wrong key type for the request. |
| 429 | rate_limited | Key exceeded its rate limit; retry after the Retry-After header. |
| 415 | unsupported_media_type | Non-empty request bodies must use Content-Type: application/json. |
| 400 | validation_error | The request body failed validation. TCF/GPP inputs, including structurally valid strings without an authoritative policy mapping, are currently rejected. |
## Response example
```json
{
"job_id": "job_demo_123",
"slot_ids": ["slot_demo_123"],
"poll_path_template": "/v1/decisions/{slot_id}",
"request_id": "req_JVd28x"
}
```
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/jobs \
-H "Authorization: Bearer sk_test_wavebird_demo_secret" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_demo_8jK42",
"session_id": "sess_demo_123",
"job_type": "chat",
"slots_requested": 1,
"consent": {
"semantic_targeting": false,
"prompt_shared": false,
"gdpr_applies": false,
"consent_source": "wavebird_consent"
}
}'
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/jobs", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
job_type: "chat",
slots_requested: 1,
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: false,
consent_source: "wavebird_consent"
}
})
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
payload = json.loads("{\"client_id\":\"wbproj_demo_8jK42\",\"session_id\":\"sess_demo_123\",\"job_type\":\"chat\",\"slots_requested\":1}")
request = urllib.request.Request(
"https://api.wavebird.ai/v1/jobs",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
# Get decision
Polls a slot for the selected sponsored asset and render metadata.
Call this after job creation only for advanced flows. Filled decisions include placement.render so the hosted renderer can own media, sizing, click, and beacon behavior.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| GET | /v1/decisions/{slot_id} | Secret key or browser activation token | advanced |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| slot_id | string | Yes | Slot ID returned from POST /v1/jobs. |
| wait_ms | integer | No | Optional long-poll wait time in milliseconds. |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| status | string | Yes | Decision state, usually pending, ready, or no_fill. |
| placement | object \| null | Yes | Hosted-render placement descriptor when a fill is ready. |
| placement.render | object | No | Hosted frame descriptor with frame_url, script_url, media_type, dimensions, label, sponsor, and click URL. |
| decision | object \| null | Yes | Canonical decision payload retained for compatibility and debugging. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing auth. |
| 404 | not_found | Slot does not exist or belongs to another project. |
| 429 | rate_limited | Polling rate exceeded. |
## Response example
```json
{
"status": "ready",
"slot_id": "slot_demo_123",
"placement": {
"image_url": "https://api.wavebird.ai/v1/test-assets/banner?...",
"click_url": "https://sponsor.example",
"sponsor_name": "Demo Sponsor",
"width": 728,
"height": 90,
"format": "banner",
"asset_token": "wbat_asset_demo",
"ad_label_text": "Sponsored",
"render": {
"strategy": "hosted_frame",
"frame_url": "https://api.wavebird.ai/v1/render/wbat_asset_demo",
"script_url": "https://api.wavebird.ai/v1/render.js",
"media_type": "image",
"width": 728,
"height": 90,
"aspect_ratio": "728/90",
"label_text": "Sponsored",
"sponsor_name": "Demo Sponsor",
"click_url": "https://sponsor.example"
}
},
"decision": {
"fill": true,
"format": "banner",
"asset_token": "wbat_asset_demo"
}
}
```
## cURL
```bash
curl "https://api.wavebird.ai/v1/decisions/slot_demo_123?wait_ms=1500" \
-H "Authorization: Bearer sk_test_wavebird_demo_secret"
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/decisions/slot_demo_123?wait_ms=1500", {
headers: { Authorization: "Bearer sk_test_wavebird_demo_secret" }
});
console.log(await response.json());
```
## Python
```python
import urllib.request
request = urllib.request.Request(
"https://api.wavebird.ai/v1/decisions/slot_demo_123?wait_ms=1500",
headers={"Authorization": "Bearer sk_test_wavebird_demo_secret"},
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
# Hosted renderer script
Returns the browser renderer that mounts hosted placement frames and manages turn lifecycle.
Load this script in the customer frontend for the default Server API render path. Use window.wavebird.renderPlacement for REST placement responses. startTurn and withTurn remain optional Script Tag lifecycle helpers.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| GET | /v1/render.js | No authorization header | stable |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| window.wavebird.renderPlacement(options) | function | Yes | Renders a decision or placement into a target element using placement.render.frame_url. |
| window.wavebird.withTurn(input, work) | function | No | Optional Script Tag lifecycle helper. It is not required for the Server API REST path, and data-wavebird-request is not read. |
| renderer diagnostics | beacon metadata | No | Hosted renderer beacons include safe geometry and dry-run diagnostics such as proof_source, proof_eligible, billable, non_billable_reason, geometry_reason, renderer_diagnostic_case, production_dry_run, production_live_approved, contract_exists, and renderer_geometry_summary for dashboard debugging. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 404 | not_found | Renderer script route is unavailable. |
## Response example
```json
const cleanup = await window.wavebird.renderPlacement({
target: "#wavebird-slot",
placement,
decision,
authoritative_consent
});
```
## Renderer proof diagnostics
The hosted renderer sends diagnostic beacons automatically. A renderer beacon can be accepted for logging while still being proof-ineligible and non-billable.
Dashboard logs may show proof_source=public_wrapper_renderer, proof_eligible=false, billable=false, non_billable_reason, geometry_reason, renderer_diagnostic_case, production_dry_run, production_live_approved, contract_exists, and renderer_geometry_summary.
Common geometry reasons are INSUFFICIENT_VISIBLE_AREA, HIDDEN_OR_NOT_VIEWABLE, OFFSCREEN, RENDERED_SIZE_TOO_SMALL, and NO_RENDERER_GEOMETRY_PROOF.
Test traffic remains non-billable. Production dry-run is also non-billable: create a Server Production Dry-run Key in Dashboard API Keys or use an operator-issued sk_dry_... server key as WAVEBIRD_SECRET_KEY.
Production dry-run is selected by credential class, not by request fields. Do not send production_dry_run, production_billing_dry_run, billing_suppressed, or production_live_approved in request bodies; those names are response, log, or dashboard diagnostics only.
Qualifying renderer proof can show proof_eligible=true and proof_would_be_eligible=true while billable=false, billing_suppressed=true, dry_run_billable_suppressed=true, and non_billable_reason=PRODUCTION_DRY_RUN_NON_BILLABLE.
Production dry-run requires no payout setup and does not use production-live-approved. Dashboard rows should expose mode=production-dry-run and production_live_approved=false during dry-run QA. If no active SSP demand is available, placement creation returns 409 ssp_not_ready with production_dry_run=true, production_billing_dry_run=true, billing_suppressed=true, production_live_approved=false, live_billing=false, and billable=false.
## cURL
```bash
curl "https://api.wavebird.ai/v1/render.js" \
-H "Accept: application/javascript"
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/render.js", {
headers: { Accept: "application/javascript" }
});
console.log((await response.text()).slice(0, 80));
```
## Python
```python
import urllib.request
request = urllib.request.Request(
"https://api.wavebird.ai/v1/render.js",
headers={"Accept": "application/javascript"},
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8")[:80])
```
# Hosted render frame
Returns isolated creative HTML for a single asset token.
The hosted renderer creates an iframe for this route. Customer frontends should not construct this URL manually in the default flow; use placement.render.frame_url.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| GET | /v1/render/{asset_token} | Asset token in the path | stable |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| asset_token | string | Yes | Asset token from placement.render.frame_url or placement.asset_token. |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| html | text/html | Yes | Isolated creative frame with strict CSP and no-store/private caching. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 404 | not_found | Asset token is invalid, expired, or not renderable. |
| 403 | forbidden | Asset token is not authorized for the requested creative context. |
## Response example
```json
```
## cURL
```bash
curl "https://api.wavebird.ai/v1/render/wbat_asset_demo" \
-H "Accept: text/html"
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/render/wbat_asset_demo", {
headers: { Accept: "text/html" }
});
console.log((await response.text()).slice(0, 80));
```
## Python
```python
import urllib.request
request = urllib.request.Request(
"https://api.wavebird.ai/v1/render/wbat_asset_demo",
headers={"Accept": "text/html"},
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8")[:80])
```
# Record beacon
Records render, visible, click, completion, or heartbeat events for billing and proof integrity.
Advanced direct server path. Default integrations should render placement.render with the hosted renderer, which sends browser beacons automatically to /public/wrapper/v1/beacons. Direct beacon writes are for server-rendered, custom-rendered, or QA validation flows; they require the sensitive asset_token returned with the placement or decision and are idempotent by beacon_id.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| POST | /v1/beacons | Secret key or browser activation token plus the issued asset_token | stable |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| beacon_id | string | Yes | Idempotency key for this beacon event. |
| slot_id | string | Yes | Slot associated with the rendered decision. |
| asset_token | string | Yes | Sensitive asset token issued with the placement response, usually placement.asset_token or decision.asset_token. Required for direct server /v1/beacons; redact it from logs, screenshots, reports, terminal output, external evidence, and client-visible debug output. Do not commit it, do not confuse it with WAVEBIRD_SECRET_KEY/server secret key or publishable key, and do not expose it unnecessarily to frontend code unless required by the hosted renderer flow. |
| event | string | Yes | rendered, visible, clicked, completed, play_started, play_completed, or heartbeat. |
| occurred_at | string | Yes | Fresh ISO timestamp for when the event happened. Generate it at runtime; stale copied sample timestamps can return BEACON_TOO_LATE. |
| metadata | object | No | Optional measurement metadata for the beacon event. |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| ok | boolean | Yes | True when the beacon request was processed successfully. |
| accepted | boolean | Yes | True when the beacon was accepted. |
| duplicate | boolean | Yes | True when the same beacon_id and event context had already been recorded. |
| reason_code | string | Yes | OK for accepted and duplicate beacons. |
| proof_source | string | No | Safe diagnostic source label, for example public_wrapper_renderer for hosted renderer rows or direct_server for advanced direct-server beacon rows. |
| proof_eligible | boolean | No | Whether the event currently qualifies as proof. Accepted diagnostics can still be proof_eligible=false. |
| billable | boolean | No | Whether the event is billable. Accepted diagnostics and all Test traffic can still be billable=false. |
| non_billable_reason | string | No | Reason an accepted diagnostic is not billable, such as DIRECT_BEACON_NOT_BILLABLE, SANDBOX_NON_BILLABLE, or a geometry reason. |
| geometry_reason | string | No | Renderer geometry classification: INSUFFICIENT_VISIBLE_AREA, HIDDEN_OR_NOT_VIEWABLE, OFFSCREEN, RENDERED_SIZE_TOO_SMALL, or NO_RENDERER_GEOMETRY_PROOF. |
| contract_exists | boolean | No | Whether the beacon was matched to an internal placement contract for diagnostics/proof evaluation. |
| renderer_diagnostic_case | string | No | Safe derived case label for renderer diagnostics, such as clipped_8x1_25, hidden, opacity_zero, ancestor_opacity_zero, offscreen, zero_size, tiny, or normal_visible. |
| renderer_geometry_summary | object | No | Safe renderer geometry summary for debugging, including declared, wrapper, target, visible, intersection, opacity, offscreen, cropped, and renderer version fields when available. |
| mode | string | No | Safe runtime mode label, such as production-dry-run when production dry-run validation is enabled. |
| production_dry_run | boolean | No | True when the event was evaluated under production dry-run mode. |
| production_billing_dry_run | boolean | No | True when production billing side effects are suppressed for dry-run validation. |
| production_live_approved | boolean | No | False in this dry-run flow. production-live-approved is not used for dry-run QA. |
| billing_suppressed | boolean | No | True when dry-run mode suppresses billing side effects. |
| dry_run_billable_suppressed | boolean | No | True when proof would otherwise be eligible but billing was suppressed by production dry-run. |
| proof_would_be_eligible | boolean | No | True for qualifying renderer proof that would be proof-eligible while remaining non-billable in production dry-run. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing auth or asset token. |
| 403 | forbidden | API key or activation token does not match the project that issued the asset_token. |
| 400 | validation_error | Unsupported event name, malformed body, stale occurred_at / BEACON_TOO_LATE, or slot_id/asset_token mismatch. Errors include field paths. |
| 404 | not_found | asset_token was not found or is no longer valid. |
## Response example
```json
{
"ok": true,
"accepted": true,
"duplicate": false,
"reason_code": "OK"
}
```
## Hosted renderer beacons vs direct server beacons
Most integrations should use the hosted renderer first. Hosted renderer beacons are sent automatically to /public/wrapper/v1/beacons after placement.render is mounted.
For clip placements, the hosted renderer emits play_started and play_completed. Direct server integrations can send play_started and play_completed to /v1/beacons when they own a custom clip renderer.
For native and banner placements, render/click/visibility beacons use the same issued asset_token; native click handling is attached to the hosted native frame overlay.
Use direct /v1/beacons only for advanced server-rendered, custom-rendered, or QA flows. Direct server beacons should not be used to fake or duplicate renderer events except for explicit idempotency or QA testing.
Direct server beacons always require the issued asset_token from placement.asset_token or decision.asset_token.
Direct server beacons are diagnostic/non-billable by default unless a trusted measurement policy explicitly allows them.
| Path | Endpoint | Caller | Use case | Manual code required |
| --- | --- | --- | --- | --- |
| Hosted renderer beacons | /public/wrapper/v1/beacons | wavebird hosted renderer/browser runtime | Default measurement/proof path | No |
| Direct server beacons | /v1/beacons | Publisher server | Advanced server-rendered/custom-rendered/QA path | Yes |
## Renderer diagnostic outcomes
Accepted renderer diagnostics do not automatically mean proof eligible or billable. Check proof_eligible and billable separately.
INSUFFICIENT_VISIBLE_AREA means the declared creative exists but the measured visible/intersection area was too small, for example because the slot was clipped, cropped, masked, or mostly outside the viewport.
HIDDEN_OR_NOT_VIEWABLE means the renderer evidence indicates display:none, visibility:hidden/collapse, zero/effective opacity, or a hidden/zero-viewable state.
OFFSCREEN means the renderer evidence indicates the slot is outside the viewport. RENDERED_SIZE_TOO_SMALL means the rendered container itself is below the paid-size contract without a clearer clipping signal.
NO_RENDERER_GEOMETRY_PROOF means proof-required geometry was missing. Test traffic remains non-billable even when diagnostics are accepted.
renderer_diagnostic_case is a safe derived label that helps identify the exact renderer row during QA without exposing raw tokens, prompts, or private advertiser policy.
Production dry-run is explicitly non-billable. Create a Server Production Dry-run Key in Dashboard API Keys or use an operator-issued sk_dry_... server key as WAVEBIRD_SECRET_KEY.
Production dry-run is selected by credential class, not by request fields. Do not send production_dry_run, production_billing_dry_run, billing_suppressed, or production_live_approved in request bodies; those names are response, log, or dashboard diagnostics only.
A qualifying renderer proof row can expose mode=production-dry-run, PRODUCTION_DRY_RUN_NON_BILLABLE, production_dry_run=true, production_live_approved=false, billing_suppressed=true, dry_run_billable_suppressed=true, and proof_would_be_eligible=true.
No payout setup is required for production dry-run, and production-live-approved is not used. If no active SSP demand is available, the safe blocker is 409 ssp_not_ready with mode=production-dry-run, production_dry_run=true, production_billing_dry_run=true, billing_suppressed=true, production_live_approved=false, live_billing=false, and billable=false.
| Field | Meaning |
| --- | --- |
| proof_source | Where the proof/diagnostic evidence came from, such as public_wrapper_renderer or direct_server. |
| proof_eligible | Whether the event qualifies as proof after policy and geometry checks. |
| billable | Whether the event can create billable production value. |
| non_billable_reason | Why the accepted event is not billable. |
| geometry_reason | Renderer geometry classification when geometry prevents proof. |
| contract_exists | Whether an internal placement contract was found. |
| renderer_diagnostic_case | Derived renderer case label, such as clipped_8x1_25, hidden, opacity_zero, offscreen, zero_size, tiny, or normal_visible. |
| renderer_geometry_summary | Safe geometry summary for debugging, without raw tokens or private advertiser policy. |
| production_dry_run | Whether production dry-run mode was active for the event. |
| production_live_approved | False for dry-run QA; production-live-approved is not used. |
| proof_would_be_eligible | Whether qualifying renderer proof would be eligible if billing were not suppressed by dry-run. |
| dry_run_billable_suppressed | Whether production dry-run suppressed billable side effects for otherwise qualifying proof. |
## Fresh timestamps and BEACON_TOO_LATE
occurred_at must be close to the real event time. Do not copy old static timestamps from examples.
Generate a fresh timestamp at runtime for every direct server beacon call.
BEACON_TOO_LATE means the event timestamp is too old to be accepted. This commonly happens when testing with copied sample timestamps; use current timestamps for direct server beacon calls.
```typescript
const occurred_at = new Date().toISOString();
```
## asset_token handling
asset_token is required for direct server /v1/beacons. It is issued with the placement response and is usually available as placement.asset_token or decision.asset_token.
Treat asset_token as sensitive proof material. Redact it from logs, screenshots, reports, support tickets, terminal output, external evidence, and client-visible debug output.
Do not commit asset_token, do not confuse it with WAVEBIRD_SECRET_KEY/server secret key or publishable key, and do not expose it unnecessarily to frontend code unless required by the hosted renderer flow.
## cURL
```bash
OCCURRED_AT="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
curl -X POST https://api.wavebird.ai/v1/beacons \
-H "Authorization: Bearer sk_test_wavebird_demo_secret" \
-H "Content-Type: application/json" \
-d "{
\"beacon_id\": \"bcn_example_rendered_001\",
\"slot_id\": \"slot_...\",
\"asset_token\": \"wbat_...\",
\"event\": \"rendered\",
\"occurred_at\": \"$OCCURRED_AT\",
\"metadata\": {}
}"
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/beacons", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({
beacon_id: "bcn_demo_123",
slot_id: "slot_demo_123",
asset_token: "wbat_asset_demo",
event: "rendered",
occurred_at: new Date().toISOString(),
metadata: {}
})
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
from datetime import datetime, timezone
body = {
"beacon_id": "bcn_example_rendered_001",
"slot_id": "slot_...",
"asset_token": "wbat_...",
"event": "rendered",
"occurred_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"metadata": {},
}
request = urllib.request.Request(
"https://api.wavebird.ai/v1/beacons",
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
## Illustrative JSON shape
```json
{
"beacon_id": "bcn_example_rendered_001",
"slot_id": "slot_...",
"asset_token": "wbat_...",
"event": "rendered",
"occurred_at": "",
"metadata": {}
}
```
# Record consent
Stores publisher consent state for Script Tag and custom consent flows.
Projects using wavebird_consent require a current stored consent record for the authenticated tenant, client_id, and session_id. Record the actual user decision through /v1/consent before placement; inline flags cannot replace or expand that permission. Other project modes may accept request-level flags; they do not bypass origin, protocol, GPC, or activation gates.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| POST | /v1/consent | Secret key, or activation token for browser flows | beta |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| client_id | string | Yes | Project ID (client_id, WAVEBIRD_CLIENT_ID), formatted like wbproj_.... |
| session_id | string | Yes | Publisher session identifier. |
| decision | string | Yes | Consent decision: personalized, basic, or custom. |
| source | string | Yes | Consent source: publisher_custom, server_sync, or wavebird_dialog. Compatibility aliases publisher and custom_dialog normalize to publisher_custom but are not recommended for new integrations. |
| lifecycle_state | string | No | Explicit session state: granted, denied, withdrawn, revoked, or cmp_error. Omission retains an existing state; it does not reopen a withdrawn record. For synthetic staff Test sessions, record an explicitly scoped test authorization. |
| revision | integer | No | Positive state revision. Omission allocates the next revision; when supplied, stale or conflicting updates are rejected. Use a newer explicit granted state only after a fresh authorization decision. |
| purposes | object | No | Purpose-level consent flags: semantic_targeting, session_persistence, cross_session_persistence, and prompt_shared. Compatibility purposes.ads maps to semantic_targeting and purposes.measurement maps to session_persistence. |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| consent_id | string | Yes | Stored consent record identifier. |
| session_id | string | Yes | Publisher session identifier for the stored consent state. |
| expires_at | string | Yes | ISO timestamp when the stored consent state expires. |
| authoritative_consent | object | Yes | Complete stored lifecycle snapshot: optional consent_id, lifecycle_state, revision, update and expiry timestamps, source, and purposes. |
| normalized.source | string | Yes | Canonical source after compatibility alias mapping. |
| normalized.purposes | object | No | Canonical purpose flags after compatibility ads/measurement mapping. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing auth. |
| 403 | forbidden | Publishable key origin is not allowed, or raw publishable-key auth was used instead of a browser activation token. |
| 400 | validation_error | Consent purposes are missing or malformed. |
| 409 | consent_state_conflict | A stale lifecycle revision cannot replace the current session state. |
| 409 | consent_state_invalid | The lifecycle transition or stored scope is invalid. |
## Response example
```json
{
"ok": true,
"consent_id": "consent_demo_123",
"session_id": "sess_demo_123",
"expires_at": "2026-08-02T08:31:00.000Z",
"normalized": {
"source": "publisher_custom",
"purposes": {
"semantic_targeting": false,
"session_persistence": true,
"cross_session_persistence": false,
"prompt_shared": false
}
}
}
```
## Placement consent vs /v1/consent sync
Projects using wavebird_consent require a current stored consent record for the authenticated tenant, client_id, and session_id. Record the actual user decision through /v1/consent before placement; inline flags cannot replace or expand that permission.
Only synchronize a decision actually obtained for this session and its purposes. A documentation example is not consent. TCF and GPP inputs are currently rejected at admission with validation_error (400), including structurally valid strings, because an authoritative vendor, purpose, and jurisdiction policy mapping is not configured. Do not interpret field support as permission to deliver ads. An observed positive Sec-GPC signal blocks new advertising admission with privacy_signal_policy_required (403). Preserve the signal across backend requests and keep the primary application flow available; do not retry by removing the signal.
On withdrawal, synchronize lifecycle_state: withdrawn with a newer revision for the same authenticated project and session. Subsequent placements fail closed with consent_not_current (403). Replaying an older grant returns consent_state_conflict (409); inline placement flags cannot restore authorization. Missing, expired, invalid, foreign-scope or unavailable stored authorization also denies advertising while the primary application remains available.
| Consent path | Location | Purpose | Required before placement |
| --- | --- | --- | --- |
| Request-level placement consent | inside /v1/placements | per-request privacy and targeting signal | Does not replace required stored consent |
| Consent sync | /v1/consent | session-level publisher consent state | Required for wavebird_consent projects |
## Canonical /v1/consent sync body and compatibility aliases
This body illustrates a recorded decision. Replace its purpose flags with the actual session decision; never grant permission merely to make a placement succeed.
Compatibility aliases remain supported after the canonical form: source: "publisher" normalizes to publisher_custom, purposes.ads maps to semantic_targeting, and purposes.measurement maps to session_persistence.
```json
{
"client_id": "wbproj_...",
"session_id": "sess_...",
"decision": "custom",
"source": "publisher_custom",
"purposes": {
"semantic_targeting": false,
"session_persistence": true,
"cross_session_persistence": false,
"prompt_shared": false
}
}
```
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/consent \
-H "Authorization: Bearer sk_test_wavebird_demo_secret" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_demo_8jK42",
"session_id": "sess_demo_123",
"decision": "custom",
"source": "publisher_custom",
"purposes": {
"semantic_targeting": false,
"session_persistence": true,
"cross_session_persistence": false,
"prompt_shared": false
}
}'
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/consent", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
decision: "custom",
source: "publisher_custom",
purposes: {
semantic_targeting: false,
session_persistence: true,
cross_session_persistence: false,
prompt_shared: false
}
})
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
payload = json.loads("{\"client_id\":\"wbproj_demo_8jK42\",\"session_id\":\"sess_demo_123\",\"decision\":\"custom\",\"source\":\"publisher_custom\",\"purposes\":{\"semantic_targeting\":false,\"session_persistence\":true,\"cross_session_persistence\":false,\"prompt_shared\":false}}")
request = urllib.request.Request(
"https://api.wavebird.ai/v1/consent",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
# Get project config
Returns non-secret runtime configuration for a project.
Use this to inspect the server-stored defaults that jobs use. Secret values are never returned by this endpoint.
| Method | Path | Authentication | Stability |
| --- | --- | --- | --- |
| GET | /v1/projects/{client_id}/config | Secret key or browser activation token | stable |
## Request fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| client_id | string | Yes | Project ID (client_id, WAVEBIRD_CLIENT_ID), formatted like wbproj_.... |
## Response fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| client_id | string | Yes | Project ID (client_id, WAVEBIRD_CLIENT_ID), formatted like wbproj_.... |
| defaults | object | Yes | Public runtime defaults applied to jobs. |
| allowed_origins | string[] | No | Origins configured for publishable-key browser flows. |
| request_id | string | Yes | Support identifier. |
## Errors
| Status | Code | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing or inactive secret key or browser activation token. |
| 403 | forbidden | Key does not belong to this project. |
| 404 | not_found | Project was not found. |
## Response example
```json
{
"client_id": "wbproj_demo_8jK42",
"defaults": {
"formats": ["banner", "native"],
"decision_delivery": "polling"
},
"allowed_origins": ["https://publisher.example"],
"request_id": "req_jP5bb2"
}
```
## cURL
```bash
curl "https://api.wavebird.ai/v1/projects/wbproj_demo_8jK42/config" \
-H "Authorization: Bearer sk_test_wavebird_demo_secret"
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/projects/wbproj_demo_8jK42/config", {
headers: { Authorization: "Bearer sk_test_wavebird_demo_secret" }
});
console.log(await response.json());
```
## Python
```python
import urllib.request
request = urllib.request.Request(
"https://api.wavebird.ai/v1/projects/wbproj_demo_8jK42/config",
headers={"Authorization": "Bearer sk_test_wavebird_demo_secret"},
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
## API documentation
# Testing
Verify a placement from request to rendered ad, including consent, no-fill, and measurement. Start with test credentials, then check production dry-run before enabling billable traffic.
## Environments and credentials
### Test
Use a Server Test Key (`sk_test_...`) for integration checks. Test traffic is non-billable, including validation and guardrail diagnostics.
### Production dry-run
Use a Server Production Dry-run Key (`sk_dry_...`). Dry-run is selected by credential class, remains non-billable, and requires no payout setup.
### Live boundary
Before sending billable traffic, complete payout setup and activate live delivery for your project in Dashboard. Billable impressions require verified delivery records.
## Minimum validation path
- [Key classes and rotation](/api/reference/authentication)
### Check credentials and project
The server key and browser publishable key must belong to the same Test project. Use its Project ID as `client_id` or `WAVEBIRD_CLIENT_ID`. Copy server secrets when created or rotated; masked previews cannot authenticate requests.
### Exercise no-fill and fill
A filled response should render the selected format. No-fill should leave no empty ad container and must not interrupt the AI task. Use the controlled no-fill request below to test this branch.
### Exercise optional fields
Test `prompt`, `slot_hint`, request-level `consent`, and advanced `overrides` separately. Validation errors include `request_id` and field paths such as `slot_hint.max_width`.
### Confirm beacons
Check rendered and completion beacons for the formats you actually ship. Billing evidence depends on that path, not just on placement creation.
## Deterministic Test no-fill
### Trigger one controlled no-fill
Call `/v1/placements?wait_ms=1500&test_outcome=no_fill` with the same valid body you use for a normal Test request. The response is successful and processed, but returns `decision.fill: false`, `placement: null`, and `test_controlled_no_fill` as its no-fill reason.
### Keep it outside Production
The control is accepted only with a Server Test Key and remains non-billable. Production, production dry-run, and browser keys cannot use it. Remove the query parameter for ordinary fill testing.
## Browser-specific checks
Verify activation against the expected origin, validate consent-required behavior, and confirm that the browser receives only the runtime-safe project config for the active `client_id`.
## Expected negative tests
### Beacon validation failures
Intentionally missing `asset_token`, missing `slot_id`, invalid `event`, or stale `occurred_at` requests should return actionable `400` validation errors in sandbox tests.
A stale timestamp can return `BEACON_TOO_LATE`. Generate a fresh timestamp for every direct server beacon with `new Date().toISOString()`.
### Consent validation failures
Invalid consent purposes should return field-level validation errors. These are expected negative tests when deliberately triggered and should not be treated as production integration failures.
### Hosted renderer first
Default integrations should rely on hosted renderer beacons sent to `/public/wrapper/v1/beacons`. Direct server `/v1/beacons` is an advanced QA or custom-rendering path.
### Direct beacon token handling
Direct server beacons require the sensitive `asset_token` from `placement.asset_token` or `decision.asset_token`. Redact it from logs, screenshots, reports, and client-visible debug output.
## Renderer proof diagnostics
Hosted renderer beacons are accepted for sandbox QA even when they are not proof-eligible or billable. Dashboard logs may expose safe diagnostic fields such as `proof_source=public_wrapper_renderer`, `proof_eligible=false`, `billable=false`, `non_billable_reason`, `geometry_reason`, `renderer_diagnostic_case`, `contract_exists=true`, and `renderer_geometry_summary`.
| Case | Expected reason | Use in QA |
| --- | --- | --- |
| `tiny` | `INSUFFICIENT_VISIBLE_AREA` | Positive but too-small visible geometry, such as a `1x1` diagnostic slot. |
| `clipped_8x1_25` | `INSUFFICIENT_VISIBLE_AREA` | A clipped renderer row with visible/intersection geometry below the paid contract. |
| `hidden` or `opacity_zero` | `HIDDEN_OR_NOT_VIEWABLE` | CSS-hidden or transparent renderer evidence that must remain non-billable. |
| `offscreen` | `OFFSCREEN` | Renderer evidence outside the viewport or intersection path. |
## Production dry-run contract
### Use the dry-run server key
Production dry-run is a public pre-live validation mode selected by credential class, not by a request-body flag. Create a Server Production Dry-run Key in Dashboard API Keys, or request one from your wavebird contact. Set its `sk_dry_...` value as `WAVEBIRD_SECRET_KEY` for normal server-side `/v1/placements` calls. Do not send undocumented `production_dry_run`, `production_billing_dry_run`, `billing_suppressed`, or `production_live_approved` fields in request bodies; those names are response, log, or dashboard diagnostics only.
### Check project activation
If live delivery is not activated for your project, use a Server Test Key to validate the integration. Use a Production Dry-run Key when it is available in Dashboard; it verifies the delivery path without billing.
### Expect safe blocked responses
A dry-run placement with no active SSP readiness should fail closed with `409 ssp_not_ready`. The expected response/log fields include `mode=production-dry-run`, `production_dry_run=true`, `production_billing_dry_run=true`, `billing_suppressed=true`, `production_live_approved=false`, `live_billing=false`, and `billable=false`.
### Check renderer evidence
Qualifying hosted renderer proof in dry-run should report `mode=production-dry-run`, `PRODUCTION_DRY_RUN_NON_BILLABLE`, `production_dry_run=true`, `production_live_approved=false`, `billing_suppressed=true`, `dry_run_billable_suppressed=true`, and `proof_would_be_eligible=true`.
### Live billing stays separate
Use test or dry-run credentials for these checks. Do not click real ads for testing. A successful dry-run verifies delivery, not live billing. The mode is `production-dry-run`, not `production-live-approved`.
# Concepts
Use the SDK as an app-facing jobs-and-decisions flow: create a Job, run the Generation and sponsor Slots in parallel, then receive a Decision and send Beacons.
Understand the core concepts behind the wavebird SDK: Job, Generation, Slot, Decision, Beacon, and Proof Pack.
## When does the ad appear?
Your application code controls when createJob() runs relative to your model call. The SDK does not enforce timing, but it exposes resolveAdTimingPlan() for before, while, and after inference orchestration.
### Before inference
Prepare a placement before the user starts a generation. Start the model when requested; do not make it wait for ad delivery.
### During inference
Start createJob() and the model call in parallel.
### After inference
Wait for the model response, then call createJob() and show the ad afterwards.
## Core concepts
### Job
A Job brackets the AI request, sponsor slots, and proof trail.
### Generation
A Generation is the AI model request within a Job.
### Slot and Decision
A Slot is a single ad placement; a Decision is the configured ad path's answer to that Slot: Fill or No-Fill.
### Beacon
A Beacon is a measurement event after an ad has rendered.
### Proof Pack
A Proof Pack is a signed evidence package for independent invoice verification.
# Consent and privacy signals
Projects using wavebird_consent require a current stored consent record for the authenticated tenant, client_id, and session_id. Record the actual user decision through /v1/consent before placement; inline flags cannot replace or expand that permission. An observed positive Sec-GPC signal blocks new advertising admission with privacy_signal_policy_required (403). Preserve the signal across backend requests and keep the primary application flow available; do not retry by removing the signal.
Understand stored session consent, observed GPC, and the currently disabled TCF/GPP admission path.
## Consent paths
### Built-in consent collection
The browser widget can render an inline consent card before the ad when project jurisdiction requires consent collection. It is driven by project settings and jurisdiction overrides. The consent widget contract can change; review integration changes when upgrading.
### Passing GDPR consent
TCF and GPP inputs are currently rejected at admission with validation_error (400), including structurally valid strings, because an authoritative vendor, purpose, and jurisdiction policy mapping is not configured. Do not interpret field support as permission to deliver ads.
### US privacy
GPP fields are represented in the contract but are not currently an enabled admission path. TCF and GPP inputs are currently rejected at admission with validation_error (400), including structurally valid strings, because an authoritative vendor, purpose, and jurisdiction policy mapping is not configured. Do not interpret field support as permission to deliver ads.
### Prompt sharing
If your application supplies a prompt, it reaches the Wavebird API. prompt_shared controls its use for targeting; omit the prompt when it should not be transmitted. The standard SSP request builders use the validated partner signal projection rather than forwarding the prompt body.
# Troubleshooting
Start with key class, request shape, decision state, rendering, and beacon delivery. Keep the API runtime contract as the canonical reference when diagnosing SDK behavior.
Troubleshoot an advanced wavebird SDK integration.
## Diagnostic order
- Check the key class and active environment.
- Inspect createJob() and getDecision() results, including no-fill.
- Attach options.onError for fail-silent SDK diagnostics.
- Validate rendering and the required beacon sequence.
- [Read API troubleshooting](/api/troubleshooting)
- [Read SDK testing](/sdk/testing)
# Testing your integration
Onboarding gives you a sandbox test key and a live key. Use the sandbox path first so you can validate the full request, decision, widget, and beacon flow without touching live SSP traffic.
Use sandbox test keys, live cutover, and onError-based observability to validate a wavebird SDK integration safely.
## Test keys
Use sandbox keys for local development, CI/CD pipelines, integration tests, and demo environments.
- Sandbox keys use the prefix sk_test_....
- Eligible Test requests use synthetic creatives and can return pending or no-fill; admission gates still apply.
- They do not send live SSP traffic.
- They do not generate billable revenue.
## Switching to live
After validating your integration, activate live delivery for your project and replace the sandbox key with your live secret key sk_live_.... Complete payout setup before sending billable traffic.
## Observability during integration
The SDK is fail-silent by design and does not spam your console automatically. During integration, attach an onError handler and opt into logLevel or a custom logger so transport, fallback, and contract issues are visible in your own telemetry.
## OBSERVE
```typescript
new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getApiKey: () => "sk_test_...",
options: {
logLevel: "debug",
onError: (error) => {
console.error(error.code, error.message, error.cause);
},
logger: {
debug: (message, meta) => console.debug(message, meta),
info: (message, meta) => console.info(message, meta),
warn: (message, meta) => console.warn(message, meta),
error: (message, meta) => console.error(message, meta),
},
},
});
```
## Abort and offline behavior
The widget flushes visible_ended when the page becomes hidden, the tab unloads, or the browser goes offline. If you wire browser-side beacon delivery through the SDK client, beacon requests use unload-safe keepalive delivery where the platform allows it.
## Human verification and IVT signals
The browser SDK can attach device- and interaction-level verification signals to createJob(), including a stable fingerprint_hint, viewport/screen data, navigator.webdriver, and trusted pointer/keyboard/touch counts. For abuse-sensitive wrappers, enable humanVerification.mode = "interaction_required" so browser-side createJob() calls wait for a recent trusted user interaction before requesting ad inventory.
## Verification
wavebird uses its own beacon-based proof system for impression tracking. Third-party verification (MOAT, IAS, DoubleVerify, OM SDK) is not yet supported. If you need third-party verification for compliance or campaign requirements, contact support@wavebird.ai before launch.
- [Troubleshooting](/sdk/troubleshooting)
- [SDK Quickstart](/sdk/quickstart)
# SDK Server Integration
Direct Node/server usage of `wavebird` and `WavebirdClient` for job creation, decision reads, generation reporting, and beacon sending.
Advanced SDK pattern: SDK Server Integration.
## When it fits
Use it when you want the released package surface with typed lifecycle methods and direct control over ad requests and beacons.
- `wavebird`
- `createJob()`
- `getDecision()`
- `reportGeneration()`
- `sendBeacon()`
## Important
Use hosted API docs instead when you want wavebird to own more of the rendering flow.
## ADVANCED
```typescript
import { WavebirdClient } from "wavebird";
const client = new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getApiKey: () => process.env.WAVEBIRD_SECRET_KEY ?? "",
publisher: {
app_name: "MyAIChatApp",
app_domain: "mychatapp.com",
categories: ["IAB19"],
},
decisionDelivery: "polling",
options: {
wrapper_version: "wavebird-wrapper/2026.03",
},
});
const job = await client.createJob({
job_type: "chat",
context: {
topic: "programming",
},
});
if (job?.slot_ids[0]) {
// D02_GENERATION_AUTHORIZATION_GAP: use only synthetic single-tenant local
// data until immutable target-job ownership is enforced.
await client.reportGeneration(job.job_id, "started", {
generation_id: `gen_${job.job_id}`,
model_id: "gpt-4o-mini",
});
const decision = await client.getDecision(job.slot_ids[0]);
if (decision?.fill === true) {
await client.sendBeacon({
beacon_id: `rendered-${Date.now()}`,
asset_token: decision.asset_token,
beacon_type: "rendered",
occurred_at_ms_client: Date.now(),
});
}
await client.reportGeneration(job.job_id, "finished", {
generation_id: `gen_${job.job_id}`,
model_id: "gpt-4o-mini",
});
}
```
# SDK Browser Integration
Direct browser usage of `wavebird/browser` with the same `WavebirdClient` surface.
Advanced SDK pattern: SDK Browser Integration.
## When it fits
It fits when you explicitly need browser-side package orchestration with a publishable key and activation handshake, rather than the Script Tag or hosted-renderer path.
- `wavebird/browser`
- `decisionDelivery: "auto"`
- `getPublishableKey()` or `publishableKey`
- allowed origins configured in the dashboard
- job creation and decision retrieval in browser code
## Important
Direct browser SDK orchestration is development-only until its consent-provider contract is approved. Use the Server API and hosted renderer for the recommended path. Asset tokens are not consent assertions. Browser package usage relies on publishable-key activation and allowed-origin checks. Secret keys remain server-only, and new browser work should prefer the Script Tag unless you explicitly need the package surface.
## BROWSER
```typescript
import { WavebirdClient } from "wavebird/browser";
const client = new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getPublishableKey: async () => (await fetch("/api/wavebird/publishable-key")).text(),
publisher: {
app_name: "MyAIChatApp",
app_domain: "mychatapp.com",
categories: ["IAB19"],
},
decisionDelivery: "auto",
});
// D01_BROWSER_CONSENT_GAP: browser examples are local/development-only until
// an approved browser-side consent provider contract is implemented. Asset
// tokens are not consent assertions.
const job = await client.createJob({
job_type: "chat",
});
const decision = job?.slot_ids[0] ? await client.getDecision(job.slot_ids[0]) : null;
console.log(decision);
```
# Callback delivery
A server-side path where wavebird posts the decision to a `callback_url`.
Advanced SDK pattern: Callback delivery.
## When it fits
It fits apps that want signed callback-style decision delivery and control over callback handling on the server side.
- `decisionDelivery: "callback"`
- `callback_url` in `createJob()`
- `x-csl-signature` at the callback boundary
## Important
A dedicated SDK verifier helper is not provided; signature verification remains integration-side.
## CALLBACK
```typescript
import { WavebirdClient } from "wavebird";
const client = new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getApiKey: () => process.env.WAVEBIRD_SECRET_KEY ?? "",
publisher: {
app_name: "MyAIChatApp",
app_domain: "mychatapp.com",
categories: ["IAB19"],
},
decisionDelivery: "callback",
options: {
wrapper_version: "wavebird-wrapper/2026.03",
},
});
await client.createJob({
job_type: "chat",
callback_url: "https://wrapper.example.com/csl/callback",
});
```
# Proxy compatibility
An HTTP/API path where the app sends OpenAI-compatible or Anthropic-compatible requests directly to wavebird proxy routes.
Advanced SDK pattern: Proxy compatibility.
## When it fits
It fits systems that already emit upstream-shaped HTTP requests and do not want to start with `WavebirdClient`.
- `/v1/proxy/openai/v1/chat/completions`
- `/v1/proxy/anthropic/v1/messages`
- direct HTTP instead of `WavebirdClient`
## Important
Proxy compatibility is real, but it is separate from the `WavebirdClient` method surface. React Native and Electron are not part of the current official example surface.
# Main client surface
This section documents the frozen, released package surface that integrators can use today.
Reference index for the released advanced wavebird SDK surface.
### WavebirdClient
`WavebirdClient` is the wrapper-facing entry point for the SDK server and browser package paths. It carries the core paths for jobs, decisions, generation events, and beacons.
[Read more](/sdk/reference/wavebird-client)
### Types
Released SDK request, response, and supporting types.
[Read more](/sdk/reference/types)
### Routes
These compatibility routes describe the public wrapper surface behind the direct `WavebirdClient` integration. New Server API examples should start with `/v1/placements` instead.
[Read more](/sdk/reference/routes)
# Public API boundary
These compatibility routes describe the public wrapper surface behind the direct `WavebirdClient` integration. New Server API examples should start with `/v1/placements` instead.
SDK route reference for the released wavebird package surface.
This page is limited to the public wrapper routes used by `WavebirdClient`. They remain documented for SDK and migration compatibility, but they are not the shortest Server API flow.
| Method | Path | Note |
| --- | --- | --- |
| POST | /public/wrapper/v1/jobs | `createJob()` submits wrapper jobs here. Supplying `callback_url` opts the job into callback delivery. |
| GET | /public/wrapper/v1/slots/{slot_id}/decision | Polling decision retrieval path used by `getDecision()` when polling is selected or used as fallback. |
| GET | /public/wrapper/v1/slots/{slot_id}/decision/ws | Explicit WebSocket decision-delivery path. The SDK falls back to polling on connection, parse, close, or timeout failures. |
| POST | /public/wrapper/v1/jobs/{job_id}/generation | `reportGeneration()` reports `started`, `finished`, and `failed` lifecycle events here. Generation lifecycle mutations require tenant-bound authorization; request-provided job and generation identifiers are not sufficient authority. |
| POST | /public/wrapper/v1/beacons | `sendBeacon()` submits `rendered`, `visible_started`, `visible_ended`, `clicked`, and related public wrapper beacon types here. |
# SDK types
These type groups describe the released public SDK package surface.
Released public types for the advanced wavebird SDK surface.
## Package and client types
### WavebirdClientOptions
Constructor options for `WavebirdClient`, including `baseUrl`, server `getApiKey`, browser `getPublishableKey`, delivery mode, timing, `onError`, and `wrapper_version`.
### DecisionDeliveryMode
The four supported delivery modes: `auto`, `websocket`, `polling`, and `callback`.
### WavebirdSdkError
Structured SDK error object emitted through `options.onError` when transport, parsing, timeout, or internal failures are observed.
### WavebirdSdkErrorCode
Machine-readable error-code enum for HTTP, network, timeout, WebSocket, parse, and internal failures.
## Jobs and decisions
### ConsentFlags
Request-level flags used by `createJob()`; they do not replace required stored session consent.
### JobRequest
Create-job request shape. After the project's stored session consent requirement is met, start with `job_type` and add optional context, slot configuration, brand safety, routing, or callback settings as needed.
### JobResponse
Union of accepted job metadata and the typed `rate_limit_exceeded` result returned by `createJob()`.
### DecisionResponse
Normalized decision union for `pending`, `ready` no-fill, and `ready` fill. Fill preserves `creative`, `asset_token`, `constraints`, and `cs_declaration`.
## Lifecycle and contracts
### GenerationEvent
`"started" | "finished" | "failed"`. This type sits behind `reportGeneration()`.
### GenerationRequest
Payload for generation lifecycle reporting. Usage is limited to model id, token counts, and compute time; failure details use a bounded machine-readable code.
### BeaconRequest
Payload for wrapper beacon events. Optional measurements are runtime-validated against a fixed, non-content renderer-proof vocabulary.
### BeaconResponse
Acknowledgement response for `sendBeacon()`. It returns `{ accepted, reason_code }`.
# `WavebirdClient`
`WavebirdClient` is the wrapper-facing entry point for the SDK server and browser package paths. It carries the core paths for jobs, decisions, generation events, and beacons.
WavebirdClient reference for the released advanced SDK package surface.
## Methods
Public request methods are fail-silent by design. Use `options.onError` to observe structured `WavebirdSdkError` values and machine-readable `WavebirdSdkErrorCode` values.
### createJob(request: JobRequest): Promise
Creates a wrapper job and returns accepted job metadata, a typed rate-limit result, or `null` on fail-silent transport or contract failure.
Use `callback_url` inside `request` when the job should switch to callback delivery.
Transport or contract failures are observed through `options.onError`, not thrown as hard failures.
### getDecision(slotId: string, options?: { wait_ms?: number }): Promise
Returns a normalized `DecisionResponse` union. Ready decisions preserve `constraints` and `cs_declaration`.
When `decisionDelivery` is `"auto"`, the SDK prefers WebSocket where available and falls back to polling.
### reportGeneration(jobId: string, event: GenerationEvent, request?: GenerationRequest): Promise
Reports `started`, `finished`, or `failed` generation events to the public wrapper API. Multi-tenant production lifecycle mutation requires an approved tenant-bound authorization check; do not treat a caller-supplied job ID as permission.
Use the same `jobId` for every lifecycle update tied to one wrapper job.
### sendBeacon(request: BeaconRequest): Promise
Sends wrapper beacons such as `rendered`, `visible_started`, `visible_ended`, `clicked`, and related public beacon types.
Send beacons only after a fill decision because `asset_token` comes from the filled response.
Revenue requires the full beacon sequence, not just `rendered`: banner and native use `rendered` -> `visible_started` -> `visible_ended`, while clip uses `rendered` -> `play_started` -> `play_completed`.
## createJob fields
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| job_type | string | yes | Type of job, for example `"chat"`. |
| slots_requested | number | no | Number of sponsoring slots. |
| model_id | string | no | Optional model identifier. |
| locale | string | no | Optional locale code, for example `"en-US"`. |
| context.topic | string | no | Optional context hint for matching without sharing prompt text. |
| prompt | { text?: string; token_count_estimate?: number } \| string | no | Optional prompt context when your integration chooses to send it. |
| consent | ConsentFlags | no | Request privacy flags; current stored session consent may still be required by the project. |
| slot_config | { allowed_formats?: ("banner" \| "clip" \| "native")[]; max_width?: number; max_height?: number; position_hint?: "above" \| "below" \| "sidebar" \| "between"; bidfloor?: number; bidfloorcur?: string } | no | Optional slot and bidfloor hints. |
| brand_safety | { blocked_categories?: string[]; blocked_domains?: string[] } | no | Optional category and advertiser blocking controls. |
## Error handling
Public methods stay fail-silent. Observe SDK failures through `options.onError`, then handle `null` or incomplete results in your integration code.
`WavebirdSdkError` is the structured error object surfaced through `options.onError`.
`WavebirdSdkErrorCode` is the machine-readable companion for transport, timeout, parse, WebSocket, HTTP, and internal failure categories.
Treat `null` from `createJob()` as a failed handoff and keep your app path moving without assuming a sponsored slot exists.
# SDK integration flow
This is the advanced SDK lifecycle around job creation, decision delivery, generation reporting, and beacons. The default API lifecycle is shorter and lives under /api.
Understand the current integration flow around jobs, decisions, generation reporting, and beacons.
### 1. Import and initialize `WavebirdClient`
Initialize the client with `baseUrl`, server `getApiKey` or browser `getPublishableKey`, and optionally `decisionDelivery` plus timing, `wrapper_version`, and error observation options.
### 2. Create a job
After the required stored session permission is current, call `createJob({ job_type: "chat" })`. Context, brand safety, and slot hints are optional. An accepted response returns `job_id` and `slot_ids`; rejected admission is not a fill.
### 3. Report generation started
Report model generation start through `reportGeneration(jobId, "started", request)`.
### 4. Retrieve the decision
Retrieve the sponsor decision for the first slot through `getDecision(slotId)` using `auto`, `websocket`, `polling`, or callback-driven delivery.
### 5. Continue and finish generation
Continue model generation and then report `finished` or, if needed, `failed`.
### 6. Render fill or handle no-fill
On fill, render `creative`, respect `constraints`, and use `asset_token`. The filled creative may also include `click_through_url` and `sponsor_name`. On no-fill, react to `reason` and `cs_declaration`.
### 7. Send beacons after fill
Send wrapper beacon events such as `rendered`, `visible_started`, `visible_ended`, or `clicked` through `sendBeacon()` only with current permission. A complete creative-specific sequence supplies delivery evidence; it does not by itself establish billable revenue, settlement, or a payout.
## What goes into `createJob()`
The job payload goes to the wavebird runtime. An optional prompt must be permitted by the integration and consent settings. Standard SSP builders use the [[data firewall|/glossary/data-firewall]] signal projection rather than the raw prompt body. This boundary does not cover separate model requests, custom adapters or application logs. Placement, transaction and regulatory fields are checked separately.
- `job_type` is the only required field. `slots_requested` defaults to `1`.
- `prompt` is optional and accepts either a string or an object with `text` and an optional `token_count_estimate`.
- `context.topic` is the lightest way to provide matching context without sharing prompt text.
- `consent` flags do not replace the current stored session permission required by wavebird_consent projects. TCF/GPP admission remains disabled until an authoritative policy mapping exists. Preserve observed positive Sec-GPC across server hops; keep the app running without ads when admission is denied.
- `brand_safety` and `slot_config` are optional expansion surfaces for blocking and placement control.
- `decisionDelivery` can be `auto`, `websocket`, `polling`, or `callback` at the client level.
- `callback_url` opts a job into callback delivery even when the client-level mode is not `"callback"`.
- Optional fields also include publisher metadata, latency hints, client identifiers, routing hints, and callback settings for advanced delivery flows.
## What comes back
- From `createJob()` you get accepted job metadata, a typed `rate_limit_exceeded` result, or `null` when the SDK falls back silently.
- From `getDecision()` you get pending or ready decisions, preserving `constraints` and `cs_declaration`.
- From `sendBeacon()` you get `{ accepted, reason_code }`. `reportGeneration()` always resolves.
## WRAPPER FLOW
```typescript
// D02_GENERATION_AUTHORIZATION_GAP: use only synthetic single-tenant local
// data until immutable target-job ownership is enforced.
await client.reportGeneration(job.job_id, "started", {
generation_id: `gen_${job.job_id}`,
model_id: "gpt-4o-mini",
});
const slotId = job?.slot_ids[0];
const decision = slotId ? await client.getDecision(slotId) : null;
if (decision?.fill === true) {
await client.sendBeacon({
beacon_id: `rendered-${Date.now()}`,
asset_token: decision.asset_token,
beacon_type: "rendered",
occurred_at_ms_client: Date.now(),
});
}
await client.reportGeneration(job.job_id, "finished", {
generation_id: `gen_${job.job_id}`,
model_id: "gpt-4o-mini",
});
```
# Integration patterns
The SDK supports multiple real integration styles. The order below is for advanced package users only; new integrations should still start with /api/quickstart or /api/patterns/server-api.
Choose the right advanced wavebird SDK package path after the Server API default is clear.
## Choose your path
### SDK Server Integration
Direct Node/server usage of `wavebird` and `WavebirdClient` for job creation, decision reads, generation reporting, and beacon sending.
Use it when you want the released package surface with typed lifecycle methods and direct control over ad requests and beacons.
`wavebird`
`createJob()`
`getDecision()`
`reportGeneration()`
`sendBeacon()`
[Read more](/sdk/patterns/node)
### SDK Browser Integration
Direct browser usage of `wavebird/browser` with the same `WavebirdClient` surface.
It fits when you explicitly need browser-side package orchestration with a publishable key and activation handshake, rather than the Script Tag or hosted-renderer path.
`wavebird/browser`
`decisionDelivery: "auto"`
`getPublishableKey()` or `publishableKey`
allowed origins configured in the dashboard
job creation and decision retrieval in browser code
[Read more](/sdk/patterns/browser)
### Callback delivery
A server-side path where wavebird posts the decision to a `callback_url`.
It fits apps that want signed callback-style decision delivery and control over callback handling on the server side.
`decisionDelivery: "callback"`
`callback_url` in `createJob()`
`x-csl-signature` at the callback boundary
[Read more](/sdk/patterns/callback)
### Proxy compatibility
An HTTP/API path where the app sends OpenAI-compatible or Anthropic-compatible requests directly to wavebird proxy routes.
It fits systems that already emit upstream-shaped HTTP requests and do not want to start with `WavebirdClient`.
`/v1/proxy/openai/v1/chat/completions`
`/v1/proxy/anthropic/v1/messages`
direct HTTP instead of `WavebirdClient`
[Read more](/sdk/patterns/proxy)
## Read this section in context
If you are new to wavebird, compare the hosted API path and the SDK package path first. Stay here when you already chose the package layer on purpose and the SDK package layer is the right fit.
- [Open API quickstart](/api/quickstart)
- [Open SDK quickstart](/sdk/quickstart)
- [Review Integration flow](/sdk/integration-flow)
- [Check Configuration](/sdk/configuration)
- [Open WavebirdClient reference](/sdk/reference/wavebird-client)
# Developer FAQ
The answers below stay close to the released SDK surface and the documented evidence behind it.
Read the current developer FAQ for wavebird, including API-first guidance, keys, rendering, callbacks, and migration behavior.
## What is live today?
`wavebird` is available with Dashboard signup, key management, and a self-serve sandbox. Start with a Test Key, configure consent and allowed origins, then complete payout setup and activate live delivery for your project.
## Which integration path should I start with?
If you are still choosing an integration model, start with the API docs first. If you are intentionally using the SDK, start with SDK Server Integration plus the SDK integration flow.
## Does the SDK run in Node or in the browser?
Both. Use `wavebird` in Node/server code with a dashboard secret key, and `wavebird/browser` in browser code with a dashboard publishable key. Browser usage is publishable-key based and requires activation plus allowed-origin checks before runtime calls. React Native and Electron are not part of the current official support or example surface.
## What data do I pass into `createJob()`?
The SDK type requires `job_type`; `slots_requested` defaults to `1`. Runtime admission also enforces project, session, stored consent, and privacy-signal boundaries. Optional context or inline consent flags do not replace these checks.
## Do I need callback verification logic myself today?
If you use the callback path, verification is handled in integration code today. A dedicated public SDK helper is not documented.
## Are proxy-compatible routes part of `WavebirdClient`?
No. Proxy compatibility is a direct HTTP integration pattern for OpenAI-compatible and Anthropic-compatible request shapes. It is not part of the `WavebirdClient` method family.
## How do errors surface?
Public methods stay fail-silent. Use `options.onError` to observe structured `WavebirdSdkError` values and inspect machine-readable `WavebirdSdkErrorCode` values.
### Need help diagnosing no-fill?
Start with the troubleshooting decision tree for key, request, widget, and no-fill checks.
[Read more](/sdk/troubleshooting)
### Testing and sandbox
Use sk_test_... keys to validate the full integration path with mock creatives and no billing.
[Read more](/sdk/testing)
# Configuration reference
The reference below documents the released constructor and payload fields that are safe to explain publicly today. Secret keys belong only in server environments; browser integrations use publishable keys with allowed origins configured in the dashboard.
Configuration reference for the released wavebird SDK constructor and request payload fields.
## `WavebirdClientOptions`
Every SDK HTTP request sends `x-csl-wrapper-version: ` as a wire-protocol header. `decisionDelivery` defaults to `"auto"`, and timing values are clamped to safe ranges.
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| baseUrl | string | yes | none | Base wavebird API URL. A trailing slash is normalized away. |
| getApiKey | () => string \| Promise | server only | none | Server-side secret key resolver for wrapper-authenticated requests. Use a sandbox `sk_test_...` key first, then switch to a live key after live delivery is activated for your project. |
| getPublishableKey | () => string \| Promise | browser only | none | Browser-safe publishable key resolver. The SDK exchanges this for an activation token after the request origin matches the allowed origins configured in the dashboard. |
| publishableKey | string | browser only | none | Static browser-safe publishable key alternative to `getPublishableKey`. Use only publishable keys in browser code. |
| decisionDelivery | "auto" \| "websocket" \| "polling" \| "callback" | no | "auto" | Decision delivery strategy for the client. |
| publisher | { app_name?: string; app_domain?: string; app_bundle?: string; categories?: string[] } | no | none | Optional publisher metadata automatically merged into every `createJob()` request. |
| options.timeout_ms | number | no | 2000 | Base request timeout. Values are clamped to the documented safe range. |
| options.decision_timeout_ms | number | no | 30000 | Decision wait budget. Values are clamped to the documented safe range. |
| options.long_poll_wait_ms | number | no | 1500 | Long-poll `wait_ms` used by decision reads. |
| options.short_poll_interval_ms | number | no | 250 | Short-poll interval used after long-poll attempts are exhausted. |
| options.onError | (error: WavebirdSdkError) => void | no | none | Observation hook for structured SDK errors surfaced through `onError` rather than thrown as hard failures. |
| options.wrapper_version | string | no | "sdk" | Optional wrapper version label. Sent as `x-csl-wrapper-version` on every SDK request. |
## `JobRequest`
`job_type` is required and `slots_requested` defaults to `1`. When `decisionDelivery: "callback"` is used without `callback_url`, `createJob()` returns `null` and `onError` receives `WavebirdSdkError` with code `sdk_internal`.
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| job_type | string | yes | none | App job type. |
| slots_requested | number | no | 1 | Number of requested slots. |
| model_id | string | no | "unknown" | Optional model identifier for the job. |
| locale | string | no | "en" | Optional app locale. |
| predicted_latency_ms | number | no | none | Optional latency hint. |
| client_id | string | no | runtime-derived if omitted | Optional client id. |
| chat_session_id | string | no | none | Optional chat session id. |
| prompt | { text?: string; token_count_estimate?: number } \| string | no | none | Optional prompt payload. Supplying it transmits text to the Wavebird API; targeting requires the applicable permissions. Standard SSP builders use the [[data firewall\|/glossary/data-firewall]] signal projection rather than forwarding the raw prompt body. This is not a guarantee about separate model requests, custom adapters or application logs. |
| context.topic | string | no | none | Optional topic hint when you do not want to share prompt text. |
| context.prompt_text | string | no | none | Optional prompt alias for integrations that separate explicit context from prompt capture. |
| consent | ConsentFlags | no | server defaults | Request-level privacy flags. Required stored consent remains authoritative; TCF/GPP is currently rejected at admission, and an observed positive GPC blocks new advertising. |
| publisher | { app_name?: string; app_domain?: string; app_bundle?: string; categories?: string[] } | no | constructor defaults if configured | Optional publisher metadata automatically merged from the client constructor when configured. |
| slot_config | { allowed_formats?: ("banner" \| "clip" \| "native")[]; max_width?: number; max_height?: number; position_hint?: string; bidfloor?: number; bidfloorcur?: string } | no | none | Optional format, size, placement, and bidfloor hints. |
| brand_safety | { blocked_categories?: string[]; blocked_domains?: string[] } | no | none | Optional blocking controls for categories and advertiser domains. |
| ssp_partner_id | string | no | none | Legacy single-partner selector for advanced routing cases. |
| routing.preferred_partner_id | string | no | none | Preferred routing hint for new integrations. |
| routing.candidate_partner_ids | string[] | no | none | Optional routing hint list for candidate partners. |
| callback_url | string | no | none | Callback target for callback delivery. |
## `GenerationRequest`
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| generation_id | string | no | none | Identifier for the model generation. |
| model_id | string | no | none | Reported model identifier. |
| usage_json | { input_tokens?: number; output_tokens?: number; total_tokens?: number; compute_ms?: number } | no | none | Bounded, non-content usage counters for `finished` or `failed`; unknown fields are rejected. |
| error | string | no | none | Machine-readable failure code (maximum 64 characters), not free error text. |
## `BeaconRequest`
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| beacon_id | string | yes | none | Idempotency input for the beacon. |
| asset_token | string | yes | none | Asset authenticator for fill beacons. |
| beacon_type | "rendered" \| "visible_started" \| "visible_ended" \| "heartbeat" \| "play_started" \| "play_completed" \| "clicked" | yes | none | Beacon type for render, visibility, playback, or click events. |
| occurred_at_ms_client | number | yes | none | Client timestamp for the event. |
| measurements | fixed renderer-proof fields | no | none | Optional numeric geometry, visibility/playback, boolean state, and bounded renderer fields; unknown or content-bearing fields are rejected. |
# SDK package quickstart
Use this page when you intentionally want package-level control. Create a wavebird account first, complete onboarding, and start with the sandbox secret key from your dashboard. Hosted defaults remain documented under /api/quickstart and /api/patterns/server-api. The SDK quickstart below uses the released `wavebird` surface.
Quickstart for the released wavebird SDK package surface.
Copy the code only after choosing the SDK path and storing your sandbox secret key server-side as `WAVEBIRD_SECRET_KEY`. Move to Integration flow, Configuration, and the `WavebirdClient` reference for deeper lifecycle and type details.
## NODE ENTRY
```typescript
import { WavebirdClient } from "wavebird";
const client = new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getApiKey: () => process.env.WAVEBIRD_SECRET_KEY ?? "",
publisher: {
app_name: "MyAIChatApp",
app_domain: "mychatapp.com",
categories: ["IAB19"],
},
decisionDelivery: "polling",
});
const job = await client.createJob({
job_type: "chat",
});
const decision = job?.slot_ids[0] ? await client.getDecision(job.slot_ids[0]) : null;
console.log(decision);
```
## PRIVACY
```typescript
// Requires current stored consent for this project and session.
// Use the actual recorded decision; inline flags do not grant consent.
const job = await client.createJob({
job_type: "chat",
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: true,
consent_source: "wavebird_consent",
},
});
```
## BRAND SAFETY
```typescript
const job = await client.createJob({
job_type: "chat",
brand_safety: {
blocked_categories: ["IAB25-3"],
blocked_domains: ["ads.bad.example"],
},
});
```
## CONTEXT
```typescript
const job = await client.createJob({
job_type: "chat",
context: {
topic: "programming",
},
});
```
## SLOT CONFIG
```typescript
const job = await client.createJob({
job_type: "chat",
slot_config: {
allowed_formats: ["banner"],
max_width: 728,
max_height: 90,
position_hint: "above",
bidfloor: 0.5,
bidfloorcur: "EUR",
},
});
```
# API v1 reference
API v1 defines the request and response contracts used by REST, Script Tag, and the SDK.
## Base URL
Use https://api.wavebird.ai for production requests. Endpoint paths begin with /v1. Local backend tests generally use http://127.0.0.1:3300; this is not a public service URL.
## Request and delivery paths
Use /v1/placements to request an ad and /v1/render.js to render a filled response. Browser activation supplies the token for browser-authorized requests. Jobs and decisions expose the advanced lifecycle.
- [Browse all endpoints](/api/reference/endpoints)
- [Make the first request](/api/quickstart)
## Compatibility and endpoint status
Check each endpoint for its authentication and stability requirements. An API version does not imply that every endpoint has the same stability status.
- [Endpoint status](/api/reference/stability)
- [Versioning policy](/api/reference/versioning)
- [Migration guide](/api/migration)
# Troubleshooting
Diagnose the API path in this order: key class, activation, client_id, decision response, brand-safety or consent gating, then beacon delivery. That order usually exposes the real fault faster than debugging renderer details first.
## Trace the REST path in six stages
- 1. Check authorization. A 401 unauthorized response means “Missing or inactive secret key.” A 403 forbidden response means a protected key, project, origin, or consent boundary rejected the request; use the documented reason code and hint.
- 2. Check the request body. client_id, session_id, and job_type are required. client_id must be the Project ID paired with the presented key. A 400 validation_error means “The request body failed validation.”
- 3. Check the content type and rate limit. A 415 unsupported_media_type means “Non-empty request bodies must use Content-Type: application/json.” A 429 rate_limited means “Key exceeded its rate limit; retry after the Retry-After header.”
- 4. Check the placement result. placement can be null; treat that as an optional sponsor-path outcome while the primary product flow continues.
- 5. Check rendering and beacons after a fill. A filled placement includes placement.render; use the hosted descriptor, then inspect the documented beacon path for rendered or clip-completion evidence.
- 6. Preserve the endpoint error and route. Record the status, documented error code, and whether the failure occurred before or after a placement response when escalating an integration issue.
### Why am I not seeing ads?
Check for no-fill first. Then validate whether activation, allowed origins, blocked categories, or missing consent are gating the sponsor path intentionally.
### Why is the browser path failing?
Confirm that you are using a publishable key, the active origin is allowed, and activation succeeded before calling protected runtime routes.
### Why is the backend path failing?
Check the secret key, the client_id, and whether your backend is calling the canonical /v1 surface rather than older compatibility routes.
### Why is billing missing?
Validate the beacon path. Rendering alone is not enough for billable completion. The missing evidence is usually in visibility or clip-completion events.
# Brand safety
Use brand_safety to block advertiser categories and domains per request. You can also configure dashboard defaults under Settings → Brand Safety.
Block advertiser categories and domains, and understand how request-level brand safety overrides dashboard defaults.
## Blocking categories
Common categories include gambling, tobacco, crypto, and adult. If you need a formal taxonomy, align these values to your IAB category mapping or internal policy list.
## CATEGORIES
```typescript
const job = await client.createJob({
job_type: "chat",
brand_safety: {
blocked_categories: ["gambling", "tobacco", "crypto", "adult"],
},
});
```
## Blocking specific advertisers
Block advertiser landing domains when a broad category block would be too aggressive.
## DOMAINS
```typescript
const job = await client.createJob({
job_type: "chat",
brand_safety: {
blocked_domains: ["competitor.com", "unwanted-brand.com"],
},
});
```
## Priority rules
Dashboard defaults apply to every request from your project. If you pass blocked_categories or blocked_domains in the SDK request, those request values override the dashboard defaults instead of merging with them.
## Ad formats
Your app can receive three ad formats. Banner is a fixed-dimension image creative. Clip is a short silent video. Native is a structured asset bundle containing title, image, description, CTA, and optional icon that the SDK renders to match your app's visual style.
By default, wrappers should pass allowed_formats: ["banner"] when they want banner-only behavior. Native assets can match the surrounding layout while remaining clearly labeled. Measure CTR and user experience in your own surface; no format guarantees better results.
## FORMATS
```typescript
const job = await client.createJob({
job_type: "chat",
slot_config: {
allowed_formats: ["banner", "native"],
},
});
```
- [Configuration](/sdk/configuration)
- [Troubleshooting](/sdk/troubleshooting)
# Privacy and GDPR
Projects using wavebird_consent require a current stored consent record for the authenticated tenant, client_id, and session_id. Record the actual user decision through /v1/consent before placement; inline flags cannot replace or expand that permission. Prompt text is optional; prefer controlled context.topic and keep prompt_shared false unless separately permitted.
Pass GDPR and privacy controls through the wavebird contract and understand what data leaves the system.
## Request shape after stored consent
- prompt_shared: false keeps prompt text out of the targeting path while still letting you send a direct context.topic.
- The request-level gdpr_applies flag does not authorize delivery or override stored consent.
- TCF and GPP inputs are currently rejected at admission with validation_error (400), including structurally valid strings, because an authoritative vendor, purpose, and jurisdiction policy mapping is not configured. Do not interpret field support as permission to deliver ads.
- An observed positive Sec-GPC signal blocks new advertising admission with privacy_signal_policy_required (403). Preserve the signal across backend requests and keep the primary application flow available; do not retry by removing the signal.
- If you enable the built-in wavebird consent widget, it may store wavebird_consent_v1 in the end user's browser on your domain to remember the local semantic-targeting choice. Disclose this in your own privacy and cookie notice and obtain any legally required consent or rely on an applicable strict-necessity exemption.
## PRIVACY
```typescript
const { slot_ids } = await client.createJob({
job_type: "chat",
context: { topic: "programming" },
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: true,
consent_source: "wavebird_consent",
},
});
```
### What can leave your system
The contract supports topic, consent, brand-safety, publisher and slot metadata. Supplying prompt text transmits it to the Wavebird API. prompt_shared: false blocks its use for targeting. Standard SSP builders project permitted signal fields rather than forwarding the raw prompt body; separate model requests, custom adapters and application logs require their own review.
[Read more](/glossary/data-firewall)
### What ad partners do not receive
The standard SSP request builders use a validated partner signal projection rather than the raw prompt body. This does not describe separate model-provider requests, custom adapters, or your own application logs. Omit prompt text to avoid transmitting it to the Wavebird API.
# Graceful degradation
The public SDK surface is fail-silent by design. Your integration should keep the core app path moving when a sponsored slot cannot be created, when a decision does not fill, or when delivery falls back between modes.
Understand how wavebird degrades when runtime delivery channels or SSP responses are unavailable.
## Basic fallback pattern
## FALLBACK
```typescript
const job = await client.createJob({ job_type: "chat" });
if (!job) {
// keep the app path moving without assuming a sponsored slot exists
return { decision: null };
}
const decision = await client.getDecision(job.slot_ids[0]);
if (!decision || decision.fill !== true) {
return { decision: null };
}
```
### Create-job failure
createJob() may resolve to null. Treat that as a failed handoff and continue without a sponsored placement.
### Delivery fallback
The SDK supports explicit polling, websocket, and callback delivery. The SDK-side auto path falls back when the preferred delivery channel is unavailable.
### No-fill behavior
A no-fill should leave your primary GenAI response untouched. The widget renders nothing for pending or no-fill decisions, and your app should treat that as a normal outcome.
# Disclosure
Disclosure guidance is maintained on the canonical API documentation page.
SDK alias for the canonical wavebird API disclosure guidance.
- [Read canonical API disclosure guidance](/api/disclosure)
# Privacy template
Privacy-template guidance is maintained on the canonical API documentation page.
SDK alias for the canonical wavebird API privacy-template guidance.
- [Read canonical API privacy template](/api/privacy-template)
# Script Tag
Use Script Tag when the browser should discover slots and handle activation, consent, rendering, and beacons. It uses a publishable key with origin-bound activation.
## BROWSER PATH
```html
```
## What Script Tag does for you
### Activation
Exchanges the publishable key for an origin-bound browser token before protected runtime calls.
### Config loading
Loads non-secret project config through client_id so the browser can render the right format and disclosure defaults.
### Consent
Can collect consent through the built-in flow before retrying the decision path.
### Beacons
Sends rendered and completion beacons automatically.
# Reference
The reference surface is organized around the public API first. Endpoints and runtime types are canonical. The SDK reference remains available as an advanced compatibility layer.
### Endpoints
Canonical /v1 routes, auth expectations, and runtime summaries.
[Read more](/api/reference/endpoints)
### Types
Script Tag attributes, methods, events, and other public runtime shape notes.
[Read more](/api/reference/types)
### SDK reference
Secondary SDK-specific packages and entrypoints.
[Read more](/api/reference/sdk)
# Versioning
wavebird docs currently describe the stable API v1 surface. The public base path is /v1, and the current canonical docs live at /api with stable reference aliases under /api/v1.
> **V1 launch policy** Breaking API changes should be announced in the changelog before partners depend on them. Compatibility aliases can remain available, but new snippets should use canonical /v1/* routes.
## Current version
Use https://api.wavebird.ai/v1/* for production requests. The API reference documents the canonical v1 endpoints: placements, hosted rendering, browser activation, advanced jobs and decisions, beacons, consent, and project config.
## Compatibility routes
Script Tag and SDK compatibility routes may continue to exist for older integrations. They are not the preferred public docs surface for new partners.
## Future versions
There is no version selector in v1. Add one when a future /v2 or breaking behavior change is planned, then keep /api pointed at the current stable version.
# Privacy template
Your public privacy copy should now describe the API-driven runtime correctly: what client_id does, what signals may be sent, which keys are browser-safe or server-only, and how consent changes the sponsor path.
### Include in your policy
Describe sponsor delivery, consent handling, project configuration, and the classes of data that can be sent into placement requests or browser activation.
### Avoid outdated wording
Name the wavebird API and describe the data your integration actually sends. If you use Script Tag or the SDK, identify it as the integration method.
## Suggested privacy-policy addition
Replace the settings reference and adjust this template when your integration enables optional prompt sharing or personalization settings.
## TEXT
```text
Advertising and monetization
We use wavebird (wavebird.ai) to display labeled sponsored placements separately from our application's AI output.
Our integration sends [list the actual placement, device, regional, operational-identifier and consent fields] to wavebird for [describe delivery, measurement and security purposes and the applicable legal bases].
[If enabled:] With your permission, we also send the current prompt to wavebird for classification and contextual ad matching. The partner ad-matching request does not contain the raw prompt or chat history. Permitted semantic context is reduced before partner delivery.
Operational records may be retained for measurement, security and settlement. See [link to our retention schedule] for the categories, periods and exceptions, and https://wavebird.ai/privacy for wavebird's processing information.
You can manage the available advertising and consent choices at [link to the actual controls]. Describe what happens when a choice is declined or withdrawn.
```
## HTML
```html
Advertising and monetization
We use wavebird (wavebird.ai) to display labeled sponsored placements separately from our application's AI output.
Our integration sends [actual fields] to wavebird for [purposes and applicable legal bases].
[If enabled:] With your permission, we also send the current prompt to wavebird for classification and contextual ad matching. The partner ad-matching request does not contain the raw prompt or chat history.
Operational records may be retained for measurement, security and settlement. Add the actual retention periods, exceptions and a link to your retention schedule.
Add a link to your actual advertising and consent controls, and explain what happens when consent is declined or withdrawn.
```
# Disclosure
Sponsored output must stay clearly labeled. The browser surface can inherit disclosure defaults from project config, but your app remains responsible for using a placement and styling that keeps the label visible.
## Disclosure rules
### Always visible
Labels such as “Sponsored” or your configured variant must remain visible where the placement renders. A hidden or removed label breaks the intended integration contract.
### Policy-bounded rendering
Disclosure behavior still applies when no-fill or consent-required branches occur. The sponsor surface should degrade gracefully, not blur product and sponsor content together.
## Suggested disclosure copy
Use these templates as a starting point for a visible sponsored-content disclosure. Adapt the wording to your UI, jurisdiction, consent posture, and privacy policy.
## TEXT
```text
This application displays sponsored content provided by wavebird.
Ads are matched using topic categories and language preferences.
Your prompts and conversations are not shared with advertisers.
You can learn more about how ads work in our privacy policy.
```
## HTML
```html
```
## REACT
```tsx
export function SponsoredDisclosure() {
return (
);
}
```
# Privacy
Send only the context needed for a placement. Keep consent controls explicit and raw prompts out of requests to advertising partners.
## Send a privacy-bounded placement request
Send a topic object when you need matching context, and keep prompt_shared false when raw prompt text must not enter the targeting path. For wavebird_consent projects, first record the actual session decision through /v1/consent. The example assumes that permission is current; its flags cannot grant it. Preserve observed Sec-GPC across backend hops.
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/placements \
-H "Authorization: Bearer $WAVEBIRD_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_...",
"session_id": "sess_...",
"job_type": "chat",
"prompt": { "topic": "programming" },
"consent": {
"semantic_targeting": true,
"prompt_shared": false,
"gdpr_applies": true,
"consent_source": "wavebird_consent"
}
}'
```
## Default privacy posture
### Minimal signal egress
Standard SSP request builders project permitted signal fields, such as topic category and language. This does not cover separate model requests, custom adapters or application logs.
### Consent-bounded relevance
Semantic relevance must remain bounded by the active consent profile and the current jurisdiction mode.
### No model influence
The ad path remains separate from model I/O. Sponsored delivery does not rewrite prompts or influence outputs.
## Apply the controls in your integration
Browser teams should read privacy in terms of activation, consent storage, and render-time config. Backend teams should read privacy in terms of the request fields they choose to send into placement creation. The advanced SDK does not change those boundaries.
# Configuration
Configure your project through `client_id`, key class, allowed origins, project defaults, and the runtime-safe project config exposed to browsers.
## Start with the smallest valid placement body
These three fields identify the project, session, and surface. Consent requirements also depend on the project and request: Dashboard-created Test projects require a current stored consent record in the same authenticated project and session; inline consent is not a substitute. Add context, hints, and overrides only when needed.
## JSON
```json
{
"client_id": "wbproj_...",
"session_id": "sess_...",
"job_type": "chat",
"consent": {
"semantic_targeting": false,
"prompt_shared": false,
"gdpr_applies": true,
"consent_source": "wavebird_consent"
}
}
```
## Identity and keys
### Project ID (client_id, WAVEBIRD_CLIENT_ID)
Public project identifier used by both browser and server flows to resolve defaults and config. In the dashboard this is the Project ID (`client_id`, `WAVEBIRD_CLIENT_ID`), formatted like `wbproj_...`.
### Publishable key
Browser-safe key used only for activation-backed traffic. It must be bound to allowed origins.
### Secret key
Server-only key for canonical backend calls against `/v1/placements`, advanced job/decision routes, and `/v1/beacons`. Use a Server Test Key (`sk_test_...`) first for Test traffic.
### Production dry-run key
Server-only key for pre-live production dry-run checks (`sk_dry_...`). It selects dry-run mode by credential class; do not add `production_dry_run`, `production_billing_dry_run`, `billing_suppressed`, or `production_live_approved` to request bodies.
## Runtime defaults
### Format and label defaults
Preferred formats, slot hints, disclosure labels, and other non-secret defaults are resolved from the authenticated key's project and `client_id`. Advanced Server API calls can override supported fields such as `allowed_formats`, `timing`, `bidfloor`, `publisher`, and `blocked_categories`.
### Allowed origins and browser safety
Allowed origins gate publishable-key activation. Browser-safe integration is not just about the key prefix; it depends on server-side origin enforcement.
## Change behavior
### Config propagation
Changes apply through the active project config path. Browser integrations pick them up on the next activation or the next initialization cycle.
## Rate limits and CORS
| Surface | Key type | Origin behavior | Limit behavior |
| --- | --- | --- | --- |
| `/v1/placements`, `/v1/jobs`, `/v1/decisions`, `/v1/beacons` | Secret key | Server-to-server requests do not depend on browser origin allowlists. | Per-key buckets; 429 includes `Retry-After`. |
| `/v1/browser/activate` | Publishable key | The request origin must match the key's allowed origins. | Publishable-key and browser-IP buckets protect activation. |
| Browser token calls | Activation token | Preflight can succeed for candidate origins; the actual request is still checked against stored project/key state. | Separated from secret-key traffic so sandbox and live integrations do not share counters. |
## Related reference
Key classes and rotation · Rate limits and retries · Compatibility routes
- [Key classes and rotation](/api/reference/authentication)
- [Rate limits and retries](/api/reference/rate-limits)
- [Compatibility routes](/api/reference/versioning)
# Brand safety
Brand safety belongs to your project config and job overrides, not to undocumented renderer behavior. The API surface exposes the controls explicitly so browser and server paths stay aligned.
## Apply request-level controls
Put format and brand-safety controls in overrides. The required project, session, and job fields remain present even when the request only changes policy.
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/placements \
-H "Authorization: Bearer $WAVEBIRD_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_...",
"session_id": "sess_...",
"job_type": "chat",
"consent": {
"semantic_targeting": false,
"prompt_shared": false,
"gdpr_applies": true,
"consent_source": "wavebird_consent"
},
"overrides": {
"allowed_formats": ["banner", "native"],
"blocked_categories": ["gambling", "tobacco"],
"blocked_domains": ["competitor.example"]
}
}'
```
## Controls that matter
### allowed_formats
Limit the renderable creative formats for the active job or project defaults.
### blocked_categories
Apply category-based exclusions before the market-side request is accepted.
### blocked_domains
Prevent delivery from blocked advertiser or destination domains when your project policy requires it.
## Failure behavior
Invalid or overly strict policy does not break your app. The sponsor path should resolve as no-fill while the model or product flow continues.
# Graceful degradation
The sponsor path is opportunistic. Your product flow stays primary. When the API cannot or should not return a sponsored placement, the correct outcome is usually no-fill, not a broken app state.
## Treat an unavailable placement as optional
A placement response can contain placement: null. Handle that result and request failures as an optional branch, then continue the primary product flow.
## REST
```javascript
async function requestOptionalPlacement() {
try {
const response = await fetch("https://api.wavebird.ai/v1/placements", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WAVEBIRD_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: "wbproj_...",
session_id: "sess_...",
job_type: "chat",
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: true,
consent_source: "wavebird_consent",
},
}),
});
if (!response.ok) return null;
const { placement } = await response.json();
return placement ?? null;
} catch {
return null;
}
}
```
### No-fill is valid
No-fill is a normal runtime result. Treat it as a valid branch and keep the rest of the UI responsive.
### Consent can block delivery
Missing or insufficient consent should prevent sponsor delivery without preventing the main product response.
### Renderer must stay optional
Your UI should not depend on a successful sponsored placement. The slot can disappear, collapse, or render a safe fallback shell.
# Endpoint reference
These are the canonical public API v1 routes. Script Tag and the advanced SDK are wrappers around this surface.
### POST /v1/browser/activate
Exchanges a publishable key and origin for a short-lived browser activation token.
[Read more](/api/v1/browser-activate)
### POST /v1/placements
Creates one job, waits for the first slot decision, and returns the canonical placement response.
[Read more](/api/v1/placements)
### POST /v1/jobs
Advanced compatibility route that creates a sponsorship job and returns one or more slot IDs.
[Read more](/api/v1/jobs)
### GET /v1/decisions/{slot_id}
Polls a slot for the selected sponsored asset and render metadata.
[Read more](/api/v1/decisions)
### GET /v1/render.js
Returns the browser renderer that mounts hosted placement frames and manages turn lifecycle.
[Read more](/api/v1/render-js)
### GET /v1/render/{asset_token}
Returns isolated creative HTML for a single asset token.
[Read more](/api/v1/render-frame)
### POST /v1/beacons
Records render, visible, click, completion, or heartbeat events for billing and proof integrity.
[Read more](/api/v1/beacons)
### POST /v1/consent
Stores publisher consent state for Script Tag and custom consent flows.
[Read more](/api/v1/consent)
### GET /v1/projects/{client_id}/config
Returns non-secret runtime configuration for a project.
[Read more](/api/v1/project-config)
# Type and runtime reference
Look up Script Tag attributes, methods, and events. Script attributes configure the browser integration; slot attributes configure an individual placement.
## Script Tag attributes
### data-client-id
Required. Public client identifier for the app whose server-side config should load.
### data-publishable-key
Required in production. Browser-safe pk_publishable_* key bound to your allowed origins.
### data-session-id
Optional stable session identifier for consent, pacing, and future frequency controls.
### data-job-type
Optional product hint. Supported values are chat, code, image, voice, and agent.
### data-locale
Optional locale hint for creative selection and disclosures, for example en-US or de-DE.
### data-prompt-topic / data-prompt-text
Optional contextual hints passed into the initial job request.
### data-native-template
Optional native renderer preset. Supported values are default and compact.
### data-timing
Optional timing hint. Supported values are during, before, and after.
### data-api-base-url
Optional override for non-production environments. Defaults to https://api.wavebird.ai.
### data-auto-discover-slots
Optional. Set false if you want to call renderAd() manually instead of auto-discovering [data-wavebird-slot] targets.
## Slot attributes
### data-wavebird-slot
Marks an element as an auto-discovered render target for the next available slot.
### data-wavebird-position
Optional slot placement hint. Supported values are above, below, sidebar, and between.
### data-wavebird-max-width / data-wavebird-max-height
Optional sizing constraints used when the renderer reserves space for banner, clip, or native placements.
### data-wavebird-formats
Optional comma-separated format hint such as banner,native or clip.
### data-wavebird-prompt-topic / data-wavebird-prompt-text
Optional per-slot prompt hints when different placements should use different sponsor context.
### data-wavebird-timing
Optional per-slot timing hint. Supported values are during, before, and after; a resolved decision also writes its timing to this attribute.
## Script Tag methods
### init(config)
Initializes the global controller. Config requires clientId and accepts publishableKey, sessionId, apiBaseUrl (default https://api.wavebird.ai), jobType, locale, promptTopic, promptText, timing, nativeTemplate (default or compact), nativeStyles ({ card, media, copy, label, sponsor, title, description, cta }), and autoDiscoverSlots (defaults true).
### createJob(options)
Creates a browser job and returns jobId plus slotIds. Options are sessionId, jobType, locale, promptTopic, promptText, timing, slotHint ({ position, maxWidth, maxHeight, allowedFormats, bidfloor, bidfloorCurrency, timing }), and slotsRequested.
### renderAd({ slotId, target, waitMs? })
Polls the canonical /v1/decisions/{slot_id} route, renders banner, clip, or native output, and wires beacons automatically. waitMs defaults to 1500; it makes at most four attempts with a 500 ms delay between pending attempts.
### setConsent(tcfString)
Stores a local TCF string; this is not the authoritative session consent record. Protocol-bearing admission is currently denied with HTTP 400 validation_error (consent.protocol) pending a supported policy.
### on(event, handler)
Subscribes to lifecycle events such as ready, ad:rendered, consent:required, and error.
## Script Tag events
### ready
Payload: { clientId: string; apiBaseUrl: string }. Fires after init() stores the active configuration.
### job:created
Payload: { jobId: string; slotIds: string[] }.
### decision:loaded
Payload: { slotId: string; status: string; metadata?: { timing: "during" | "before" | "after" } }.
### ad:rendered
Payload: { slotId: string; format: "banner" | "clip" | "native"; metadata?: { timing: "during" | "before" | "after" } }.
### ad:empty
Payload: { slotId: string }.
### clip:play_started
Payload: { slotId: string }.
### clip:play_completed
Payload: { slotId: string }.
### clip:autoplay_blocked
Payload: { slotId: string }.
### consent:required
Payload: { clientId: string; sessionId: string }.
### consent:submitted
Payload: { clientId: string; sessionId: string; decision: "personalized" | "basic" | "custom" }.
### error
Payload: { code: string; message: string; cause?: unknown }. Non-fatal runtime errors for debugging.
## Related reference
- [Authentication and key classes](/api/reference/authentication)
- [Rate limits](/api/reference/rate-limits)
# SDK reference
These packages remain available for advanced integrations. They are documented here as secondary surfaces, not as the primary way to understand wavebird.
- [Open full SDK reference](/sdk/reference)
### wavebird
Advanced Node or server entrypoint. Good when you already want a thin TypeScript wrapper over the public API.
### wavebird/browser
Advanced browser entrypoint. Still activation-backed and intended for custom client-side orchestration.
### wavebird/react
React renderer for teams that want component-level control around an already-fetched decision.
### wavebird/consent
Local consent storage and parsing helpers. Parsing is not protocol approval, and local state does not replace scoped /v1/consent synchronization. Observed positive GPC prevents new sponsorship admission.
### wavebird/mount
Deprecated compatibility helper. Use wavebird.js for new browser integrations.
# Advanced SDK
The SDK is the public wavebird package for consumer AI app teams that need package-level control over ad requests, decisions, rendering helpers, consent, generation lifecycle events, and beacons.
Start with Server API or Script Tag when hosted defaults are enough. Use the SDK when framework ergonomics, migration constraints, or package-level control over the same production API justify the package layer. Teams moving existing package integrations can follow the SDK-first migration guide.
- [Server API or Script Tag](/api/quickstart)
- [SDK-first migration guide](/api/migration)
## SDK example
## TypeScript
```typescript
import { WavebirdClient } from "wavebird";
const client = new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getApiKey: () => process.env.WAVEBIRD_SECRET_KEY ?? "",
});
const job = await client.createJob({
client_id: process.env.WAVEBIRD_CLIENT_ID,
job_type: "chat",
slots_requested: 1,
});
```
## When the SDK still makes sense
### Typed client ergonomics
Helpful when your team wants a typed client wrapper around jobs, decisions, and beacons.
### React rendering helpers
Useful if you specifically want component-level rendering helpers on top of the public API flow.
### Compatibility path
Useful when you are migrating an existing SDK-based integration but want the docs and contracts to stay API-first.
## Install
Install command: npm install wavebird
- [Open full SDK docs](/sdk)
# Proxy
Proxy mode is a compatibility pattern for systems that need to keep wavebird behind a controlled HTTP boundary. It is not the primary integration path and should stay a deliberate architecture choice, not the default.
### Use when required
Keep this pattern for constrained environments with fixed ingress rules, policy gateways, or hard separation between browser and runtime services.
### Keep the public model intact
Even behind a proxy, the target behavior should still map cleanly to the canonical public API and not invent a separate product contract.
# Callback
Callback delivery is an advanced server-side pattern. Use it only when you specifically need decision handoff back into your own callback endpoint rather than polling or standard browser-driven reads.
### When it fits
When your infrastructure is already organized around callback-based ingestion and you want decisions to arrive at a controlled server boundary.
### What it does not change
The canonical concepts stay the same: jobs, decisions, consent, and beacons remain part of the same API model.
# Consent
Projects using wavebird_consent require a current stored consent record for the authenticated tenant, client_id, and session_id. Record the actual user decision through /v1/consent before placement; inline flags cannot replace or expand that permission.
## Choose the genuine decision source
| Project profile | Decision source and responsibility |
| --- | --- |
| Own CMP (own_cmp) | Your application owns the actual per-user, per-conversation choice. Synchronize it as publisher_custom through /v1/consent; placement privacy metadata uses wrapper_cmp. Those labels describe the source, not a grant of permission. |
| Wavebird dialog (wavebird_consent) | Use the genuine Wavebird dialog and synchronize its actual choice as wavebird_dialog. The project requires a current stored record for the authenticated tenant, project and session before placement; inline flags cannot replace it. |
- [Open your project's three-copy Chat Test guide: ENV, BACKEND and FRONTEND](https://dashboard.wavebird.ai/wavebird/overview)
## Per-user conversation lifecycle
Bind consent, placement and withdrawal to the same anonymous session for one user conversation. Create fresh state when the user or conversation changes; never reuse a shared demo session or replay a cached grant.
Grant only after an explicit current choice. Denial, withdrawal, expiry or an unavailable consent state keeps the slot hidden, cancels pending sponsor work and prevents late rendering and ad beacons. Purpose flags do not grant lifecycle permission.
Preserve observed Sec-GPC across the browser and backend. A positive signal blocks advertising admission; do not strip the signal, protocol strings or denied permission to make a request succeed.
Keep the primary chat stream available without waiting for consent or an ad. Request sponsorship only at the saved Before, During or After phase. Clear a During slot when the stream ends; start After work only after completion. Bound each phase's lifetime and cancel it when the conversation changes or its permission is lost.
## Three separate controls
### Lifecycle consent
A current authoritative consent record permits the placement and renderer lifecycle. If it is refused, expired, or revoked, wavebird does not render or send beacons.
### Semantic targeting
`semantic_targeting: false` disables prompt-derived targeting. Permitted product or placement context is a separate input. Turning semantic targeting off does not remove lifecycle consent requirements.
### Prompt sharing
`prompt_shared: false` keeps raw prompts and chat content out of targeting. Prefer a controlled topic and never send identities.
## Which fallback is permitted?
| State | Permitted ad behavior |
| --- | --- |
| Required lifecycle permission is missing, refused, expired, or revoked | Do not render or send ad beacons. Keep the AI task available; non-personalized delivery is not a way around this requirement. |
| Current lifecycle permission; semantic_targeting is false | Do not derive targeting from the user's prompt. Non-personalized delivery may use permitted product or placement context only when the active jurisdiction, project, and partner rules allow it. |
| prompt_shared is false | Do not use raw prompt text for matching, even if semantic_targeting is enabled. Keep the request within the remaining permitted fields. |
| Both semantic_targeting and prompt_shared are true | Permitted prompt classification can run inside wavebird. Reduce it to allowed semantic fields; raw prompts and chat history still do not reach advertising partners. |
## Test data and real-user traffic
### Synthetic Test traffic
Server Test Keys can be created before a DPA is signed. Until then, use only synthetic sessions and test data that do not represent real end users.
### Real-user testing
A Test integration that sends real end-user data is not treated as synthetic. Complete the DPA first when wavebird acts as processor, and apply the required notice, legal-basis, and consent controls.
### Live Ads
Live Ads require a current DPA for every publisher app. Company country prepares the legal workflow, while request-level jurisdiction and CMP signals control the active end-user delivery decision.
## /v1/consent shape
- [Full endpoint reference](/api/v1/consent)
### Canonical sync body
Use the selected project's complete backend and frontend lifecycle handlers. Include client_id, the same session_id, the actual lifecycle_state, a positive increasing revision, the profile's genuine decision source and canonical purposes. Verify the returned authoritative snapshot before rendering; source labels and inline flags cannot manufacture a user's decision.
### Compatibility aliases
The API accepts legacy `source: "publisher"` and `source: "custom_dialog"` as `publisher_custom`. `purposes.ads` maps to `semantic_targeting`, `purposes.measurement` maps to `session_persistence`, and missing `decision` is inferred as `custom` when `purposes` is supplied.
### Response normalization
Successful sync responses include a safe `normalized` object with canonical `source` and `purposes` values so test harnesses can verify alias handling without reading stored server state.
## Choose where to pass consent
| Consent path | Location | Purpose | Separate sync needed? |
| --- | --- | --- | --- |
| Built-in Script Tag flow | Configured browser dialog | Collect and store permission before the next request | Handled by the tag |
| Request-level placement consent | inside `/v1/placements` | per-request privacy and targeting signal | Does not replace required stored consent |
| Consent sync | `/v1/consent` | session-level publisher consent state | Required for wavebird_consent projects |
## Use the current lifecycle handlers
Copy the project-scoped ENV, BACKEND and FRONTEND blocks together. The BACKEND block includes both placement and consent routes; never omit the lifecycle handler or substitute a static granted JSON body.
- [Open the Chat Test integration guide](https://dashboard.wavebird.ai/wavebird/overview)
- [Full consent endpoint reference](/api/v1/consent)
## What the API enforces
### External protocol admission
TCF and GPP inputs are currently rejected at admission with validation_error (400), including structurally valid strings, because an authoritative vendor, purpose, and jurisdiction policy mapping is not configured. Do not interpret field support as permission to deliver ads.
### Fail-closed behavior
Admission can return a 403 consent error rather than a successful no-fill response. An observed positive Sec-GPC signal blocks new advertising admission with privacy_signal_policy_required (403). Preserve the signal across backend requests and keep the primary application flow available; do not retry by removing the signal.
### Prompt sharing is explicit
Supplying prompt text transmits it to the Wavebird API. prompt_shared: false blocks its use for targeting. Standard SSP builders project permitted signal fields rather than forwarding the raw prompt body; separate model requests, custom adapters and application logs require their own review.
# Migrate from SDK-first
New wavebird integrations should start with the Server API, use Script Tag for browser-first installs, and keep the SDK as an advanced compatibility layer. Existing SDK integrations can keep running while you move route ownership and docs references to the API-first model.
## What changes
### 1. Server API
Use /v1/placements from your backend and /v1/render.js in the frontend when your server owns the flow.
### 2. Script Tag
Use wavebird.js with a publishable key when the browser should discover slots, render ads, and send beacons directly.
### 3. Advanced SDK
Keep wavebird for teams that intentionally want package ergonomics or need backwards compatibility during migration.
## Route mapping
| Old SDK-first surface | Current preferred surface | Migration note |
| --- | --- | --- |
| createJob() + getDecision() | POST /v1/placements | Keep SDK code working, but show the one-call placement route in new onboarding. |
| Raw media rendering | GET /v1/render.js | Let the hosted renderer consume placement.render instead of adding image or video DOM yourself. |
| sendBeacon() | POST /v1/beacons | Preserve render, visibility, click, and completion evidence. |
| /public/browser/v1/activate | /v1/browser/activate | The public-browser route remains a compatibility alias. |
## Safe migration order
### Step 1
Create or verify secret and publishable keys in the dashboard.
### Step 2
Move new backend snippets to /v1/placements and pass filled responses to /v1/render.js with wavebird.renderPlacement().
### Step 3
For browser-first pages, replace package bootstrap snippets with Script Tag markup.
### Step 4
Keep existing SDK installs pinned and validate them with test keys before removing package code.
### Step 5
Update your integration documentation to match the routes and rendering method your app now uses.
### Step 6
Confirm logs show placements, decisions, consent, and beacons under the same project before live traffic.
The SDK is not removed. It remains supported as a compatibility and advanced-package path while partner-facing onboarding moves to the API-first sequence.
- [Review Advanced SDK positioning](/api/sdk)
# Developer FAQ
The questions below assume the API-first model. Use the SDK only as an advanced compatibility option when you already know why the direct Script Tag or Server API path is not enough.
## Do I need the SDK to integrate wavebird?
No. The API is now the primary surface. Most teams should start with Script Tag or direct Server API calls. The SDK is optional.
## What is the recommended browser path?
Script Tag. It gives you the browser-safe activation path, consent handling, rendering, and beacon wiring on top of the same public API contract.
## What is the canonical backend path?
The canonical backend path is the public /v1 API with a secret key. That is the primary server reference surface.
## What must I verify before moving to live keys?
Validate activation or server auth, confirm decision shapes, test no-fill branches, and make sure the beacon path is complete for the formats you ship.
### Need help diagnosing no-fill?
Start with the API troubleshooting page before you debug renderer details.
[Read more](/api/troubleshooting)
### Need the SDK on purpose?
Use the advanced SDK page and the SDK reference once the API model itself is already clear.
[Read more](/api/sdk)
### Open full SDK docs
[Read more](/sdk)
# Integration paths
Pick the smallest integration path that matches where your app controls rendering and consent.
## Server API is the default
Use the Server API when your app already has a backend. It keeps secret keys out of the browser while the hosted renderer owns media sizing, clicks, and beacons.
- Best for production consumer AI apps with backend orchestration.
- Uses secret keys and POST /v1/placements.
- Pairs cleanly with render.js and wavebird.renderPlacement().
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/placements?wait_ms=1500 \
-H "Authorization: Bearer sk_test_wavebird_demo_secret" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_demo_8jK42",
"session_id": "sess_demo_123",
"job_type": "chat",
"slots_requested": 1,
"slot_hint": {
"position": "below",
"max_width": 728,
"max_height": 90
},
"overrides": {
"allowed_formats": ["banner", "clip", "native"],
"timing": "during"
},
"consent": {
"semantic_targeting": false,
"prompt_shared": false,
"gdpr_applies": false,
"consent_source": "wavebird_consent"
}
}'
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/placements?wait_ms=1500", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
job_type: "chat",
slots_requested: 1,
slot_hint: {
position: "below",
max_width: 728,
max_height: 90
},
overrides: {
allowed_formats: ["banner", "clip", "native"],
timing: "during"
},
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: false,
consent_source: "wavebird_consent"
}
})
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
payload = json.loads("{\"client_id\":\"wbproj_demo_8jK42\",\"session_id\":\"sess_demo_123\",\"job_type\":\"chat\",\"slots_requested\":1,\"consent\":{\"semantic_targeting\":false,\"prompt_shared\":false,\"gdpr_applies\":true,\"consent_source\":\"wavebird_consent\"}}")
request = urllib.request.Request(
"https://api.wavebird.ai/v1/placements?wait_ms=1500",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
## Script Tag is the browser path
Use the Script Tag when you want a browser slot to activate, request, render, and beacon with minimal application code.
- Uses publishable keys plus allowed origins.
- Handles consent UI when configured.
- Keeps publisher markup small and framework independent.
## HTML
```html
```
## SDK is advanced compatibility
Use the SDK when a TypeScript wrapper and the job-plus-decision model are intentionally better than direct REST or Script Tag markup. The SDK has no client.createPlacement() method; POST /v1/placements remains the separate one-step REST path.
- Useful when your app needs slot IDs and explicit decision polling.
- Still supported for backwards compatibility.
- Not the primary onboarding surface.
## Node
```typescript
import { WavebirdClient } from "wavebird";
const client = new WavebirdClient({
baseUrl: "https://api.wavebird.ai",
getApiKey: () => process.env.WAVEBIRD_SECRET_KEY ?? "",
});
const job = await client.createJob({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
job_type: "chat",
slots_requested: 1,
});
```
# Authentication
wavebird uses secret keys for server calls and publishable keys for browser activation.
## Secret keys
Secret keys authenticate server-to-server calls. Never ship them to browsers, mobile apps, or public repositories. Server requests also send the Project ID (`client_id`, `WAVEBIRD_CLIENT_ID`), formatted like `wbproj_...`.
- Use secret keys for /v1/placements, advanced job routes, project config reads, and server-managed beacons.
- Copy the Project ID (`client_id`, `WAVEBIRD_CLIENT_ID`) from the dashboard env block or integration page.
- Rotate keys in the dashboard if ownership or deployment boundaries change.
- Responses after creation must not reveal the full key again.
## Publishable keys and origins
Publishable keys are browser-safe only when paired with allowed origins. Browser activation rejects mismatched origins before a job can be created.
- Configure every production origin in the dashboard.
- Use test keys for local and staging origins.
- Origin errors include request IDs and docs links for debugging.
## Request IDs
Every JSON API response should include a request_id and an X-Request-Id header. Include that value when you contact support.
# Concepts
Use client_id to load project context, then choose the Server API, Script Tag, or advanced SDK based on where your app owns rendering and orchestration.
The default backend path creates a placement, the browser path can add activation, the hosted renderer mounts a decision, and rendered slots emit beacons.
The shortest mental model is: client_id loads project defaults, the default backend path creates a placement, the browser path can add activation, the hosted renderer mounts the decision, and rendered slots emit beacons.
## client_id
The stable public identifier for a project. Browser and server integrations use it to load project defaults, non-secret config, and routing context without exposing a secret key.
## Browser activation
The publishable-key exchange that binds browser traffic to allowed origins. Script Tag and advanced browser integrations use this before calling protected runtime routes.
Activation is why publishable keys stay browser-safe only when allowed origins are enforced. See API configuration.
- [API configuration](/api/configuration)
## Placement
The default Server API response from POST /v1/placements. It combines job creation with the first slot decision and includes placement.render when a fill should be mounted by the hosted renderer.
New Server API integrations should start here: one backend call to /v1/placements, then render.js handles the browser surface.
## Job
The advanced compatibility unit of work for one monetization opportunity. A job creates one or more slot IDs and brackets the decision and beacon lifecycle for clients that intentionally orchestrate polling.
Jobs remain available for integrations that intentionally need slot IDs and manual decision polling. They are no longer the shortest default onboarding path.
## Hosted renderer
The render.js browser surface that mounts placement.render.frame_url in one iframe, applies declared dimensions, and manages turn lifecycle and beacons for the default Server API flow.
## Decision
The latest sponsor result for a slot. A decision can be a fill or a no-fill, and it carries the structured creative payload or stable no-fill reason codes.
## Beacon
A lifecycle event recorded after rendering, visibility, or clip playback. Beacons turn ad delivery into auditable evidence for billing and diagnostics.
For proof and billing context behind these events, see proof integrity.
- [proof integrity](/resources/proof-integrity)
## Project config
The non-secret runtime configuration exposed to browser renderers and debug tooling. This includes format hints, disclosure labels, and consent-related defaults.
### Consent and privacy
Consent is not a side topic. The public API includes explicit consent storage and routing behavior because semantic relevance and persistence must stay policy-bounded.
### API before SDK
SDK methods use the same API request, response, consent, and measurement contracts. Consult the endpoint reference when checking runtime behavior.
# Integration flow
The public API flow is intentionally small. Browser paths add activation. Server paths use /v1/placements. The default frontend loads render.js and lets wavebird own media rendering and beacons.
### 1. Load project context
Resolve client_id and the right key class for the surface: publishable key for the browser path, secret key for the backend path.
### 2. Activate the browser when needed
Script Tag or advanced browser integrations exchange the publishable key for an origin-bound activation token before runtime calls.
### 3. Create a placement
First establish the required stored session consent from an actual decision. Call POST /v1/placements with project, session, and job type; preserve observed Sec-GPC across your backend. Prompt hints and overrides are optional.
### 4. Render the decision
The placement response is either pending/no-fill or a fill with placement.render. Load render.js and let the hosted renderer mount the creative frame.
### 5. Disclose and finish
Keep sponsor disclosure visible and let the turn lifecycle preserve the required viewability window before cleanup.
### 6. Send beacons
Record rendered, visibility, click, or clip-completion events so diagnostics and billing evidence stay complete.
### Script Tag path
Handles activation, config loading, slot discovery, consent prompting, rendering, and beacon wiring for you.
### Server API path
Keeps request configuration on your backend. Call /v1/placements, then pass the filled canonical response to wavebird.renderPlacement so the hosted renderer owns media, sizing, and beacons.
# Patterns
Start with the Server API when your application has a backend, and use Script Tag when the browser should activate, render, and beacon a slot directly.
Callback and proxy patterns are narrower compatibility choices for integrations with those specific delivery or boundary requirements.
# Authentication
Choose the credential class for the request path, then keep server secrets and browser activation flows separate.
## Key classes
Server Test Keys start with sk_test_ and are for non-billable Test requests. Server Production Dry-run Keys start with sk_dry_ and are for non-billable pre-live validation. Server Production Keys start with sk_live_ and are for approved live server traffic. Browser Publishable Keys start with pk_publishable_ and are only for browser activation.
Keep sk_test_, sk_dry_, and sk_live_ values on your server. Never place server secret keys in browser bundles, HTML, mobile apps, or public repositories.
## Browser activation
A browser sends its pk_publishable_ key in the POST /v1/browser/activate request body together with an allowed Origin. A successful activation returns a short-lived activation_token bound to that project and origin.
Use the returned activation token for browser-authorized /v1 requests. Do not send the raw publishable key as Authorization for placements, jobs, decisions, beacons, consent, or project configuration.
## Permissions and origins
Configure every local, staging, preview, and production browser origin on the publishable key in the dashboard. Activation rejects an Origin that is not configured for that key.
A valid server secret key or a valid browser activation token is required for the authenticated /v1 routes. The hosted renderer script and issued render-frame URLs do not use an Authorization header.
## Create and rotate server keys
Raw server secrets are shown only once when created or rotated. The later masked server-secret previews identify keys but are not usable credentials and cannot recover the original value.
Update your server configuration before retiring the previous credential. Treat revoked keys as unavailable immediately unless the dashboard explicitly shows a grace deadline.
Use test credentials for integration checks and sk_dry_ for non-billable production dry-run. Live traffic requires completed payout setup and activated delivery. Browser Publishable Keys remain separate from all server key classes.
Dry-run is selected by credential class. Do not send production_dry_run, production_billing_dry_run, billing_suppressed, or production_live_approved in request bodies.
# API stability
Endpoint stability communicates compatibility expectations for the public API surface.
## Stability levels
Stable endpoints are the default public contract and receive compatible changes within API v1. For endpoints marked Contract may change, review request and response changes before upgrading your integration.
Advanced endpoints are public for integrations that need lower-level control, but they are not the default onboarding path and require closer review when upgrading.
## Current endpoint status
This table is generated from the canonical API endpoint model.
| Endpoint | Status |
| --- | --- |
| POST /v1/browser/activate | stable |
| POST /v1/placements | stable |
| POST /v1/jobs | advanced |
| GET /v1/decisions/{slot_id} | advanced |
| GET /v1/render.js | stable |
| GET /v1/render/{asset_token} | stable |
| POST /v1/beacons | stable |
| POST /v1/consent | Contract may change |
| GET /v1/projects/{client_id}/config | stable |
## Changes and versioning
API v1 itself is stable; no version 2 exists today. Compatibility and change-announcement practice are documented on the versioning page.
- [Read versioning](/api/reference/versioning)
# Compute Sponsoring
Compute Sponsoring funds consumer AI experiences through separate sponsored placements alongside the application flow.
The publisher keeps ownership of model output while configuration controls sponsorship defaults, pacing, consent, and safety.
## Definition
The September 2026 paper by Christian Leinberger proposes linking an advertising exposure to an identifiable compute unit. A job can provide that reference: user intention, the generation process and its result. The paper develops a category definition, not a technical specification or certification.
## Why it matters
Funding is one part of the argument. Because AI content is generated in the moment, an output alone cannot reliably show whether advertising influenced it. Coupling advertising to the compute process makes the separation addressable for publishers, users and advertisers.
## Source and persistence
Semantic Source distinguishes no semantics (S0), user intention (S1), model-derived semantics (S2), and both sources (S3). Semantic Persistence distinguishes transient signals (P0), retention within a session (P1), and retention across sessions (P2).
Neutral is S0/P0. Situational uses S1, S2 or S3 with P0 or P1. Profiling uses semantic signals with P2. These are descriptive categories, not a list of wavebird features.
## How to read the notation
CS-S (S1/P0) describes prompt-derived situational relevance without semantic persistence. The notation applies only when advertising is coupled to a compute unit and the proposed principles hold; a label or API flag does not establish that by itself.
## Configuration
Configure sponsorship through project defaults, formats, pacing, consent, and safety policies. Job requests can still carry documented per-request overrides.
## Publication
Current paper: Christian Leinberger, September 2026. The earlier February 2026 white paper was co-authored with Mario von Bassen.
- [Read the current paper](https://compute-sponsoring.org/)
- [Publication record (DOI)](https://doi.org/10.5281/zenodo.22236295)
- [Earlier version (February 2026)](https://compute-sponsoring.org/versions/1.0)
# Loader ad placement
Loader ad placement places sponsorship near the user experience while the application remains in control of timing and rendering.
## Technical model
The app asks its backend for a placement decision while the model response remains owned by the publisher app. Advanced clients can still create jobs and poll slot decisions directly.
## Rendering
Use render.js with placement.render for the default Server API path, or use the Script Tag for browser-first integrations. Either path records beacons for proof and settlement.
## Fallback
If no decision is ready, keep the user flow moving. Do not delay model start or completion for an ad response. Skip a late or unavailable placement and keep the AI task moving.
# Privacy firewall
The privacy firewall limits what leaves the publisher app and keeps sensitive application context out of ad-facing paths.
## Controlled egress
Send only the fields needed for sponsorship decisions. Use project configuration for defaults instead of repeating sensitive values in every request.
## Browser boundaries
Publishable keys require allowed origins and browser activation. Secret keys remain server-only.
## Logs and redaction
Request IDs are safe to share. API keys, activation tokens, and user identifiers should be redacted from application logs.
# Proof chain
The proof chain links placement requests, decisions, render events, and visibility/click beacons into an auditable record.
## Inputs
Placements establish the request context. Decisions identify selected assets. Beacons record what was rendered and observed.
## Idempotency
Beacon IDs should be stable per event. Re-sending the same beacon must not double-count settlement events.
## Check the evidence
After integration changes, check that each rendered placement has the expected observation events and that retries do not duplicate them.
- [Beacon fields and diagnostics](/api/v1/beacons)
- [Proof integrity](/resources/proof-integrity)
# Consent model
Distinguish the permission to run an ad lifecycle from the purposes allowed within that lifecycle.
## Where the decision comes from
The source field identifies publisher_custom, server_sync, or wavebird_dialog. It records how the consent decision was supplied, not which purposes were granted. Store only the state needed for the integration.
## What the decision permits
Lifecycle permission controls rendering and beacons. Purpose flags control semantic targeting, session persistence, cross-session persistence, and prompt sharing. Turning off semantic targeting does not remove the requirement for current lifecycle permission.
- [Consent paths and permitted fallback](/api/consent)
- [Decision and purpose fields](/api/v1/consent)
## Where the state is carried
The Script Tag can collect and persist a decision when configured. For wavebird_consent projects, a publisher must synchronize the actual session decision through /v1/consent; placement flags cannot substitute for required stored permission.
- [Consent timing in an AI interaction](/api/guides/consent-genai)
# Ad formats
Formats describe how a sponsorship asset is rendered, not how the application requests placements.
## Banner
Banner is the simplest display format. Use it where a fixed slot is appropriate.
## Native
Native assets provide structured fields such as headline, body, image, and call-to-action so the publisher can render inside its own UI.
## Clip
Clip is intended for richer media placements. Test autoplay, mute, completion, and visibility behavior before production rollout.
# Script Tag installation
Install wavebird.js to activate your project, discover browser slots, and render ads with the configured consent and measurement behavior.
## Install
Add wavebird.js and at least one data-wavebird-slot element to the page. Use a publishable key and client_id from the dashboard.
## HTML
```html
```
## Allowed origins
Every browser origin must be configured for the publishable key. Localhost, staging, and production are separate origins.
## Debug mode
Append ?wavebird-debug=1 to the page URL to surface initialization, activation, consent, render, decision, and beacon diagnostics in the console.
# Script Tag configuration
Use data attributes for client_id, publishable key, job type, formats, position, and optional debug behavior.
## Script attributes
Set the project, publishable key, and script-wide timing on the script element. data-timing applies to the integration; keep server secrets out of the markup.
## HTML
```html
```
## Slot attributes
Set placement intent, allowed formats, optional labels, and data-wavebird-timing on the slot. Values not specified by the slot use supported project defaults.
- [All Script Tag attributes and defaults](/api/reference/types)
## Preview-only controls
data-preview-* attributes belong to editable website demos. wavebird.js ignores them; use documented runtime attributes in your app.
# Ad slot markup
Every browser-rendered placement needs a stable element with data-wavebird-slot.
## Slot element
Place the slot where the sponsored asset may render. Keep dimensions stable so rendering does not shift the page unexpectedly.
## HTML
```html
```
## Native assets
Native assets render from structured fields when supported by the decision. Use CSS to keep the placement consistent with your app.
## Beacon timing
The Script Tag sends render and visibility beacons automatically after insertion and IAB-style visibility observation.
# Events and callbacks
Script Tag events help you observe activation, consent, rendering, decisions, and beacon delivery.
## Debug diagnostics
Use ?wavebird-debug=1 during integration. Remove it for production user sessions unless you need targeted diagnostics.
- [Event payload types and method defaults](/api/reference/types)
## Callback-style delivery
Callback-style integrations are supported for advanced flows. Public webhook subscriptions are not a documented API v1 feature.
## Failure behavior
If the API is unreachable, the script should fail gracefully and leave the publisher page functional.
# Script Tag troubleshooting
Most Script Tag issues are caused by origin mismatches, missing slots, or blocked network calls.
## Origin mismatch
If activation returns `forbidden` with an origin-mismatch message, add the exact origin to the publishable key in the dashboard. Include scheme and port for local development.
## No slot rendered
Confirm the page has a data-wavebird-slot element before the script initializes, or call the public API after dynamically inserting slots.
## Missing beacons
Check that render and visibility events are not blocked by CSP, ad blockers, or page lifecycle interruptions.
# First ad in 5 minutes
Use this checklist to confirm that your first test placement works.
## 1. Prepare the test project
Create a Server Test Key and copy the Project ID (client_id, WAVEBIRD_CLIENT_ID). Keep live keys out of local examples.
- [Follow the Quickstart](/api/quickstart)
## 2. Confirm the response
Send POST /v1/placements?wait_ms=1500 from your backend. For a fill, check that the response includes placement.render. A no-fill response is a valid outcome, not proof that installation failed.
## 3. Confirm the visible result
Load /v1/render.js and pass the filled response to wavebird.renderPlacement(). Check that the ad fits its slot and that the hosted renderer records the expected media and beacon events.
- [Run the full testing checklist](/api/testing)
- [Diagnose a failed step](/api/troubleshooting)
# Server-side integration
Keep credentials and placement requests on your backend. Use this guide to check what your server owns.
## Validate the request
Keep the secret key in server-side configuration. Use the project paired with that key, a stable anonymous session ID, and only documented placement fields. Validate browser-supplied values before forwarding them.
## cURL
```bash
curl -X POST https://api.wavebird.ai/v1/placements?wait_ms=1500 \
-H "Authorization: Bearer sk_test_wavebird_demo_secret" \
-H "Content-Type: application/json" \
-d '{
"client_id": "wbproj_demo_8jK42",
"session_id": "sess_demo_123",
"job_type": "chat",
"slots_requested": 1,
"slot_hint": {
"position": "below",
"max_width": 728,
"max_height": 90
},
"overrides": {
"allowed_formats": ["banner", "clip", "native"],
"timing": "during"
},
"consent": {
"semantic_targeting": false,
"prompt_shared": false,
"gdpr_applies": false,
"consent_source": "wavebird_consent"
}
}'
```
## Node
```javascript
const response = await fetch("https://api.wavebird.ai/v1/placements?wait_ms=1500", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json"
},
body: JSON.stringify({
client_id: "wbproj_demo_8jK42",
session_id: "sess_demo_123",
job_type: "chat",
slots_requested: 1,
slot_hint: {
position: "below",
max_width: 728,
max_height: 90
},
overrides: {
allowed_formats: ["banner", "clip", "native"],
timing: "during"
},
consent: {
semantic_targeting: false,
prompt_shared: false,
gdpr_applies: false,
consent_source: "wavebird_consent"
}
})
});
const data = await response.json();
console.log(data);
```
## Python
```python
import json
import urllib.request
payload = json.loads("{\"client_id\":\"wbproj_demo_8jK42\",\"session_id\":\"sess_demo_123\",\"job_type\":\"chat\",\"slots_requested\":1,\"consent\":{\"semantic_targeting\":false,\"prompt_shared\":false,\"gdpr_applies\":true,\"consent_source\":\"wavebird_consent\"}}")
request = urllib.request.Request(
"https://api.wavebird.ai/v1/placements?wait_ms=1500",
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": "Bearer sk_test_wavebird_demo_secret",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request) as response:
print(response.read().decode("utf-8"))
```
## Return only the response needed for rendering
The frontend needs the placement response, not your server credentials. Pass a filled placement to the hosted renderer; handle no-fill and API errors without interrupting the AI task.
- [Complete backend and renderer example](/api/patterns/server-api)
- [Frontend handoff](/api/guides/hybrid)
## Use the hosted measurement path
The hosted renderer records rendered, visibility, click, and playback events for the default flow. Direct server beacons are a separate path for custom rendering or QA.
- [Beacon reference](/api/v1/beacons)
# Client-side integration
Connect Script Tag rendering to your browser page and verify the result.
## Make the slot available
Add wavebird.js and one or more data-wavebird-slot elements. The slot must exist when the script discovers it. Keep its dimensions stable while an ad loads.
## HTML
```html
```
## Activate the correct project
Use a publishable key and configure the exact origin, including the local port. Server secrets must never appear in browser markup.
- [Installation and origins](/api/script-tag)
- [Slot markup](/api/script-tag/slots)
## Check the browser lifecycle
Enable ?wavebird-debug=1 during integration. Confirm activation, required consent, rendering, and beacons. Test a missing or blocked ad response and check that the rest of the page still works.
- [Events and diagnostics](/api/script-tag/events)
- [Browser troubleshooting](/api/script-tag/troubleshooting)
# Hybrid integration
Connect a backend placement response to the browser UI without exposing server credentials.
## Backend creates the placement
Call /v1/placements with the server key. Return the placement, decision, and authoritative_consent fields needed by the hosted renderer. Keep secrets on the server.
- [Copy the complete implementation](/api/patterns/server-api)
## Frontend decides when to display it
Load /v1/render.js. When a filled response is ready and the slot is still relevant, call wavebird.renderPlacement(). Clear the slot for no-fill and retain the returned cleanup function for replacement or removal.
## Keep activation separate
A browser calling browser-authorized endpoints needs the documented activation token. Receiving a placement from your backend does not require sharing its secret key or creating a new browser authentication scheme.
- [Browser activation](/api/v1/browser-activate)
- [Hosted renderer](/api/v1/render-js)
# Consent in consumer AI apps
Place consent handling in the AI interaction without making the AI task depend on an ad.
## Before a sponsored interaction
Collect the permission required for rendering and measurement before requesting the corresponding placement. Keep the AI response independent of whether the user accepts or declines.
- [Choose the consent path](/api/consent)
## When the user decides
Use the selected project's complete backend and frontend lifecycle handlers. Include client_id, the same session_id, the actual lifecycle_state, a positive increasing revision, the profile's genuine decision source and canonical purposes. Verify the returned authoritative snapshot before rendering; source labels and inline flags cannot manufacture a user's decision.
| Project profile | Decision source and responsibility |
| --- | --- |
| Own CMP (own_cmp) | Your application owns the actual per-user, per-conversation choice. Synchronize it as publisher_custom through /v1/consent; placement privacy metadata uses wrapper_cmp. Those labels describe the source, not a grant of permission. |
| Wavebird dialog (wavebird_consent) | Use the genuine Wavebird dialog and synchronize its actual choice as wavebird_dialog. The project requires a current stored record for the authenticated tenant, project and session before placement; inline flags cannot replace it. |
- [Project-scoped ENV, BACKEND and FRONTEND guide](https://dashboard.wavebird.ai/wavebird/overview)
- [Consent endpoint reference](/api/v1/consent)
## One user's conversation lifecycle
Bind consent, placement and withdrawal to the same anonymous session for one user conversation. Create fresh state when the user or conversation changes; never reuse a shared demo session or replay a cached grant.
Grant only after an explicit current choice. Denial, withdrawal, expiry or an unavailable consent state keeps the slot hidden, cancels pending sponsor work and prevents late rendering and ad beacons. Purpose flags do not grant lifecycle permission.
Preserve observed Sec-GPC across the browser and backend. A positive signal blocks advertising admission; do not strip the signal, protocol strings or denied permission to make a request succeed.
Keep the primary chat stream available without waiting for consent or an ad. Request sponsorship only at the saved Before, During or After phase. Clear a During slot when the stream ends; start After work only after completion. Bound each phase's lifetime and cancel it when the conversation changes or its permission is lost.
## When permission changes
A missing, expired, or revoked lifecycle decision prevents rendering and beacons. Disabling only semantic targeting is different: permitted non-personalized product context may remain usable under the active rules. Never use a fallback to bypass required permission.
- [Consent terminology](/api/concepts/consent-model)
- [Consent endpoint](/api/v1/consent)
# Multi-region deployments
Use regional infrastructure deliberately and keep keys, origins, and observability aligned per environment.
## Environment boundaries
Use separate test and live keys. Treat local, staging, and production origins as separate configuration entries.
## Latency
Measure API v1 endpoint latency from every region that requests placements. Keep fallbacks fast when decisions are delayed.
## Data minimization
Do not add region-specific data to job requests unless the config model documents it.
# Going live checklist
Before switching to live traffic, verify keys, origins, config defaults, decisions, beacons, logs, and fallback behavior.
## Keys and origins
Create live keys, store secrets securely, and configure production origins for publishable keys.
## End-to-end flow
Run placement creation, hosted rendering, consent, and beacon tests against live-shaped configuration.
## Production dry-run
Use production-dry-run before any live billing approval. Create a Server Production Dry-run Key in Dashboard API Keys or request an operator-issued sk_dry_... server key, then use it as WAVEBIRD_SECRET_KEY for the existing server integration.
Dry-run proof can show proof_eligible=true and proof_would_be_eligible=true while billable=false, billing_suppressed=true, dry_run_billable_suppressed=true, production_live_approved=false, and non_billable_reason=PRODUCTION_DRY_RUN_NON_BILLABLE.
Production dry-run is non-billable, requires no payout setup, and does not use a production-live-approved mode. Direct server beacons remain DIRECT_BEACON_NOT_BILLABLE unless a separate trusted measurement policy is approved. If no active SSP demand is available, placement creation returns 409 ssp_not_ready with mode=production-dry-run, production_dry_run=true, production_live_approved=false, and billable=false.
## Support readiness
Capture request IDs for failed requests and confirm dashboard logs show enough detail for first-line diagnosis.
# Test API keys
Test keys should be visibly separate from live keys and tied to sandbox/staging data.
## Prefixes and labels
Use a Server Test Key (sk_test_) for backend tests and a Browser Publishable Key (pk_publishable_) from the Test project for browser activation. The publishable prefix is the same across environments; check the project and environment in the dashboard. Do not paste live server keys into examples or test fixtures.
- [Key classes and authentication](/api/reference/authentication)
## Test origins
Add localhost and staging origins to test publishable keys only. Production origins belong on live keys.
## Rotation drills
Practice key rotation before launch so operational ownership is clear.
# Test decisions
Check each placement outcome and the corresponding browser behavior.
## Filled response
Send a valid request from your Test project with current consent. For a fill, check placement and decision.fill, then pass the response to the hosted renderer. Confirm the media renders before treating the ad as displayed.
## Controlled no-fill
With a Server Test Key, append test_outcome=no_fill to /v1/placements. Expect decision.fill: false, placement: null, and test_controlled_no_fill. The UI should clear the slot and keep the AI task running. Remove the parameter for normal requests.
- [Test prerequisites and restrictions](/api/testing)
## Pending or not-ready response
When no placement is ready, leave the slot empty. Do not pass an incomplete response to the renderer or delay the AI output.
- [Placement response fields](/api/v1/placements)
## Measurement after a fill
Check the expected render and visibility events once the ad is displayed. Retrying the same event must not double-count it.
- [Beacon events and idempotency](/api/v1/beacons)
# Simulating errors
Test auth, origin, rate-limit, validation, and unknown project failures.
## Auth failures
Call a secret-key endpoint without auth and with a publishable key. Expect actionable 401 or 403 errors.
## Origin mismatch
Use a publishable key from an unlisted origin and confirm the runtime `forbidden` error is visible to the developer.
## Rate limits
Generate repeated calls against a test key and confirm 429 includes Retry-After.
## Expected negative tests
Intentional sandbox checks for missing asset_token, missing slot_id, invalid event, stale occurred_at, or invalid consent purpose should return actionable 400 validation errors with request IDs and field paths. These are not production integration failures when deliberately triggered.
Stale direct server beacon timestamps can return BEACON_TOO_LATE. Generate occurred_at at runtime with new Date().toISOString() for every beacon event.
# End-to-end test harness
Test your integration with synthetic data and Test Keys, then inspect placement responses, rendering, and event outcomes.
## Local stack
Use your existing application and the dashboard's Test Keys with synthetic sessions. The internal full-stack runbook is for wavebird maintainers; publishers do not need a copy of that infrastructure.
## Smoke tests
Exercise filled, no-fill, timeout, and consent-denied cases in your application. Confirm that the AI task completes when the ad path is unavailable.
## Evidence
Inspect the dashboard logs for request and event outcomes. Keep test results separate from live revenue, and redact keys and user content when sharing a support report.
# Error codes
Errors should tell you what went wrong, what to do next, where to read more, and the request_id for support.
## Envelope
JSON errors include an error code, message, docs_url when available, and request_id. The same request ID is returned in X-Request-Id.
## Common codes
Runtime error codes are lowercase. `unauthorized` means add valid credentials, `forbidden` means use the correct key type or origin, and `rate_limited` means back off using Retry-After.
## validation_error
validation_error means fix request body shape.
## consent_not_current
The project requires current authoritative lifecycle consent. Record the actual session decision through /v1/consent; inline flags do not replace required stored consent. semantic_targeting may remain false for permitted non-personalized product context; it does not authorize prompt-derived targeting or bypass required lifecycle permission.
## consent_state_unavailable
Consent storage could not be read. Do not render or beacon, keep the primary app flow working, and retry later.
## privacy_signal_policy_required
A positive Sec-GPC signal blocks new advertising admission (403). Preserve observed signals across backend hops and continue the primary app flow without ads; do not retry by stripping the signal.
## TCF and GPP admission
Current job and placement admission returns validation_error (400) for external TCF/GPP protocol inputs, including structurally valid strings without an authoritative policy mapping. Field availability does not establish an enabled integration.
## not_found
not_found means the resource is missing or outside the authenticated scope.
## Support
Include request_id, endpoint, key mode, and environment when asking for help. Never include full API keys.
# Rate limits
Rate limits are applied per key bucket. 429 responses include Retry-After.
## Buckets
Test keys and production keys use separate operational buckets. Do not assume test traffic exercises live limits.
## 429 behavior
When limited, retry after the Retry-After header and avoid tight polling loops on decisions.
## Diagnose repeated limits
Record the endpoint, request ID, and Retry-After value. Check request frequency in your application and any available dashboard logs. Do not rely on a dashboard usage meter to decide when to retry.
# CORS and allowed origins
Browser-callable endpoints allow preflight but actual requests still enforce DB-stored allowed origins.
## Publishable keys
Publishable-key requests require an allowed Origin header. Add exact origins in the dashboard, including scheme and port.
## Secret keys
Secret-key calls are server-to-server and should not be made from browsers. Origin checks do not make secret keys browser-safe.
## Preflight
OPTIONS preflight should succeed for candidate origins so browsers can reach clear actual-request errors when the origin is not authorized.
# Request IDs
Every API response should carry a request ID in JSON and X-Request-Id.
## Capture IDs
Log request IDs beside your own trace ID when API calls fail.
## Share safely
Request IDs are safe to send to support. API keys and user identifiers are not.
## Use an ID in a support request
Include the request ID, endpoint, time of the request, and the behavior you expected. This identifies the failed operation without sharing credentials or user content.
# Callbacks
Callback delivery is an advanced integration pattern for handing a decision to your own endpoint.
## Availability
Public webhook subscriptions, an event catalog, signing secrets, a webhook retry policy, and a webhook sandbox are not documented API v1 features.
## Use the callback pattern
Use callback delivery only if your integration already requires a controlled callback endpoint. This is separate from Script Tag events raised in the browser.
- [Callback integration pattern](/api/patterns/callback)
- [Browser events](/api/script-tag/events)
## Choose the standard flow otherwise
For a new backend integration, request a placement and pass a filled response to the hosted renderer.
- [Backend integration](/api/patterns/server-api)
# Reliability
Keep the AI task running when an ad request fails, and use request IDs to investigate the failure.
## Status and support
Use request IDs and dashboard logs to diagnose incidents. Public SLA numbers are not documented in v1.
## Publisher fallbacks
Your app should tolerate no-fill, delayed decisions, and transient API errors without breaking inference flows.
## IP allowlist
IP allowlist behavior is not documented as a public v1 feature. Use key and origin controls as the supported public boundary.
# Changelog
Changes to API routes, browser integration, and compatibility support.
## 2026 Q2
Added canonical API v1 routes, browser activation, docs-linked error envelopes, Script Tag debug diagnostics, SDK migration guidance, and compatibility aliases for existing SDK/browser paths.
## Integration guidance
New integrations can use REST or Script Tag directly. Existing SDK integrations remain supported through the documented compatibility routes.
- [Migration guide](/api/migration)
## Reference
Check the versioning policy for compatibility rules and the API stability page for endpoint status.
- [Versioning](/api/reference/versioning)
- [API stability](/api/reference/stability)
## Script Tag
See [Script Tag installation Markdown](https://wavebird.ai/api/script-tag.md) and [the Script Tag pattern](https://wavebird.ai/api/patterns/script-tag).
## SDK documentation
# SDK installation
Install wavebird when your integration needs package-level control over jobs, decisions, rendering helpers, consent, or beacons.
Install the wavebird SDK package.
## Install
The SDK is the package path for teams that want direct control in app code. Hosted API and Script Tag paths remain available when you want wavebird to own more of the rendering flow.
```bash
npm install wavebird
```
## Create an account
Create a wavebird account at https://dashboard.wavebird.ai/wavebird-start, complete onboarding, and create a project before you run SDK calls. The dashboard is where you copy sandbox secret keys, publishable keys, live keys, and configure browser allowed origins.
## Runtime
The Node/server client is intended for backend use with secret keys. Start with a sandbox `sk_test_...` key in `WAVEBIRD_SECRET_KEY`; switch to a live `sk_live_...` key only after production readiness is approved in the dashboard. Browser package integrations use publishable keys plus activation and allowed-origin checks. The package requires Node.js 20 or newer for server usage.
## Browser keys
Use `NEXT_PUBLIC_WAVEBIRD_PUBLISHABLE_KEY` only with a publishable key. Publishable keys are browser-safe, but they work only from origins configured in the dashboard. Never put a server secret key in browser code.
## Versioning
Pin the package version in production and review the migration guide before changing SDK-first integrations. npm README changes are delivered with package releases, so verify the published package page after each version bump.
# Migration from pre-transition SDK
Existing SDK integrations keep working, but new rollouts should prefer the Server API default first.
Move SDK-first integrations to API-first or Script Tag paths.
## Hosted path
Server API integrations can call POST /v1/placements?wait_ms=1500 and render with the hosted render.js flow when they do not need the SDK job-plus-decision sequence in app code.
- Use /api/quickstart for hosted defaults.
- Use /api/patterns/server-api for the backend/frontend split.
- Use the SDK when package methods provide the control your app needs.
## Existing SDK method compatibility
Existing WavebirdClient methods remain mapped to lower-level compatibility routes: createJob maps to POST /v1/jobs, getDecision maps to GET /v1/decisions/{slot_id}, and sendBeacon maps to POST /v1/beacons.
- Do not present these compatibility mappings as the shortest onboarding path.
- Use the hosted renderer for browser media rendering whenever possible.
- Keep the SDK when a TypeScript wrapper reduces local migration risk.
## Breaking behavior to check
Verify key type, allowed origins, server-stored project config, beacon field names, and error envelopes. Compatibility aliases remain in place but should not be used for new examples.
## Deprecation timeline
The SDK is not removed in v1. Deprecated helpers remain callable and should emit guidance toward API-first or Script Tag paths.
# SDK methods
Jobs, decisions, generation events, and beacons with typed fail-silent returns and onError callbacks.
Public methods exposed by WavebirdClient.
## createJob(params)
Creates an ad opportunity through POST /v1/jobs and returns accepted job metadata, rate-limit state, or null on fail-silent failure.
## getDecision(slotId)
Reads decisions through /v1/decisions/{slot_id}, using WebSocket where configured and polling as fallback.
## sendBeacon(beacon)
Records delivery events through /v1/beacons when possible and falls back to wrapper beacon routes when needed.
## reportGeneration(jobId, event)
Reports generation lifecycle events so the ad path can be correlated with the AI surface lifecycle without changing model prompts or outputs.
# React components
The SDK exposes React and DOM helpers for teams that want package-level creative rendering.
React component status for the advanced SDK.
## Component status
Use `wavebird/react` for the React creative renderer and `wavebird/mount` for DOM mounting when you want your app to own package-level rendering.
## Alternatives
Use hosted render.js or the Script Tag path when you want wavebird to own more of the browser rendering flow.
## Public guides
# wavebird
## Quick answer
wavebird is ad infrastructure for consumer AI apps, founded by Mario von Bassen and operated by MC Squared UG in Munich, Germany. It connects applications to global ad demand through one integration, with labeled placements kept separate from model instructions and generated output.
## What wavebird is
wavebird serves teams building consumer AI products: chat and assistants, music and audio tools, image, video and 3D generation, app builders and other AI experiences. Product, growth, founders and finance can evaluate the economics and controls; developers use the API and SDK to connect the app.
The API and SDK give the publisher control over when an ad is requested, where it can render, which formats are allowed, and how no-fill behavior should work.
## How it works
wavebird runs ad selection in parallel with model generation, so the model response path does not wait for an ad decision.
The app keeps the AI answer primary, and any sponsored placement renders in a separate slot the publisher controls.
wavebird product documentation for delivery and configuration.
- [wavebird · placement configuration](https://wavebird.ai/api/configuration)
- [wavebird · SDK fallback behavior](https://wavebird.ai/sdk/graceful-degradation)
## Privacy and proof
The default delivery path is data-minimizing: prompts and chat history are not sent to SSPs, DSPs, advertisers, or other ad partners.
wavebird reduces allowed context into delivery metadata, applies consent and blocking rules, and records tamper-evident proof for billable impressions.
wavebird technical documentation and local measurements.
- [wavebird · SDK privacy boundaries](https://wavebird.ai/sdk/privacy)
- [wavebird · delivery proof & limits](https://wavebird.ai/resources/proof-integrity)
- [wavebird · measurement evidence](https://wavebird.ai/resources/evidence)
## European roots. Global ad demand.
Built in Europe, wavebird gives consumer AI apps access to the global advertising market through established advertising partners. Publishers do not need to build their own advertiser sales operation. Demand, eligibility rules and geography affect which ads are available.
wavebird acts between publishers and advertisers. Publishers need control over their product and revenue; advertisers need suitable environments and transparent measurement. The infrastructure supports that exchange through placement controls, data minimization and traceable delivery records.
## Helping shape advertising in AI
As a pioneer in advertising for consumer AI, wavebird develops both practical infrastructure and principles for how ads can fund AI without influencing its output. Its mechanism for ads during generation is patent pending; a patent has not been granted.
Founder Mario von Bassen co-authored the February 2026 Compute Sponsoring paper with Christian Leinberger. The current September 2026 publication, by Christian Leinberger, proposes a category definition linking an ad exposure to an identifiable compute unit. It sets out separation, data sovereignty and consent principles. It is a published proposal, not a formally adopted industry standard. Read the [current paper](https://compute-sponsoring.org/) or the [February 2026 version](https://compute-sponsoring.org/versions/1.0).
The implementation is documented in the [API and SDK guides](https://wavebird.ai/api). The [engineering evidence](https://wavebird.ai/resources/evidence), [proof-integrity explanation](https://wavebird.ai/resources/proof-integrity) and [data protection review](https://wavebird.ai/resources/dpia) describe the tests, records and safeguards behind it.
## What wavebird does not do
wavebird is not an SSP, DSP, ad network, website ad server, analytics platform, or a system that rewrites model prompts or model output.
- It does not send prompts or chat history to ad partners.
- It does not make sponsor information part of the model request or response.
- It lets the publisher choose suitable placements and timing within consent, safety and partner requirements.
## Main paths
See [how it works](https://wavebird.ai/how-it-works) and the [ad revenue guide](https://wavebird.ai/guides/ad-revenue) to evaluate your product's business case. Share the [API docs](https://wavebird.ai/api) with your engineering team when planning the integration.
Use [Contact](https://wavebird.ai/contact) for product, commercial, privacy or technical questions.
# What are ads worth in your AI app?
See how we estimate your ad revenue, explore the assumptions and compare a generation’s revenue with its AI costs. External campaign reports provide market context.
## Plan per generation
Choose a text model and its natural wait. Adjust CPM or CPC, fill and billable usage to compare expected publisher revenue with generation costs. The result is a scenario, not a guaranteed payout.
## What changes the result?
### Audience & market
Geography, advertiser budgets and purchasing intent affect bids. A focused niche can have different prices and a smaller pool of relevant ads.
### Placement & format
Use the time your product already needs. Multiple clips may fit a longer natural wait; a stable banner remains one placement. Never delay a ready result.
### Context & consent
Topics, safety rules and privacy settings determine matching options.
### Delivery & viewability
Timing, fill and visibility affect qualified views. Completed playback and deliberate, valid clicks matter when the agreement pays for those outcomes.
References for auction context and visibility; they do not establish a CPM premium for any niche.
- [IAB Tech Lab · OpenRTB 2.6](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md)
- [Google · Active View methodology](https://support.google.com/google-ads/answer/17262722?hl=en)
## What do AI ads cost today?
Published ChatGPT advertiser results. These are individual self-reported campaigns, not a market average or wavebird publisher payouts. CPM and effective CPC can describe the same spend and must not be added together.
### Synter
2–10 Jun 2026. US, UK, Canada, Australia, New Zealand. 129,663 impressions · 448 clicks. CPM: $34.16; average CPC: $9.89; CTR: 0.35%. Impression-billed campaign; CPC is the effective cost per click.
### VSSL
Published 12 Jun 2026. Market not specified. Two-week test · $400 reported spend. CPM: $61.62; average CPC: $11.65; CTR: 0.53%. Small advertiser test; the report does not specify the purchasing basis.
### SE Ranking
3–15 Jun 2026. US, Canada, Australia, New Zealand. 97,131 impressions · 1,263 clicks. CPM: not reported; average CPC: $3.16; CTR: 1.3%. Manual CPC campaign; no CPM quoted here because the report does not publish one.
- [Synter campaign report](https://syntermedia.ai/blog/chatgpt-ads-real-data)
- [VSSL campaign report](https://vsslagency.com/our-work/chatgpt-ads-what-were-seeing-so-far/)
- [SE Ranking campaign report](https://seranking.com/blog/how-to-advertise-on-chatgpt/)
## Cover inference costs. See what’s left.
Compare the same activity over the same period: a chat, image, audio minute or video render. Include model or GPU costs and allocated storage or processing. Assess support, acquisition and other operating costs separately.
```text
Cost coverage = publisher revenue per task ÷ compute cost per same task
```
The configurator prices text generations. Surplus is expected publisher revenue minus entered costs, not a guaranteed business profit. Add other operating costs to assess your own margin.
## Same model. Different opportunities.
Compare the current task and three illustrative text workloads across six audience scenarios. Quick response: 1,000 input, 500 visible output, zero reasoning, 10-second wait. Reasoned response: 4,000 input, 1,000 visible output, 2,000 billed reasoning, 30-second wait. Deep analysis: 16,000 input, 1,000 visible output, 6,000 billed reasoning, 60-second wait. These are separate workload and latency assumptions, not measured model speeds.
All audience columns start with our illustrative $15 CPM after advertising partner fees and before wavebird’s share. The $0.60 CPC and 0.5% CTR are separate illustrative click assumptions. Selecting an external report applies its advertiser prices as a comparison, not a wavebird payout forecast; individual audience values remain editable. Fill and other delivery inputs are illustrative, not measured category rates. A higher product price or a specific topic does not guarantee a higher bid; narrower demand may reduce fill. Each cell shows surplus or gap per 1,000 generations for the selected model, using the same eligibility, format, cap, qualification and later-position reach. Selecting a cell applies its workload and audience assumptions.
Clip sequences are conditional on the actual integration and commercial agreement. Loading, clip duration and the selected payment event are separate. Each next clip starts only after the previous scheduled clip and its own loading time. A maximum of two clips and 80% retained audience for the second are illustrative planning choices, not industry standards. All positions use the same entered price, fill and click rate. More wait does not automatically increase these rates. vCPM and CPCV require a separately entered price; the campaign reports do not establish those prices.
### General chat
Broad everyday queries; diverse advertiser relevance. Illustrative planning inputs: $15 CPM / $0.6 CPC, 0.5% CTR, 50% fill.
### Film & creation
Editing software, cameras and production tools. Test a higher bid alongside narrower demand. Illustrative planning inputs: $15 CPM / $0.6 CPC, 0.5% CTR, 45% fill.
### Music
Instruments, audio software and studio equipment. Purchasing intent matters more than the topic alone. Illustrative planning inputs: $15 CPM / $0.6 CPC, 0.5% CTR, 45% fill.
### Video games
Games, peripherals and hardware. Entertainment browsing and an active purchase can have different value. Illustrative planning inputs: $15 CPM / $0.6 CPC, 0.5% CTR, 50% fill.
### High-value products
Specialist equipment and expensive products. Test higher bids against a smaller pool of relevant advertisers. Illustrative planning inputs: $15 CPM / $0.6 CPC, 0.5% CTR, 35% fill.
### Fashion
Clothing and accessories. A specific product search may differ from general style inspiration. Illustrative planning inputs: $15 CPM / $0.6 CPC, 0.5% CTR, 45% fill.
- [Factors affecting ad revenue](https://adsense.google.com/start/)
- [Video ad sequences and duration budgets](https://support.google.com/admanager/answer/6399249?hl=en)
## Model prices behind the cost comparison
Prices checked 19 Sep 2026. USD per million text tokens, standard online inference with uncached input. DeepSeek uses peak-hour rates; Muse uses the Standard tier. Hosting providers and model variants are specified below. These are a snapshot, not a live feed. Quality and task suitability are not ranked.
The initial focus is gpt-oss-120B, an efficient hosted model example; other models may leave a funding gap under the same assumptions. The default example uses 4,000 input and 1,000 visible output tokens with no additional billed reasoning, our illustrative $15 CPM after advertising partner fees, 100% eligible generations, 50% fill, 100% qualified exposures, a natural 30-second wait, 15-second clips, 1 second loading per clip, a two-clip cap and 80% audience retained for the second clip. In this CPM scenario rendered opportunities occur at 1 and 17 seconds; extra wait adds no more after that limit. Two positions yield 0.9 expected qualified exposures per generation. At 90% publisher share and no additional costs, gpt-oss-120B yields $12.15 expected publisher revenue minus $1.20 entered costs, leaving $10.95 per 1,000 generations. The default CPM is a wavebird planning assumption. External advertiser reports remain separate comparisons, not net publisher prices or wavebird payout forecasts. Equal token counts compare unit prices; actual tokenization and reasoning usage differ between models.
Generation cost = (input tokens × input price + (visible output + billed reasoning tokens) × output price) ÷ 1,000,000 + additional costs. Expected qualified exposures = eligibility × sum of position reach × fill × qualified share. For position i, starting at zero: start = i × (clip length + loading) + loading. CPM and CPC opportunities begin at render; the illustrative vCPM scenario adds 2 seconds of continuous playback at 50% visibility, and CPCV adds the entire clip duration. Only events reached within the natural wait are eligible for the entered qualification rate. A stable banner counts at most once, after loading for CPM/CPC or after another second at 50% visibility for the illustrative vCPM scenario; it never refreshes here. The external viewability reference uses 1 second for display and 2 seconds for video at 50% visibility. The current wavebird qualified-view rule uses 1 continuous second at 50% visibility; actual billing follows the activated agreement. A render alone does not establish billability. CPM/vCPM revenue = price ÷ 1,000 × expected events × publisher share. CPC revenue = price × valid click rate × expected events × publisher share. CPCV revenue = price per full completion × expected validated completions × publisher share. These are alternative models, not additive revenues. Percentages are fractions in these formulas. All generations incur costs, including those without an ad. Rates and source assumptions can be changed in the configurator. This is a fixed-duration scenario; a measured wait-time distribution must be evaluated across its durations, not reduced to its mean before applying event thresholds. The 6, 15 and 30 second clip options follow IAB Tech Lab format guidance; 15 seconds is not a measured AI-ad average.
### DeepSeek V4.1 Flash
DeepSeek API: $0.3 input / $1.2 output per million tokens. Peak-hour rate; off-peak input/output rates are half.
### DeepSeek V4 Pro 0813
DeepSeek API: $1.32 input / $3.96 output per million tokens. Peak-hour rate; off-peak input/output rates are half.
### Muse Spark 1.3
Meta Model API: $1.25 input / $4.25 output per million tokens. Standard tier. The discounted Contributor tier is excluded.
### Grok 4.6
xAI API: $2 input / $6 output per million tokens. Global endpoint, short context; no search or tool charges included.
### GPT-5.6 Luna
OpenAI API: $0.2 input / $1.2 output per million tokens. Standard processing, short context; no regional uplift.
### GPT-5.6 Terra
OpenAI API: $2 input / $12 output per million tokens. Standard processing, short context; no regional uplift.
### GPT-5.6 Sol
OpenAI API: $4 input / $20 output per million tokens. Standard short-context promotional rate, available at least through 21 Nov 2026.
### Claude Sonnet 5
Claude API: $2 input / $10 output per million tokens. Standard direct API rate; no cache, batch or regional adjustments.
### Claude Haiku 4.5
Claude API: $1 input / $5 output per million tokens. Standard direct API rate; no cache, batch or regional adjustments.
### Gemini 3.7 Flash
Gemini API: $0.75 input / $3.75 output per million tokens. Standard paid tier through 31 Dec 2026; announced 2027 rates are double.
### Gemini 2.5 Flash-Lite
Gemini API: $0.1 input / $0.4 output per million tokens. Standard paid text rate; free tier and caching excluded.
### Llama 3.3 70B
Together AI: $1.04 input / $1.04 output per million tokens. Hosted open-weight model: Instruct Turbo, FP8. This is not a self-hosting cost.
### Qwen3.5 9B
Together AI: $0.17 input / $0.25 output per million tokens. Hosted open model. This is not a self-hosting cost or a quality ranking.
### gpt-oss-120B
Together AI: $0.15 input / $0.6 output per million tokens. Hosted open model. Include billed reasoning tokens in output.
- [DeepSeek V4.1 Flash pricing source](https://api-docs.deepseek.com/quick_start/pricing/)
- [DeepSeek V4 Pro 0813 pricing source](https://api-docs.deepseek.com/quick_start/pricing/)
- [Muse Spark 1.3 pricing source](https://dev.meta.ai/docs/pricing-rate-limits)
- [Grok 4.6 pricing source](https://docs.x.ai/developers/pricing)
- [GPT-5.6 Luna pricing source](https://developers.openai.com/api/docs/pricing)
- [GPT-5.6 Terra pricing source](https://developers.openai.com/api/docs/pricing)
- [GPT-5.6 Sol pricing source](https://developers.openai.com/api/docs/pricing)
- [Claude Sonnet 5 pricing source](https://platform.claude.com/docs/en/about-claude/pricing)
- [Claude Haiku 4.5 pricing source](https://platform.claude.com/docs/en/about-claude/pricing)
- [Gemini 3.7 Flash pricing source](https://ai.google.dev/gemini-api/docs/pricing)
- [Gemini 2.5 Flash-Lite pricing source](https://ai.google.dev/gemini-api/docs/pricing)
- [Llama 3.3 70B pricing source](https://www.together.ai/models/llama-3-3-70b)
- [Qwen3.5 9B pricing source](https://www.together.ai/models/qwen3-5-9b)
- [gpt-oss-120B pricing source](https://www.together.ai/models/gpt-oss-120b)
- [IAB Tech Lab video format guidance](https://iabtechlab.com/wp-content/uploads/2022/03/Ad-Format-Guidelines_DV-CTV.pdf)
- [Active View measurement thresholds](https://support.google.com/google-ads/answer/17262722?hl=en)
- [Format-specific video billing examples](https://support.google.com/google-ads/answer/2375464?hl=en)
- [Better Ads short-form video research](https://www.betterads.org/press-releases/cba-announces-better-ads-standard-short-form-video)
- [Ad Manager refresh rules](https://support.google.com/admanager/answer/6286179?hl=en)
## Your revenue. Your share.
No upfront fees.
The standard publisher share is 90%; the wavebird fee is 10%. For $1,000 eligible settled revenue: $900 to you / $100 wavebird fee.
Current standard terms, before any further permitted adjustments. A written commercial schedule can set different terms. Apply the publisher share once to the eligible settled amount for the agreed model, rather than treating an advertiser price as publisher take-home revenue.
wavebird commercial terms, not an industry benchmark.
- [wavebird · publisher terms](https://wavebird.ai/terms)
## Fees, settlement and pricing details
The $1,000 split assumes no further adjustments. Invalid traffic, refunds, chargebacks, required tax withholding and other permitted offsets can affect the final amount. Your applicable terms and settlement records determine the payout.
For fixed sponsorship and CPD, compare recorded delivery with the booked commitment. The agreed remedy can be an extension, replacement delivery or a proportional credit. Recorded delivery is evidence for reconciliation; it does not turn the agreement into CPM.
A billed advertiser amount, eligible publisher revenue and a payout are different stages. Reporting estimates and test activity are not payable funds; the applicable settlement records determine what is available for payout.
- [Read the terms](https://wavebird.ai/terms)
## From an opportunity to revenue.
### 1 Placement opportunities
Moments when your app could show an ad.
`Starting opportunities`
### 2 Eligible placements
Pass your placement, consent and safety rules.
`× eligibility rate`
### 3 Filled placements
An advertising partner returns an ad.
`× fill rate`
### 4 Billable impressions
For impression-priced ads, only delivery that meets the agreed measurement rules counts.
`× billable-impression rate`
```text
Publisher revenue = billable impressions ÷ 1,000 × CPM × publisher share
```
Impression-based example. Bar lengths are schematic, not measured rates. CPC and CPCV count qualified clicks or completed views; fixed sponsorship and CPD use the booked contract quantity.
## Six ways to price an ad.
The price model sets the unit the advertiser pays for. The qualification rule determines what counts. wavebird’s billing system supports the six models below; the model used for your integration depends on the commercial agreement and its activation.
### CPM Cost per 1,000 impressions
Counts impressions that meet the agreed measurement policy. A request, selected ad or raw render alone is not a billable impression. CPM does not automatically mean the stricter vCPM rule.
`Billable impressions ÷ 1,000 × CPM`
### vCPM Cost per 1,000 viewable impressions
Counts qualified views. wavebird’s current rule requires at least 50% of the ad to be visible for at least 1 second of verified visibility while the document is visible.
`Qualified views ÷ 1,000 × vCPM`
### CPCV Cost per completed view
Counts a full video Clip completion validated by the server. A partial play, a dismissed ad or an unverified completion report does not count. The price is per completion, not per thousand.
`Validated completions × CPCV`
### CPC Cost per qualified click
Counts a deliberate click on the ad or its call to action, backed by a verified, single-use click record. Duplicate or invalid clicks are excluded. Dismissal, feedback and player controls do not count.
`Qualified clicks × CPC`
### Fixed Fixed sponsorship
One agreed price for a booked campaign period. Delivery is measured separately; impressions or clicks do not silently recalculate the booked price. Any delivery shortfall is handled under the agreement.
`1 booked campaign × agreed price`
### CPD Cost per day
An agreed daily price multiplied by the booked day count. It uses contract days, not days inferred from impression activity. Delivery commitments and remedies remain part of the agreement.
`Booked days × daily price`
These formulas show the amount before publisher share and permitted adjustments. A listed model is not automatically active for every integration. eCPM is a comparison metric, not a seventh billing model.
External measurement and video-billing references. wavebird rules and the signed agreement determine the actual billable event.
- [Google · Active View methodology](https://support.google.com/google-ads/answer/17262722?hl=en)
- [Google · video ad billing](https://support.google.com/google-ads/answer/2375464?hl=en)
- [wavebird · delivery proof & limits](https://wavebird.ai/resources/proof-integrity)
## Six worked examples
Separate illustrative agreements, all in USD. These are invented prices, not wavebird rates or market benchmarks. The publisher amounts use the standard 90% share and assume the full amount is eligible and settled, with no further adjustments.
### CPM
100,000 billable impressions at $5 CPM.
`100,000 ÷ 1,000 × $5 = $500 → $450 publisher`
### vCPM
60,000 qualified views at $8 vCPM. Other delivered ads do not count toward this quantity.
`60,000 ÷ 1,000 × $8 = $480 → $432 publisher`
### CPCV
4,000 validated full completions at $0.04 per completion.
`4,000 × $0.04 = $160 → $144 publisher`
### CPC
500 qualified clicks at $0.60 per click. Repeated or invalid clicks add no units.
`500 × $0.60 = $300 → $270 publisher`
### Fixed
One campaign booked at $1,000, with its delivery obligations met.
`1 × $1,000 = $1,000 → $900 publisher`
### CPD
Seven booked days at $100 per day, with the agreed obligations met.
`7 × $100 = $700 → $630 publisher`
Do not add these examples together or bill the same event under several models. Each describes a separate commercial obligation.
Shortfall example: a $1,000 fixed campaign delivers 750 of its 1,000 agreed units. If the contract specifies a proportional credit, the credit is $250 and the adjusted amount is $750. At the illustrative 90% share, that leaves $675 for the publisher once eligible and settled. A contract with an extension or replacement-delivery remedy follows that remedy instead.
## Measure more than the price.
Use billing units to calculate money and performance metrics to understand delivery. Compare the same period, currency and placement scope, and state whether revenue is gross or publisher net.
### eCPM Effective revenue per 1,000 impressions
A comparison metric across pricing models. For publisher net eCPM, use net publisher revenue and the measured impressions for that same activity. It does not convert a click or booked day into an extra billable impression.
`Publisher net revenue ÷ impressions × 1,000`
### Fill rate Demand coverage
The share of eligible ad opportunities that receive an ad. Filled does not mean viewed, completed or billable.
`Filled placements ÷ eligible opportunities × 100%`
### Qualified-view rate Visibility that meets the policy
Qualified views divided by measured rendered impressions. Name the visibility policy and denominator when comparing results.
`Qualified views ÷ rendered impressions × 100%`
### Qualified CTR Clicks that pass qualification
Qualified clicks divided by measured impressions. Raw click counts can be useful for diagnostics, but cannot replace the validated count used for CPC billing.
`Qualified clicks ÷ impressions × 100%`
### Completion rate Full playback
Validated full completions divided by playback starts for the same Clip scope. The rate explains performance; only the eligible completion count creates CPCV units.
`Validated completions ÷ playback starts × 100%`
### Delivery progress A booked commitment
For fixed sponsorship or CPD, compare verified delivery with the agreed goal in its original unit. Report booked value and any credits separately.
`Verified delivery ÷ agreed delivery goal × 100%`
If a denominator is zero or has not been measured, the rate is unavailable, not 0%. Do not mix qualified views, playback starts, clicks, sessions or booked days as though they were the same unit.
## AI-specific measurement details
The billing models above determine the payable unit. The AI-specific concepts below help compare sponsorship with generation cost; they are proposals, not additional implemented billing models or established industry standards.
### CPG · Cost per generation
Cost of sponsoring one eligible generation.
`Sponsor spend ÷ sponsored generations`
### CPT · Cost per token
Cost of sponsoring one token.
`Sponsor spend ÷ sponsored tokens`
### CPT 1K · Cost per 1,000 tokens
Cost of sponsoring 1,000 tokens.
`Sponsor spend ÷ sponsored tokens × 1,000`
### CPW · Cost per wait
Cost of sponsoring one generation wait window, regardless of its duration.
`Sponsor spend ÷ sponsored wait windows`
### RPG · Revenue per generation
Publisher revenue per eligible generation, for comparison with its compute cost.
`Publisher ad revenue ÷ eligible generations`
### RPT · Revenue per 1,000 tokens
Publisher revenue per 1,000 generated tokens, for comparison with token-level compute cost.
`Publisher ad revenue ÷ generated tokens × 1,000`
### Sponsored Generation Rate
Share of eligible generations with a valid sponsored exposure.
`Sponsored generations ÷ eligible generations`
Token volume alone does not prove ad exposure. Independently qualified clips may fit within a natural wait; do not extend generation or delay a ready result to create more ad time. Repeated frames are not new ad impressions.
A published proposal, not an established advertising currency or independent validation of wavebird.
- [Compute Sponsoring · category & measurement proposal](https://compute-sponsoring.org/)
## Access to advertising demand
Connect to the global advertising market through established partners, without building your own ad-sales operation. Your app sets the placement; wavebird checks, requests, returns and measures the ad.
An OpenRTB auction bid remains CPM-based unless the partner protocol explicitly defines another basis. It does not by itself establish a CPC or CPCV agreement. Direct fixed sponsorship and CPD follow the advertiser’s booked contract.
OpenRTB defines bid prices in CPM; a different billing agreement needs its own terms.
- [IAB Tech Lab · OpenRTB 2.6](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md)
## How we estimate your revenue
Our illustrative scenario assumes $15 per 1,000 billable impressions after advertising partner fees. You keep 90% after wavebird’s share. Eligibility, ad availability and billable delivery are included in the calculation.
Billable impressions = monthly active users × sessions per user × opportunities per session × eligibility × fill × billable delivery. Publisher revenue = billable impressions ÷ 1,000 × $15 × 90%. Partner fees are already included in the $15 basis; wavebird’s share is applied once.
These are planning assumptions, not measured Consumer AI averages or guaranteed payouts. A session is a potential ad opportunity, not necessarily one generation. The separate generation configurator uses its own delivery settings, including clip limits and later-ad reach.
### Monthly active users
312,000 in the initial example; adjustable on the landing page.
### Sessions and opportunities
15 sessions per user per month; one potential opportunity per session.
### Eligibility
80% of potential opportunities are suitable for an ad.
### Ad availability
50% of eligible opportunities receive an ad.
### Billable delivery
80% of filled opportunities qualify for billing.
### Price and publisher share
$15 per 1,000 billable impressions after advertising partner fees, then 90% to you.
Example: 312,000 × 15 × 1 × 80% × 50% × 80% = 1,497,600 billable impressions. At $15 per 1,000 and a 90% share, that is $20,217.60 per month. The landing page displays approximately $20,200.
Annual revenue = unrounded monthly revenue × 12: $242,611.20, displayed as approximately $243,000. This annualization assumes the same traffic and delivery conditions each month. The effective publisher revenue is $0.00432 per potential opportunity.
External benchmarks provide market context. Appodeal’s 2025 report covers Q4 2024 mobile gaming and varies by format and country; rewarded ad prices do not establish wavebird’s format mix. IAS reports viewability, not our billable-delivery rate. Google explains ad metrics and match-rate factors. Playwire reports on its own publisher data. None validates this scenario as a Consumer AI average or wavebird payout benchmark.
The separate Fit Finder retains its own planning range of $0.0018–$0.0162 per planned monetizable session, already after wavebird’s share. Its monthly estimate is audience × sessions × free-usage share × that range. Audience choices are 5k / 50k / 300k / 750k / 1.5M, session choices are 2 / 6 / 20 / 60 and free-usage shares are 75% / 50% / 20%. Do not apply the publisher share again to that range.
- [Open the landing calculator](https://wavebird.ai/#revenue-calculator)
External context reviewed September 27, 2026. The calculator’s numerical assumptions are wavebird planning inputs, not values established by these sources.
- [Appodeal · 2025 eCPM report](https://appodeal.com/wp-content/uploads/2025/03/Appodeal-The-Latest-eCPM-Report-2025.pdf)
- [IAS · 2026 Media Quality Report](https://integralads.com/news/ias-21st-media-quality-report/)
- [Google AdMob · metrics glossary](https://support.google.com/admob/table/16327896?hl=en-GB)
- [Google AdMob · match rate](https://support.google.com/admob/answer/9655701?hl=en-GB)
- [Playwire · publisher ad revenue 2026](https://www.playwire.com/2026-state-of-publisher-ad-revenue-report)
# Subscriptions, credits or ads?
An AI app can combine these models according to usage costs, user preferences and suitable advertising opportunities.
## In brief
Subscriptions fund recurring access, credits charge against a usage balance, and advertising can help fund eligible usage without charging the user directly.
## Compare the models.
### Subscriptions
Best suited to: Regular users who value ongoing access. What it offers: Recurring revenue and a clear paid tier. Trade-off: Upfront commitment can deter casual users. Heavy usage still needs cost controls.
### Credits / pay-per-use
Best suited to: Occasional tasks or generations with different costs. What it offers: Charges aligned with usage and the value of each task. Trade-off: Top-ups and per-use costs can make casual users hesitate.
### Advertising
Best suited to: Users who prefer free access, where an ad fits the experience. What it offers: Revenue from eligible usage without charging the user directly. Trade-off: Revenue depends on available demand and measured ad delivery.
Purchased credits prepay a usage balance; promotional credits may be granted for free. Metered billing charges for measured usage. Freemium defines what is free, not how that usage is funded.
Here, Generative Engine Advertising (GEA) describes advertising in generative AI experiences. With wavebird, these placements stay separate from model instructions and generated output.
Billing definitions. The audience fit and trade-offs above are product-planning guidance, not measured conversion results.
- [Stripe · recurring billing](https://stripe.com/guides/introduction-to-recurring-billing)
- [Stripe · prepaid & promotional credits](https://docs.stripe.com/billing/subscriptions/usage-based/billing-credits)
- [Stripe · usage-based billing](https://docs.stripe.com/billing/subscriptions/usage-based)
## One app.
More than one path.
### 01 · FREE ACCESS Use it with ads.
A free usage allowance, supported by separate ads at suitable moments. User pays: No direct charge.
### 02 · SUBSCRIPTION Pay for more access.
A recurring plan with a larger allowance and an ad-free experience. User pays: Recurring fee.
Add credits when you need more. Extra generations beyond the included allowance. Available on either path.
Example product design. You set the allowances, ad policies and paid benefits; subscribing does not have to be the only way to buy more usage.
Keep ads clearly labeled and separate from the AI result. Your team chooses the eligible moments.
Disclosure reference for the placement principle; allowances and paths are an illustrative product design.
- [FTC · native ad disclosure (US guidance)](https://www.ftc.gov/business-guidance/resources/native-advertising-guide-businesses)
- [wavebird · ad disclosure](https://wavebird.ai/api/disclosure)
## Put numbers behind your model.
Use your traffic and costs to assess what advertising could contribute.
- [Explore ad revenue](https://wavebird.ai/guides/ad-revenue)
- [See ads during generation](https://wavebird.ai/glossary/inference-time-ads)
# How does wavebird protect your app?
Understand who supplies the ads, what data is used and how you stay in control.
## Safety
Your product. Your boundaries.
## Where does my app fit in the advertising market?
Your app is the publisher: you provide the experience and choose where advertising belongs. wavebird connects those placements to established advertising networks and global advertiser demand, so you do not need your own advertiser sales operation.
As the intermediary, wavebird serves both sides: your product rules protect the user experience; advertiser requirements govern where campaigns can run. An ad needs to meet both.
External auction protocol; wavebird documentation describes the configured integration.
- [IAB Tech Lab · OpenRTB 2.6](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md)
- [wavebird · placement configuration](https://wavebird.ai/api/configuration)
## How do suitable advertisers reach my app?
Matching combines permitted context, language and placement information with your restrictions and available demand. Safety checks restrict unsuitable contexts; category and domain rules narrow which ads can be considered.
### Categories Exclude whole industries.
Block categories that do not belong in your product, such as gambling or adult content.
### Advertisers Block specific domains.
Exclude an advertiser by its landing domain without blocking the entire category.
### Experience Choose the placement and format.
Set where ads appear and which formats are allowed. Keep particular plans or surfaces ad-free.
For example, a travel app can permit travel-related advertising while excluding gambling and a competing booking domain. These are selection rules, not a reservation for a particular brand. Available campaigns still determine which eligible ad is returned.
- [Advertiser and category controls](https://wavebird.ai/sdk/brand-safety)
The taxonomy supplies category labels; it does not certify an implementation or guarantee brand safety.
- [IAB Tech Lab · Content Taxonomy](https://iabtechlab.com/standards/content-taxonomy/)
## What does the data firewall let through?
The firewall separates the context used inside wavebird from the request sent to an advertising partner. Your app can supply a broad topic directly. If you explicitly enable permitted prompt classification, wavebird reduces that input to abstract matching signals before partner delivery.
Raw prompts, conversation history, account records and model output are excluded from the partner ad-matching request. Permitted technical, placement and privacy fields are checked separately; they are not automatically anonymous.
- [See the data boundary in detail](https://wavebird.ai/glossary/data-firewall)
Product documentation for permitted inputs and outbound data.
- [wavebird · SDK privacy boundaries](https://wavebird.ai/sdk/privacy)
## Who asks the user for consent?
Your app presents the required choices and passes them to wavebird. You can connect your consent manager or use the supported consent widget. Prompt-derived ad relevance requires an explicit user choice; without it, that context is not used.
### Your app
Explain the choices, collect them and communicate changes, including withdrawal.
### wavebird
Validate the supplied signals against project and delivery rules before using them.
### The user
See a clear ad label and retain access to the privacy and consent controls you provide.
A consent flag carries a decision; it does not replace your privacy notice or the consent requirements for your integration. If another permitted ad path is available it can be used; otherwise the placement remains empty.
- [Connect consent signals](https://wavebird.ai/sdk/consent)
## Can advertising change or interrupt the AI result?
The model call and ad request run on separate paths. Advertiser information stays out of prompts, model context, tool inputs and model selection. Render the ad in its own labeled slot, not inside the answer or generated asset.
If required consent is absent, a safety check fails or the ad request times out, no ad is shown for that opportunity. Your integration keeps the AI task running and handles an empty placement without blocking the result.
Product documentation for independent delivery and fallback behavior.
- [wavebird · SDK fallback behavior](https://wavebird.ai/sdk/graceful-degradation)
## What does a European base mean for my app?
wavebird is built in Europe, with its infrastructure hosted in Nuremberg, Germany. You get a European infrastructure provider and access to global advertising demand. Your advertising reach is not limited to Europe.
Hosting and advertising delivery are different boundaries: permitted ad-request fields may reach international advertising partners. The privacy notice, processing agreement and subprocessor list describe the applicable data handling.
- [Privacy](https://wavebird.ai/privacy)
- [Data processing agreement](https://wavebird.ai/dpa)
- [Subprocessors](https://wavebird.ai/subprocessors)
## How can I check what was delivered?
A returned ad is a delivery decision. Render and interaction events are checked separately before they support measurement and billing. Signed evidence connects the recorded delivery to its events, making that chain reviewable.
Use the decision state and validation results to distinguish an available ad, an empty placement and a measured event. This protects publishers and advertisers from treating every ad request as a billable view.
- [How delivery evidence works](https://wavebird.ai/resources/proof-integrity)
- [Engineering evidence](https://wavebird.ai/resources/evidence)
## Why is wavebird working on Compute Sponsoring?
Compute Sponsoring addresses trust on both sides: users and advertisers should be able to understand how advertising is separated from generation. The September 2026 paper links an ad exposure to an identifiable compute unit so that this relationship can be examined at the process level. It proposes separation, data-sovereignty and consent principles.
### Neutral
No semantic relevance targeting.
### Situational
Relevance within the current task or session.
### Profiling
Semantic history carried across sessions.
These categories describe the proposal, not a menu of enabled wavebird features. The current paper is by Christian Leinberger, September 2026. Mario von Bassen co-authored the earlier February 2026 version. This is a category proposal, not a technical specification or certification.
- [Read the current Compute Sponsoring paper](https://compute-sponsoring.org/)
## How do I start and add more control through the API?
Begin with one placement and your project rules. Your developer requests an ad, renders the returned placement and handles an empty result. The same approach can support chat, image, video, 3D and music experiences.
### 01 Set the boundaries.
Agree on formats, category and advertiser blocks, consent and ad-free areas before wiring the placement.
### 02 Connect the first surface.
Use the API and hosted renderer. Verify consent changes, delivery events and the no-ad outcome.
### 03 Extend the integration.
Add more surfaces and documented per-request controls as your product needs them. Keep project and platform restrictions in force.
Product and growth teams decide the experience. Developers implement those decisions through project configuration and the API; finance can review measured delivery and revenue.
- [API configuration](https://wavebird.ai/api/configuration)
- [Open API docs](https://wavebird.ai/api)
# Engineering evidence
Fast ad decisions. Sustained execution. Measured results from the engineering behind wavebird.
Prepared by wavebird. Updated September 12, 2026. Test dates are listed with each result.
## 28.76 ms p99 in repeated ad-path benchmarks
March 23, 2026 · seven measured runs
Result: End-to-end p99: 28.76 ms. Firewall p99: 0.22 ms. Simulated-exchange round-trip p99: 15.28 ms.
What this supports: The repeated runs demonstrated reproducible low latency across the measured decision path, from context filtering through the simulated exchange response.
Test setup: Seven local benchmark runs after 1,000 warmup requests, with a simulated ad exchange. Values use the selected median run for each benchmark and cover the ad path, excluding AI generation.
Samples in selected latency runs: 12,000 firewall; 6,000 simulated-exchange; 1,200 end-to-end; 10,000 per beacon case; 2 settlement builds.
Consistency across runs: End-to-end p99 coefficient of variation: 9.02% across seven measured runs.
[Measurement data](https://wavebird.ai/evidence/benchmark-2026-03-23.json)
## Memory stability across eight hours and 24.3 million slot attempts
April 6–7, 2026 · eight-hour memory measurement
Result: 24,327,994 slot attempts. The measured steady-state heap trough was about 2.3% above the post-warmup baseline. Open handles remained at 24.
What this supports: The memory result supported the changes to slot eviction, ledger compaction, streaming settlement and projection pruning under sustained execution.
Test setup: Eight hours of continuous execution against the local wrapper path at concurrency 10. This memory-stability measurement used a predominantly no-fill workload.
Workload: 10 filled placements and 24,327,984 no-fill responses across 24,327,994 slot attempts.
Memory measurement: Samples every 30 seconds. Steady-state heap trough: 1.02319 times the post-warmup baseline.
[Measurement data](https://wavebird.ai/evidence/soak-2026-04-07.json)
## More than 3,900 beacons per second
April 19, 2026 · one measured run
Result: Beacon throughput: 3,908.69 operations/s. Job throughput: 1,028.75 operations/s. End-to-end p99: 19.51 ms.
What this supports: The benchmark measured event-processing throughput alongside the decision path, providing a concrete reference for the work between an ad request and its recorded events.
Test setup: A local benchmark with a simulated exchange, one measured run and five warmup requests. Ad-path measurements exclude AI generation.
Additional latency results: Firewall p99: 0.24 ms; simulated-exchange p99: 7.66 ms; sequential beacon p99: 0.72 ms; seeded settlement maximum: 0.28 ms.
Beacon concurrency: p99: 2.51 ms at concurrency 10, 2.52 ms at concurrency 50 and 2.11 ms at concurrency 100.
Additional throughput: Simulated-exchange requests: 1,486.64/s. Seeded settlement processing: 47,619.05 slots/s.
[Measurement data](https://wavebird.ai/evidence/benchmark-2026-04-19.json)
## 43,200 auctions with zero failed or dropped operations
June 25, 2026 · local load test
Result: The 60-minute soak completed 43,200 auctions at 12 auctions/s, with zero failed or dropped operations and p99 latency of 1,107.31 ms. The 24-auction/s spike and recovery also met their test criteria.
What this supports: The stable, spike and soak stages demonstrated sustained processing and recovery after a defined traffic spike within the tested load profile.
Test setup: A local load test with simulated partner services, covering a ramp, boundary probe, 30-minute stable stage, double-rate spike and 60-minute soak.
Stable stage: 12 auctions/s for 30 minutes: 21,600 completed; no failed or dropped operations; p99 515.92 ms.
Spike stage: 24 auctions/s for 5 minutes: 7,200 completed; no failed or dropped operations; p99 1,639.91 ms.
Boundary probe: 30 auctions/s for 5 minutes: 9,000 completed; no failed or dropped operations; p99 1,145.21 ms.
[Measurement data](https://wavebird.ai/evidence/capacity-2026-06-25.json)
### March latency details
firewall_bench: 12,000 samples; p50 0.04 ms; p95 0.12 ms; p99/maximum 0.22 ms.
ssp_roundtrip_bench: 6,000 samples; p50 7.11 ms; p95 13.03 ms; p99/maximum 15.28 ms.
e2e_bench: 1,200 samples; p50 12.98 ms; p95 24.35 ms; p99/maximum 28.76 ms.
beacon_bench_seq: 10,000 samples; p50 0.37 ms; p95 0.93 ms; p99/maximum 1.39 ms.
beacon_bench_c10: 10,000 samples; p50 5.45 ms; p95 8.96 ms; p99/maximum 10.62 ms.
beacon_bench_c50: 10,000 samples; p50 23.83 ms; p95 36.46 ms; p99/maximum 41.76 ms.
beacon_bench_c100: 10,000 samples; p50 52.31 ms; p95 73.23 ms; p99/maximum 78.75 ms.
settlement_bench: 2 samples; p50 832.93 ms; p95 887.58 ms; p99/maximum max 887.58 ms.
### March throughput details
ssp_requests_per_second: 1,364.82 requests/s; concurrency 50.
settlement_slots_per_minute: 104,483.02 seeded slots/s; concurrency 1.
jobs_per_second: 798.99 jobs/s; concurrency 100.
beacons_per_second: 2,027.93 beacons/s; concurrency 1.
The historical settlement_slots_per_minute identifier reports seeded slots per second, calculated from the batch size and elapsed seconds.
Selected measurements from wavebird engineering records. Downloads include test settings, original measured values and source hashes.
Last updated: generated from checked-in docs models.
Source: https://wavebird.ai