# 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. |
| 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. Prefer { topic, text }; raw text is processed only when prompt_shared is true and is never sent to SSPs or advertisers. |
| 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 | Request-level consent flags: semantic_targeting, prompt_shared, gdpr_applies, and consent_source. |

## 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. |

## Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing secret key. |
| 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. |

## 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"
    }
  },
  "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.
Sandbox/test traffic is non-billable. Sandbox substitute test 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/Sandbox. 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

request = urllib.request.Request(
    "https://api.wavebird.ai/v1/placements?wait_ms=1500",
    data=json.dumps({"client_id": "wbproj_demo_8jK42", "session_id": "sess_demo_123", "job_type": "chat", "slots_requested": 1}).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"))
```
