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

# Bid

> Endpoint that DSPs must implement to receive bid requests from Thrad SSP (ssp.thrads.ai). Called by the SSP orchestrator during multi-bidder auctions.

<Note>
  **Implementation Required**: Your DSP must implement this endpoint on your own server. The SSP at `ssp.thrads.ai` will send POST requests to your configured DSP endpoint URL.
</Note>

## Authorization

<ParamField header="Authorization" type="string" required>
  Bearer token for authentication. Format: `Bearer your-api-key`
</ParamField>

## Headers

<ParamField header="Content-Type" type="string" required>
  Must be `application/json`
</ParamField>

<ParamField header="X-Forwarded-For" type="string" required>
  End-user's IP address. First hop is the real user IP. Required on every bid request — use for geolocation, fraud detection, or frequency capping. The resolved `country_code` / `region` / `city` body fields are also provided as a convenience.
</ParamField>

<ParamField header="User-Agent" type="string" required>
  End-user's device/browser string. Required on every bid request — use for device targeting, creative optimisation, or fraud detection.
</ParamField>

## Body

<ParamField body="userId" type="string" required>
  Unique user identifier. Use anonymous UUIDs (e.g., `user_a1b2c3d4-...`). Do not use email or name.
</ParamField>

<ParamField body="request_type" type="string" required>
  Pipeline selector. One of:

  * `"contextual"` — in-chat ad. `chatId` and `messages` are required.
  * `"opener"` — pre-chat ad shown before the conversation starts. `chatId` and `messages` are not sent.

  Defaults to `"contextual"` if omitted.
</ParamField>

<ParamField body="chatId" type="string">
  Conversation identifier. One unique ID per conversation, not per user. Reset when user starts a new chat.

  **Required when `request_type=contextual`.** Not sent for openers.
</ParamField>

<ParamField body="nAdsBefore" type="integer">
  Number of ads shown before this request in the current conversation. Added by SSP from cache. Contextual only.
</ParamField>

<ParamField body="messages" type="array">
  Conversation history. Must contain at least 2 messages, alternating between user and assistant roles, ending with a user message followed by an assistant message.

  **Required when `request_type=contextual`.** Not sent for openers (no conversation exists yet).

  <Expandable title="messages properties">
    <ParamField body="role" type="string" required>
      Message role. Must be either `"user"` or `"assistant"`.
    </ParamField>

    <ParamField body="content" type="string" required>
      Message text content.
    </ParamField>

    <ParamField body="timestamp" type="string">
      Optional ISO 8601 or Unix timestamp. Including timestamps enables time-in-chat analytics for better targeting.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="ad_formats" type="string[]">
  Ad formats the publisher can render. Your DSP must pick one from this list when bidding and identify it in the bid response (`ad_data.ad_format`). If omitted, assume `["sponsored_message"]`.

  **Allowed values per `request_type`:**

  * `contextual`: `"sponsored_message"`, `"sponsored_carousel"`
  * `opener`: `"sponsored_message"`, `"sponsored_carousel"`
</ParamField>

<ParamField body="prefetched" type="boolean">
  `true` when the publisher is requesting an ad **before** the assistant reply is shown (pre-fetch). `false` when the ad is requested after the reply has already been displayed. Useful for tuning latency expectations and creative relevance.
</ParamField>

<ParamField body="request_id" type="string">
  Optional request identifier for distributed tracing when called from SSP. Used for correlation across services.
</ParamField>

<ParamField body="country_code" type="string" required>
  ISO 3166-1 alpha-2 country code (e.g., `"US"`, `"GB"`). Derived from IP geolocation.
</ParamField>

<ParamField body="region" type="string">
  State or region name (e.g., `"California"`). Derived from IP geolocation.
</ParamField>

<ParamField body="city" type="string">
  City name (e.g., `"San Francisco"`). Derived from IP geolocation.
</ParamField>

<ParamField body="device" type="string" required>
  Device type. Possible values: `"mobile"`, `"desktop"`, `"tablet"`.
</ParamField>

<ParamField body="timezone" type="string" required>
  User's timezone in IANA format (e.g., `"America/Los_Angeles"`).
</ParamField>

<ParamField body="config" type="object">
  Publisher creative constraints. Your DSP should respect these when generating the ad.

  <Expandable title="config properties">
    <ParamField body="max_headline_chars" type="integer" default="100">
      Maximum headline length (in characters) the publisher can render. Minimum 30. Defaults to 100 if omitted.
    </ParamField>

    <ParamField body="image_enabled" type="boolean" default="true">
      Whether the publisher can render a hero/product image. When `false`, do not bid `placement="image"` — bid `placement="text"` with a `logo_url` instead (or no-bid).
    </ParamField>
  </Expandable>
</ParamField>

<Note>
  **Placement contract:** every `sponsored_message` creative must declare a `placement` field — `"image"` (hero product photo) or `"text"` (advertiser logo + copy, no hero image). Each placement requires a matching URL: `placement="image"` needs `image_url`, `placement="text"` needs `logo_url`. Bids that omit `placement` or supply the wrong URL are rejected (`dsp_missing_placement` / `dsp_invalid_image_bid` / `dsp_invalid_text_bid`). See [Ad Formats](/dsp/guides/ad-formats#sponsored_message) for the full contract.
</Note>

<RequestExample>
  ```json Contextual Request theme={null}
  {
    "userId": "user_123",
    "request_type": "contextual",
    "chatId": "chat_456",
    "nAdsBefore": 2,
    "messages": [
      {
        "role": "user",
        "content": "Looking for running shoes",
        "timestamp": "2024-11-23T10:00:00Z"
      },
      {
        "role": "assistant",
        "content": "What's your budget?",
        "timestamp": "2024-11-23T10:00:15Z"
      }
    ],
    "ad_formats": ["sponsored_message", "sponsored_carousel"],
    "prefetched": true,
    "request_id": "req_abc123",
    "country_code": "US",
    "region": "California",
    "city": "San Francisco",
    "device": "mobile",
    "timezone": "America/Los_Angeles",
    "config": {
      "max_headline_chars": 100,
      "image_enabled": true
    }
  }
  ```

  ```json Opener Request theme={null}
  {
    "userId": "user_123",
    "request_type": "opener",
    "ad_formats": ["sponsored_message"],
    "prefetched": false,
    "request_id": "req_abc123",
    "country_code": "US",
    "region": "California",
    "city": "San Francisco",
    "device": "mobile",
    "timezone": "America/Los_Angeles",
    "config": {
      "max_headline_chars": 100,
      "image_enabled": true
    }
  }
  ```
</RequestExample>

<ResponseExample>
  ```json 200 - Bid (render step required) theme={null}
  {
    "data": {
      "bid": 7.50,
      "bidId": "bid_abc123"
    }
  }
  ```

  ```json 200 - Bid + Pre-rendered Sponsored Message (placement=image) theme={null}
  {
    "data": {
      "bid": 7.50,
      "bidId": "bid_abc123",
      "ad_data": {
        "ad_format": "sponsored_message",
        "placement": "image",
        "headline": "Check out the Nike Air Zoom Alphafly",
        "description": "The marathon record-holder's shoe.",
        "url": "https://your-dsp.com/track/click/abc123",
        "advertiser": "Nike",
        "domain": "nike.com",
        "cta_text": "Shop Now",
        "image_url": "https://cdn.nike.com/alphafly.png",
        "logo_url": "https://cdn.nike.com/logo.png",
        "view_url": "https://your-dsp.com/track/view/abc123",
        "feedback_url": "https://your-dsp.com/feedback?bid=bid_abc123"
      }
    }
  }
  ```

  ```json 200 - Bid + Pre-rendered Sponsored Message (placement=text) theme={null}
  {
    "data": {
      "bid": 7.50,
      "bidId": "bid_abc123",
      "ad_data": {
        "ad_format": "sponsored_message",
        "placement": "text",
        "headline": "Best Running Shoes 2026",
        "description": "Get 20% off premium running shoes today.",
        "url": "https://your-dsp.com/track/click/abc123",
        "advertiser": "Acme Sports",
        "domain": "acmesports.com",
        "cta_text": "Shop Now",
        "logo_url": "https://cdn.acmesports.com/logo.png",
        "view_url": "https://your-dsp.com/track/view/abc123",
        "feedback_url": "https://your-dsp.com/feedback?bid=bid_abc123"
      }
    }
  }
  ```

  ```json 200 - Bid + Pre-rendered Carousel theme={null}
  {
    "data": {
      "bid": 7.50,
      "bidId": "bid_abc123",
      "ad_data": {
        "ad_format": "sponsored_carousel",
        "advertiser": "Acme Sports",
        "domain": "acmesports.com",
        "items": [
          {
            "headline": "Pegasus Trail 5",
            "description": "Cushioned trail runner.",
            "image_url": "https://cdn.example.com/p1.png",
            "url": "https://your-dsp.com/track/click/abc123?tile=0"
          },
          {
            "headline": "Vomero 17",
            "image_url": "https://cdn.example.com/p2.png",
            "url": "https://your-dsp.com/track/click/abc123?tile=1"
          },
          {
            "headline": "Invincible 3",
            "image_url": "https://cdn.example.com/p3.png",
            "url": "https://your-dsp.com/track/click/abc123?tile=2"
          }
        ],
        "view_url": "https://your-dsp.com/track/view/abc123",
        "feedback_url": "https://your-dsp.com/feedback?bid=bid_abc123"
      }
    }
  }
  ```

  ```json 200 - No Bid theme={null}
  {
    "data": {}
  }
  ```

  ```json 500 - Error theme={null}
  {
    "error": "Internal server error"
  }
  ```
</ResponseExample>

## Response

<Note>
  Additional fields (such as `requestId`, `timestamp`, `status`, `message`, etc.) are allowed in the response and will be ignored by the SSP.
</Note>

<ResponseField name="data" type="object" required>
  Response data payload. Must contain `bid` and `bidId` when submitting a bid. Return empty object `{}` for no-bid.

  <Expandable title="data properties">
    <ResponseField name="bid" type="float" required>
      Bid amount in CPM (cost per mille) dollars. Must be >= 0. Should have 2 decimal places. Values with more decimals will be rounded using banker's rounding (IEEE 754).
    </ResponseField>

    <ResponseField name="bidId" type="string" required>
      Unique bid identifier required for render endpoint. Used to retrieve bid details during render phase.
    </ResponseField>

    <ResponseField name="ad_data" type="object">
      **Optional** - Pre-rendered ad creative. When included, the SSP skips the render step entirely and serves this ad directly.

      **Important**: If `ad_data` is present but invalid (missing required fields, wrong types, or unknown fields), the entire bid is rejected — treated as a no-bid with reason `invalid_ad_data`. Only include this if you're confident in the format.

      The shape of `ad_data` is discriminated by the `ad_format` field. The format must be one your DSP was offered in `ad_formats` on the request. **`ad_format` defaults to `"sponsored_message"` if omitted** — only required when returning a non-default format like `"sponsored_carousel"`.

      <Expandable title="ad_data properties (sponsored_message, placement=image)">
        <Note>
          Full card with a hero product image. Use when you have a real product photo. Publishers that set `config.image_enabled=false` cannot render this — bid `placement="text"` instead (or no-bid).
        </Note>

        <ResponseField name="ad_format" type="string" default="sponsored_message">
          `"sponsored_message"`. Optional — this is the default when omitted.
        </ResponseField>

        <ResponseField name="placement" type="string" required>
          Must be `"image"`. Declares that this creative renders with a hero product image.
        </ResponseField>

        <ResponseField name="headline" type="string" required>
          Ad headline text. Must respect `config.max_headline_chars` from the request.
        </ResponseField>

        <ResponseField name="description" type="string">
          Ad description or body text. Optional.
        </ResponseField>

        <ResponseField name="url" type="string" required>
          Click-through/tracking URL.
        </ResponseField>

        <ResponseField name="advertiser" type="string" required>
          Brand display name (e.g. `"Nike"`).
        </ResponseField>

        <ResponseField name="domain" type="string">
          Brand domain (e.g. `"nike.com"`). Optional.
        </ResponseField>

        <ResponseField name="cta_text" type="string" required>
          Call-to-action button text (e.g., "Learn More", "Shop Now").
        </ResponseField>

        <ResponseField name="image_url" type="string" required>
          Hero/product image URL — rendered as the main visual of the card. **Required for `placement="image"`.**
        </ResponseField>

        <ResponseField name="logo_url" type="string">
          Advertiser brand mark. Optional, but recommended — publishers may render it as a small logo next to the product image.
        </ResponseField>

        <ResponseField name="view_url" type="string">
          Impression/view tracking pixel URL. If provided, the SSP appends it as a query parameter to the click URL for impression counting. Optional.
        </ResponseField>

        <ResponseField name="feedback_url" type="string">
          URL where Thrad should POST user feedback (thumbs up / thumbs down) for this ad. The URL is yours — embed the bid ID or any token you need to attribute the signal. Optional. See [Feedback Passthrough](/dsp/api-reference/dsp-feedback-webhook).
        </ResponseField>
      </Expandable>

      <Expandable title="ad_data properties (sponsored_message, placement=text)">
        <Note>
          Compact card with advertiser logo + copy, no hero image. Use when you don't have a real product photo — brand awareness creatives, or catalogues that only have logo-style assets.
        </Note>

        <ResponseField name="ad_format" type="string" default="sponsored_message">
          `"sponsored_message"`. Optional — this is the default when omitted.
        </ResponseField>

        <ResponseField name="placement" type="string" required>
          Must be `"text"`. Declares that this creative renders compactly with a logo rather than a hero image.
        </ResponseField>

        <ResponseField name="headline" type="string" required>
          Ad headline text. Must respect `config.max_headline_chars` from the request.
        </ResponseField>

        <ResponseField name="description" type="string">
          Ad description or body text. Optional.
        </ResponseField>

        <ResponseField name="url" type="string" required>
          Click-through/tracking URL.
        </ResponseField>

        <ResponseField name="advertiser" type="string" required>
          Brand display name (e.g. `"Acme Sports"`).
        </ResponseField>

        <ResponseField name="domain" type="string">
          Brand domain (e.g. `"acmesports.com"`). Optional.
        </ResponseField>

        <ResponseField name="cta_text" type="string" required>
          Call-to-action button text (e.g., "Learn More", "Shop Now").
        </ResponseField>

        <ResponseField name="logo_url" type="string" required>
          Advertiser brand/logo URL — rendered as the card's visual. **Required for `placement="text"`.**
        </ResponseField>

        <ResponseField name="image_url" type="string">
          Ignored for `placement="text"`. Send as `null` or omit.
        </ResponseField>

        <ResponseField name="view_url" type="string">
          Impression/view tracking pixel URL. If provided, the SSP appends it as a query parameter to the click URL for impression counting. Optional.
        </ResponseField>

        <ResponseField name="feedback_url" type="string">
          URL where Thrad should POST user feedback (thumbs up / thumbs down) for this ad. Optional. See [Feedback Passthrough](/dsp/api-reference/dsp-feedback-webhook).
        </ResponseField>
      </Expandable>

      <Expandable title="ad_data properties (carousel)">
        <Note>
          **Beta.** Carousel is in early release. Upcoming improvements: support for mixed-brand carousels (multiple advertisers per carousel) and per-tile viewability tracking (which tile was actually seen vs only rendered). Current constraints — same-brand only, single carousel-level `view_url` — will be relaxed in a future version.
        </Note>

        <ResponseField name="ad_format" type="string" required>
          Must be `"sponsored_carousel"`. Required for carousel responses (no default).
        </ResponseField>

        <ResponseField name="advertiser" type="string">
          Brand display name shared across all tiles. Optional but recommended — **all tiles in a carousel must belong to the same brand**.
        </ResponseField>

        <ResponseField name="domain" type="string">
          Brand domain shared across all tiles. Optional.
        </ResponseField>

        <ResponseField name="logo_url" type="string">
          Shared brand mark/logo URL covering the whole carousel. Optional.
        </ResponseField>

        <ResponseField name="items" type="array" required>
          Carousel tiles. **Exactly 3 items required, all from the same brand.** Order is preserved (index 0 is the lead tile). Cross-brand carousels are rejected.

          <Expandable title="item properties">
            <ResponseField name="headline" type="string" required>
              Tile headline. Must respect `config.max_headline_chars` from the request.
            </ResponseField>

            <ResponseField name="image_url" type="string" required>
              Tile image URL. **Required for every tile** — carousel without images is rejected.
            </ResponseField>

            <ResponseField name="url" type="string" required>
              Per-tile click-through URL. Must be unique per tile so click attribution can identify which tile was clicked.
            </ResponseField>

            <ResponseField name="description" type="string">
              Optional secondary text for the tile.
            </ResponseField>
          </Expandable>
        </ResponseField>

        <ResponseField name="view_url" type="string">
          Single impression/view tracking pixel URL covering the whole carousel. Optional.
        </ResponseField>

        <ResponseField name="feedback_url" type="string">
          URL where Thrad should POST user feedback (thumbs up / thumbs down) for this carousel. Optional. See [Feedback Passthrough](/dsp/api-reference/dsp-feedback-webhook).
        </ResponseField>
      </Expandable>
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="error" type="string">
  Error message when the request fails. Only present on error responses.
</ResponseField>

<Warning>
  **Pre-rendered `ad_data` validation is strict.** The SSP validates `ad_data` against the exact render response schema (`extra="forbid"`). Unknown fields will cause the bid to be rejected. Required fields: `headline`, `description`, `url`. Allowed optional fields include `view_url` and `feedback_url`. If you include `ad_data`, the render endpoint (`/render-ad`) is never called for that bid — make sure the ad creative is complete.
</Warning>

## Status Codes

| Status Code                 | Meaning               | Scenario                                                                     |
| --------------------------- | --------------------- | ---------------------------------------------------------------------------- |
| `200 OK`                    | Success               | Bid generated successfully or DSP chose not to bid                           |
| `400 Bad Request`           | Invalid input         | Malformed request body or missing required fields                            |
| `401 Unauthorized`          | Authentication failed | Missing or invalid `Authorization` header                                    |
| `429 Too Many Requests`     | Rate limit exceeded   | Publisher exceeded request quota                                             |
| `500 Internal Server Error` | Server error          | Exception during auction execution, database failure, or service unavailable |
