> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thrad.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Bid Request (v1)

> Orchestrates a multi-bidder auction across multiple DSPs and returns the winning ad creative in a single request

> [!WARNING]
>
> **v1 is deprecated.** New integrations should use [v2](https://docs.thrad.ai/api/api-reference/bid-request-v2.md).

## Authorizations

**thrad-api-key** `string` *required*

Your API key for authentication. The key's environment (`staging` or `production`) determines how ads are served — see [Staging vs. Production](https://docs.thrad.ai/quickstart.md#staging-vs-production).

**Content-Type** `string` *required*

Must be `application/json`

## End-User Headers

These headers identify the real end-user behind the request. They are **required** on every bid request and every view URL ping.

**X-Forwarded-For** `string` *required*

The end-user's original IP address. Must be the real client IP, not your server's IP.

**User-Agent** `string` *required*

The end-user's original device/browser string exactly as received from the client.

## Geolocation & Device Headers

The server resolves the user's location and device from request headers. These are **optional** but **strongly recommended** for server-side integrations — sending them skips the server-side IP lookup and reduces latency.

**X-User-Country** `string`

ISO 3166-1 alpha-2 country code (e.g. `"US"`, `"DE"`). When present, `X-User-Device` and `X-User-Timezone` are **required**.

**X-User-Device** `string`

User's device type: `"mobile"` or `"desktop"`. **Required** when `X-User-Country` is present. This is critical for device-targeted campaigns — without it, targeting will not work correctly.

**X-User-Timezone** `string`

IANA timezone (e.g. `"America/Los_Angeles"`). **Required** when `X-User-Country` is present. Used for time-of-day pacing.

**X-User-Region** `string`

Region or state (e.g. `"CA"`, `"Bavaria"`). Optional but recommended for regional targeting.

**X-User-City** `string`

City name (e.g. `"San Francisco"`). Optional but recommended for city-level targeting.

> [!WARNING]
>
> If you send `X-User-Country` without `X-User-Device` and `X-User-Timezone`, the request will be rejected with a `400 Bad Request`. These three headers must always be sent together.

If none of the above headers are sent, the server falls back to IP-based geolocation using `X-Forwarded-For`. For **direct calls** (browser or mobile app → our API), this is handled automatically by the load balancer. If you're **proxying requests through your own backend**, you must forward the user's IP via `X-Forwarded-For` — otherwise the server sees your server's IP and geo will be wrong.

> [!NOTE]
>
> If unsure about which headers to send, contact **[marco@thrads.ai](mailto:marco@thrads.ai)** and we'll help you set it up.

## Body

**user\_id** `string`

Optional anonymous user identifier (e.g. a UUID like `user_a1b2c3d4-...`). If supplied, 1–100 characters and not blank. Keep it stable for user-based ad controls. Do not use email, name, or other PII. On web, store in localStorage; on mobile, use the platform's persistent storage (e.g. UserDefaults on iOS, SharedPreferences on Android).

**chat\_id** `string`

`contextual only` Optional conversation identifier. At most 100 characters. An empty `chat_id` is treated as no chat ID. One unique ID per conversation, not per user. Reset when the user starts a new chat. Without it, matching still works, but stored chat pacing, chat repeat checks, experiment cooldowns, and transcript storage are skipped. User daily campaign limits and click cooldowns still apply when `user_id` is supplied.

**messages** `array`

`contextual only` Conversation history. **Required** when `request_type` is `"contextual"`. Each message must have a `role` (`"user"` or `"assistant"`) and `content`. PII (names, emails, addresses, etc.) should be masked or removed before sending.

At least one `user` message is required. A single `user` message is valid on its own — send it to request an ad mid-turn, while your model is still generating its reply. A conversation containing only `assistant` messages has no user intent to match against and returns `"bid": null`.

**messages properties**

**role** `string` *required*

Message role. Must be either `"user"` or `"assistant"`.

**content** `string` *required*

Message text content.

**timestamp** `string` *required*

ISO 8601 (e.g. `"2025-11-23T10:00:00Z"`) or Unix timestamp (e.g. `1700730000`). Used for conversation continuity tracking — without timestamps, the server cannot merge partial message histories and conversation analytics will be degraded.

**config** `object`

Publisher configuration for pacing, contextual serving, creative constraints, and analytics. All fields are optional — omitted fields fall back to defaults set on your publisher in the platform dashboard.

**config properties**

**ad\_offset** `integer`

`contextual only` Minimum turns before the first ad in a conversation. `null` uses your dashboard setting. `0` disables the offset check.

**max\_frequency** `integer`

`contextual only` Minimum number of requests between consecutive ads. For example, `3` means an ad can appear at most once every 3 requests. `null` uses your dashboard setting. `0` disables the frequency check.

**contextuality** `integer`

`contextual only` How strictly an ad must match the conversation, on a `1`–`8` scale. Higher values require a stronger match and lower the fill rate; lower values are more permissive and fill more often. `null` uses your dashboard setting (default `2`).

**retargeting\_enabled** `boolean` (default: true)

`contextual only` Allow re-proposing a recently-relevant ad to the same user: if a product was contextually relevant to them in a conversation within the last 7 days, it can be shown again even when the current conversation isn't relevant to it. Set `false` to serve only ads relevant to the current conversation. `null` uses your dashboard setting (default `true`).

**max\_headline\_chars** `integer`

Maximum character length for ad headlines. Minimum value is `30`. When set, the system guarantees no headline exceeds this limit — creatives that don't fit are filtered or replaced.

**image\_enabled** `boolean` (default: true)

Whether the publisher can render ad images. When `false`, product images are omitted and the ad is returned with a logo or no image. Defaults to `true`.

**restricted\_click\_area** `boolean` (default: false)

**Experimental, bespoke** (piloting with select publishers). When `true`, only specific card elements (logo, advertiser name, CTA button) should be clickable rather than the whole card. The value you send is echoed back verbatim on the card bid response as `restricted_click_area`; if your integration doesn't accommodate it, ignore it. Defaults to `false`.

**redirect\_after\_vote** `boolean` (default: false)

**Experimental, bespoke** (piloting with select publishers). Poll render directive. When `true`, send the user to the advertiser (`cta_url`) after they vote; when `false`, stay inline and show the results bar. The value you send is echoed back verbatim on the poll bid response as `redirect_after_vote`; if your integration doesn't accommodate it, ignore it. Defaults to `false`.

**experiment\_tag** `string`

A/B test label for analytics segmentation (max 30 characters). See [Experiments](#experiments) below.

**request\_type** `string` (default: contextual)

Pipeline selector. Determines which ad pipeline runs and what fields are required.

* `"contextual"` (default) — in-chat ads. Requires context: `messages`, a `summary`, or `keywords`. `chat_id` is optional.
* `"opener"` — pre-chat ads (e.g. sponsored prompts, banners). `chat_id` and `messages` are not required. Sponsored polls are not opener inventory.

**ad\_formats** `array`

Ad formats the publisher can render. The server picks the best match from this list.

* **Contextual:** any combination of `card` (default), `poll`, and `carousel` — e.g. `["card", "carousel"]`.
* **Opener:** `["card"]`. Always declare it explicitly. Do not include `poll` or `carousel` on opener requests. (`sponsored_prompt` is coming soon.)

The legacy spellings `sponsored_message` and `sponsored_poll` remain accepted for backward compatibility. Responses currently echo the legacy names in `ad_format` for the card and poll (`"sponsored_message"`, `"sponsored_poll"`); carousel returns `"carousel"`.

When contextual requests include both `sponsored_message` and `sponsored_poll`, Thrad selects the campaign through the normal contextual auction, then uses campaign delivery settings to choose whether the returned creative is a message or poll.

**turn\_number** `integer`

`contextual only` Current turn number in the conversation. Used for ad offset pacing and analytics. Must be >= 0. **Only needed if you're not calling the endpoint on every user-assistant exchange** — if you are, the system derives this from the message count.

**summary** `string`

`contextual only` Publisher-provided conversation summary. Useful when you already have a summary from your chatbot and want to reduce latency.

**user\_metadata** `object`

User metadata for targeting and compliance. All fields are optional. Extra fields are accepted and stored for future use.

**user\_metadata properties**

**age\_range** `string`

IAB standard age range. Accepted values: `"0-12"`, `"13-17"`, `"18-24"`, `"25-34"`, `"35-44"`, `"45-54"`, `"55-64"`, `"65+"`. When the value is `"0-12"` or `"13-17"`, the request is treated as underage: the auction is skipped and the response is `200 OK` with `"bid": null`. Send this whenever your platform knows the user's age — it's the only way to comply with age-gating on Thrad's side.

**tcf\_consent** `string`

IAB TCF v2 consent string generated by your CMP (e.g. OneTrust, Quantcast) when the user interacts with your cookie banner. Required for EU/UK users if you want oRTB DSPs to bid on that traffic — without it, EU-based DSPs will scrub or skip the request. Pass the raw base64-encoded string exactly as produced by your CMP. Non-EU publishers can omit this field.

**us\_privacy** `string`

IAB CCPA US Privacy string (e.g. `"1YNN"`, `"1---"`). Generated by your CMP for California/US users. Forwarded to oRTB DSPs as `regs.us_privacy`. Non-US publishers can omit this field.

```json Contextual (in-chat ad) theme={null}
{
  "user_id": "user_123",
  "chat_id": "chat_456",
  "messages": [
    {
      "role": "user",
      "content": "Looking for running shoes",
      "timestamp": "2025-11-23T10:00:00Z"
    },
    {
      "role": "assistant",
      "content": "What's your budget?",
      "timestamp": "2025-11-23T10:00:15Z"
    }
  ],
  "summary": "User is looking for running shoes for marathon training",
  "config": {
    "ad_offset": 3,
    "max_frequency": 5,
    "contextuality": 2,
    "retargeting_enabled": true,
    "max_headline_chars": 60,
    "experiment_tag": "variant_a"
  },
  "user_metadata": {
    "age_range": "25-34"
  },
  "turn_number": 4,
  "ad_formats": ["card", "poll", "carousel"]
}
```

```json Opener (pre-chat ad) theme={null}
{
  "user_id": "user_123",
  "request_type": "opener",
  "ad_formats": ["card"]
}
```

> [!WARNING]
>
> `poll` and `carousel` are contextual-only. Opener requests that include them are invalid and should be changed to an opener-supported format.

```json 200 - Sponsored Message theme={null}
{
  "requestId": "api_req_123",
  "timestamp": "2025-11-24T21:51:52.240297Z",
  "totalTime": 0.123,
  "status": "success",
  "message": "Bid successful",
  "data": {
    "bid": {
      "ad_format": "sponsored_message",
      "price": 5.50,
      "advertiser": "Nike",
      "domain": "nike.com",
      "headline": "Premium Running Shoes",
      "description": "Perfect for marathon training. Lightweight and comfortable.",
      "cta_text": "Shop Now",
      "url": "https://ssp.thrads.ai/api/v1/tracking/redirect?token=abc123",
      "placement": "image",
      "logo_url": "https://cdn.example.com/nike-logo.png",
      "image_url": "https://cdn.example.com/nike-shoes.jpg",
      "restricted_click_area": false,
      "view_url": "https://ssp.thrads.ai/api/v1/tracking/view?token=v1",
      "dsp": "thrad_dsp",
      "bidId": "bid_abc123"
    }
  },
  "error": null
}
```

```json 200 - Sponsored Poll theme={null}
{
  "requestId": "api_req_126",
  "timestamp": "2025-11-24T21:51:52.240297Z",
  "totalTime": 0.128,
  "status": "success",
  "message": "Bid successful",
  "data": {
    "bid": {
      "ad_format": "sponsored_poll",
      "price": 5.50,
      "sponsored_poll_id": 123,
      "advertiser": "Warner Music Group",
      "domain": "warnermusic.com",
      "logo_url": "https://img.logokit.com/warnermusic.com?token=pk_xxx",
      "question": "What kind of playlist are you making?",
      "options": [
        { "id": "0", "text": "Wedding" },
        { "id": "1", "text": "Study" },
        { "id": "2", "text": "Workout" }
      ],
      "vote_url": "https://ssp.thrads.ai/api/v1/tracking/sponsored-polls/vote?token=vote123",
      "results_url": "https://ssp.thrads.ai/api/v1/tracking/sponsored-polls/results?token=results123",
      "cta_text": "Learn more",
      "cta_url": "https://ssp.thrads.ai/api/v1/tracking/sponsored-polls/cta?token=cta123&view_url=https%3A%2F%2Fssp.thrads.ai%2Fapi%2Fv1%2Ftracking%2Fview%3Ftoken%3Dv1",
      "redirect_after_vote": true,
      "results": {
        "total_votes": 0,
        "options": [
          { "id": "0", "text": "Wedding", "votes": 0, "percentage": 0 },
          { "id": "1", "text": "Study", "votes": 0, "percentage": 0 },
          { "id": "2", "text": "Workout", "votes": 0, "percentage": 0 }
        ]
      },
      "dsp": "thrad_dsp",
      "bidId": "bid_poll_abc123"
    }
  },
  "error": null
}
```

```json 200 - Carousel theme={null}
{
  "requestId": "api_req_129",
  "timestamp": "2025-11-24T21:51:52.240297Z",
  "totalTime": 0.125,
  "status": "success",
  "message": "Bid successful",
  "data": {
    "bid": {
      "ad_format": "carousel",
      "price": 5.50,
      "advertiser": "De'Longhi",
      "domain": "delonghi.com",
      "logo_url": "https://cdn.example.com/delonghi-logo.png",
      "view_url": "https://ssp.thrads.ai/api/v1/tracking/view?token=v1",
      "items": [
        {
          "index": 0,
          "title": "De'Longhi ECP3220 Espresso Machine",
          "image_url": "https://cdn.example.com/ecp3220.jpg",
          "price": "119.95",
          "currency": "USD",
          "click_url": "https://ssp.thrads.ai/api/v1/tracking/redirect?token=c0"
        },
        {
          "index": 1,
          "title": "De'Longhi Dedica Grinder",
          "image_url": "https://cdn.example.com/dedica.jpg",
          "price": "89.00",
          "currency": "USD",
          "click_url": "https://ssp.thrads.ai/api/v1/tracking/redirect?token=c1"
        },
        {
          "index": 2,
          "title": "De'Longhi Milk Frother",
          "image_url": "https://cdn.example.com/frother.jpg",
          "price": "49.95",
          "currency": "USD",
          "click_url": "https://ssp.thrads.ai/api/v1/tracking/redirect?token=c2"
        }
      ],
      "dsp": "thrad_dsp",
      "bidId": "bid_car_abc123"
    }
  },
  "error": null
}
```

```json 200 - No Bid theme={null}
{
  "requestId": "api_req_124",
  "timestamp": "2025-11-24T21:51:52.240297Z",
  "totalTime": 0.095,
  "status": "success",
  "message": "No bids",
  "data": {
    "bid": null
  },
  "error": null
}
```

```json 500 - Error theme={null}
{
  "requestId": "api_req_125",
  "timestamp": "2025-11-24T21:51:52.240297Z",
  "totalTime": 0.234,
  "status": "error",
  "message": "Render failed",
  "data": {
    "bid": null
  },
  "error": {
    "type": "HTTPException",
    "code": 500
  }
}
```

> [!NOTE]
>
> A `200 OK` response with `"bid": null` is **not an error** - it means the auction ran successfully but no DSP submitted a winning bid. This is normal behavior.

## Response

**requestId** `string`

Unique request identifier used in logs and tracing.

**timestamp** `string`

ISO 8601 timestamp of when the response was generated.

**totalTime** `float`

Total processing time for this request in seconds.

**status** `string`

Response status: `"success"` or `"error"`.

**message** `string`

Human-readable message describing the result.

**data** `object`

Response data payload.

**data properties**

**bid** `object|null`

Winning bid object (null if no bids). Contains ad creative information when available.

**bid properties**

**price** `float`

Clearing price (CPM) in dollars — what the publisher earns.

**ad\_format** `string`

The ad format returned: `"sponsored_message"` (card), `"sponsored_poll"` (poll), or `"carousel"`. Card and poll currently echo their legacy names even when requested as `card`/`poll`.

**advertiser** `string`

Advertiser brand name. Falls back to the advertiser's website domain if no brand name is available.

**domain** `string`

Advertiser website domain (e.g. `"nike.com"`). Always present when a bid is returned.

**dsp** `string`

Winning DSP identifier (e.g., `"thrad_dsp"`).

**bidId** `string`

Bid identifier for tracking and debugging.

**headline** `string`

Ad headline text. Respects `config.max_headline_chars` when set.

**description** `string`

Ad body text. May be `null` in some cases.

**cta\_text** `string`

Call-to-action text, e.g., "Shop Now", "Learn More". Defaults to "Learn More" on the publisher side if absent.

**url** `string`

Sponsored message and prompt click tracking URL. Use this for ad clicks — it tracks the click and redirects to the advertiser's landing page. Poll option clicks use `vote_url`, not `url`.

**placement** `string`

`sponsored_message` only. Render mode: `"text"` (compact, logo only) or `"image"` (full product photo). When missing, treat as `"text"`. See [Ad Formats & Rendering](https://docs.thrad.ai/api/guides/rendering-ads.md#sponsored-message) for the full creative-field rules per mode.

**logo\_url** `string`

Advertiser brand/logo URL. Required when `placement="text"`. May also be present alongside `image_url` when `placement="image"`.

**image\_url** `string`

Product/hero photo URL. Present when `placement="image"`, absent when `placement="text"`. Setting `config.image_enabled=false` forces text mode, so `image_url` will never be returned.

**restricted\_click\_area** `boolean` (default: false)

`sponsored_message` (card) only. **Experimental, bespoke.** Echoes the request `config.restricted_click_area`. When `true`, only the CTA (and logo) should route to `url`; when `false` (default), the whole card is the click target. Ignore it if your integration doesn't support restricted click areas.

**view\_url** `string`

Optional impression tracking pixel URL. Fire it when the ad becomes visible to the user. See [Billing & Viewability](https://docs.thrad.ai/api/guides/billing-viewability.md).

**sponsored\_poll\_id** `integer`

`sponsored_poll` only. Poll identifier.

**question** `string`

`sponsored_poll` only. Poll question text.

**options** `array`

`sponsored_poll` only. Poll answer options. Render each option as a button or equivalent selectable control.

**options properties**

**id** `string`

Option identifier to submit when voting.

**text** `string`

Option label to display.

**vote\_url** `string`

`sponsored_poll` only. Signed vote URL. Submit the selected option to this URL instead of using normal click tracking. See [Sponsored Poll Tracking](https://docs.thrad.ai/api/api-reference/sponsored-poll-tracking.md).

**results\_url** `string`

`sponsored_poll` only. Signed results URL. Fetch this URL to refresh aggregate results without recording a vote. See [Sponsored Poll Tracking](https://docs.thrad.ai/api/api-reference/sponsored-poll-tracking.md).

**cta\_url** `string`

`sponsored_poll` only. Optional signed click tracking URL for the advertiser CTA shown after voting. Whether voting redirects to this URL is controlled by `redirect_after_vote`.

**redirect\_after\_vote** `boolean` (default: false)

`sponsored_poll` only. **Experimental, bespoke.** Echoes the request `config.redirect_after_vote`. When `true`, send the user to the advertiser (`cta_url`) after they vote; when `false` (default), stay inline and show the results bar. Ignore it if your integration doesn't support post-vote redirects.

**results** `object`

`sponsored_poll` only. Optional aggregate results returned with the creative.

**results properties**

**total\_votes** `integer`

Total votes counted for the poll.

**options** `array`

Result rows for each poll option, including `id`, `text`, `votes`, and `percentage`.

**items** `array`

`carousel` only. The product cards, 3–5 entries, in serving order. Render left-to-right; you may truncate trailing items if space demands, but never reorder. Each item is its own click target via its `click_url`; the carousel as a whole fires the single top-level `view_url` once when it renders.

**items properties**

**index** `integer`

Stable item position (0-based). Echoed in click analytics.

**title** `string`

Product name to display on the card.

**image\_url** `string`

Product image URL.

**price** `string`

Optional display price (e.g. `"119.95"`). Always paired with `currency`; both absent when the product has no price.

**currency** `string`

ISO-4217 currency code for `price` (e.g. `"USD"`).

**click\_url** `string`

Signed click tracking URL for this card. Navigate the user here on click; it redirects to the product page.

**error** `object|null`

Error details when `status = "error"` (otherwise `null`).

## Status Codes

| Status Code | Meaning | Scenario |
| - | - | - |
| `200 OK` | Success | Auction ran successfully (with or without winning bid), or pacing blocked |
| `400 Bad Request` | Invalid input | Malformed request body or missing required fields |
| `401 Unauthorized` | Authentication failed | Missing or invalid `thrad-api-key` |
| `403 Forbidden` | Origin not allowed | Request from non-whitelisted domain (browser integration only) |
| `429 Too Many Requests` | Rate limit exceeded | Publisher exceeded request quota |
| `500 Internal Server Error` | Server error | DSP render failed, validation error, or internal exception |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.