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

> OpenRTB 2.6 bid endpoint your DSP must expose. Thrad SSP POSTs a BidRequest and expects a BidResponse or no-bid.

<Note>
  The endpoint URL is yours — configure it with the Thrad team during onboarding.
</Note>

## Headers

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

<ParamField header="x-openrtb-version" type="string" required>
  Always `2.6` — explicit version declaration. Some DSPs gate the parser on this header; reject unsupported versions with `400`.
</ParamField>

<ParamField header="Accept" type="string">
  `application/json` — sent on every request.
</ParamField>

## Authentication

Two auth styles are supported per DSP — picked at onboarding and stored against your DSP config.

<ParamField header="Authorization" type="string">
  `Bearer <your-shared-key>` — provided by you at onboarding. Use this if you prefer header-based auth.
</ParamField>

<Note>
  **Or:** pubid-in-URL pattern. If your endpoint identifies the publisher via a URL query parameter (e.g. `https://your-host/bid?pubid=12345`), provide the full URL with the parameter baked in at onboarding and leave the bearer token blank. We will not add any `Authorization` header in that case. NoBid is integrated this way.
</Note>

<Tabs>
  <Tab title="BidRequest">
    ## Envelope

    <ParamField body="id" type="string" required>Auction id. Echo in `BidResponse.id`.</ParamField>
    <ParamField body="at" type="integer" required>Always `1` — first-price.</ParamField>
    <ParamField body="tmax" type="integer" required>Always `2000` ms. Responses after this are dropped.</ParamField>
    <ParamField body="cur" type="string[]" required>Always `["USD"]`.</ParamField>
    <ParamField body="test" type="integer" required>`1` during onboarding, `0` in production.</ParamField>

    ## `imp[]`

    Always a single impression.

    <ParamField body="imp[].id" type="string" required>Always `"1"`.</ParamField>

    <ParamField body="imp[].tagid" type="string" required>
      Slot identifier. Encodes both format and render mode — key your bid multipliers, frequency caps, and creative eligibility off this. One of:

      | tagid                        | Format          | Mode                                           | DSP must populate                     |
      | ---------------------------- | --------------- | ---------------------------------------------- | ------------------------------------- |
      | `sponsored_message_image_v1` | In-conversation | Image preferred (logo also accepted)           | Asset 4 (image) **or** asset 5 (icon) |
      | `sponsored_message_text_v1`  | In-conversation | Text only — image will be rejected server-side | Asset 5 (icon/logo) — required        |
      | `opener_image_v1`            | Pre-chat opener | Image preferred (logo also accepted)           | Asset 4 (image) **or** asset 5 (icon) |
      | `opener_text_v1`             | Pre-chat opener | Text only — image will be rejected server-side | Asset 5 (icon/logo) — required        |

      Bumps to `_vN` only happen when the slot's creative contract changes in a way that breaks existing creatives.
    </ParamField>

    <ParamField body="imp[].bidfloor" type="float" required>Floor in CPM USD.</ParamField>
    <ParamField body="imp[].bidfloorcur" type="string" required>Always `"USD"`.</ParamField>

    <Note>
      **Fields Thrad intentionally does not send:**

      * `imp[].pos` — none of the oRTB position enums (above-fold, full-screen, etc.) cleanly describe inline chatbot inventory. Route on `imp[].tagid` and the Native asset list instead.
      * `imp[].battr` — the Native 1.2 asset shape Thrad requests already structurally rejects video, audio, expandable, and pop creatives. Sending `battr` would be noise on top.
    </Note>

    <ParamField body="imp[].secure" type="integer" required>Always `1`. All URLs in your response must be HTTPS.</ParamField>
    <ParamField body="imp[].native.ver" type="string" required>Always `"1.2"` (outer ver double-declaration). The inner `native.request` envelope also carries `ver: "1.2"`.</ParamField>

    <ParamField body="imp[].native.request" type="string" required>
      JSON-encoded Native 1.2 request object. Shape forks by render mode (signalled in `imp[].tagid`) — the publisher's `image_enabled` config drives both the tagid and the asset list, so they're always consistent.

      **Image mode** (`tagid` ends in `_image_v1`) — accepts both image and text creatives:

      ```json theme={null}
      {
        "native": {
          "ver": "1.2",
          "context": 1,
          "contextsubtype": 10,
          "plcmttype": 4,
          "plcmtcnt": 1,
          "assets": [
            { "id": 1, "required": 1, "title": { "len": 90 } },
            { "id": 2, "required": 1, "data": { "type": 2, "len": 200 } },
            { "id": 3, "required": 1, "data": { "type": 1, "len": 50 } },
            { "id": 4, "required": 0, "img": { "type": 3, "wmin": 400, "hmin": 400, "mimes": ["image/jpeg", "image/png", "image/webp"] } },
            { "id": 5, "required": 0, "img": { "type": 1, "wmin": 100, "hmin": 100, "mimes": ["image/jpeg", "image/png", "image/webp"] } },
            { "id": 6, "required": 1, "data": { "type": 12, "len": 15 } }
          ],
          "eventtrackers": [{ "event": 1, "methods": [1] }],
          "privacy": 1
        }
      }
      ```

      **Text mode** (`tagid` ends in `_text_v1`) — publisher cannot render images. Asset 4 is **omitted entirely** and asset 5 (logo) becomes required:

      ```json theme={null}
      "assets": [
        { "id": 1, "required": 1, "title": { "len": 90 } },
        { "id": 2, "required": 1, "data": { "type": 2, "len": 200 } },
        { "id": 3, "required": 1, "data": { "type": 1, "len": 50 } },
        { "id": 5, "required": 1, "img": { "type": 1, "wmin": 100, "hmin": 100, "mimes": ["image/jpeg", "image/png", "image/webp"] } },
        { "id": 6, "required": 1, "data": { "type": 12, "len": 15 } }
      ]
      ```

      **Required assets in both modes:** 1 (title), 2 (description), 3 (sponsoredBy), 6 (CTA). Bids missing any of these are rejected.

      **Asset 5 (logo):** required in text mode, optional in image mode.

      **Asset 4 (main image):** only present (and only acceptable in your response) in image mode. If you return `image_url` on a text-mode request, the SSP **drops it server-side** before render — a logo-only creative will still serve, but if you sent neither asset 5 nor a usable image, the bid is silently lost. Always honour the tagid signal.

      **`title.len`** is parameterised by the publisher's `max_headline_chars` setting (≥ 30, default 90). The other `len` values and image dimensions are global per format. `len` is a **maximum** character count; `wmin`/`hmin` are minimum image dimensions in pixels.
    </ParamField>

    <ParamField body="imp[].ext.thrad" type="object">
      Thrad extension — conversation-level signals that do not fit any standard oRTB field. Lives under `ext` so spec-strict bidders can ignore it cleanly; none of these affect SSP-side auction logic, they're hints your DSP can use for creative selection or pacing.

      <Expandable title="fields">
        <ParamField body="chat_id" type="string">Conversation id. Stable across turns within the same chat. Omitted on `opener` (no chat exists yet).</ParamField>
        <ParamField body="n_ads_before" type="integer">Number of ads already served earlier in this conversation. Useful for frequency capping and creative rotation.</ParamField>
        <ParamField body="prefetched" type="boolean">`true` if the request was issued speculatively before the assistant's reply is shown to the user. Prefetched impressions still bill normally on render confirmation.</ParamField>
        <ParamField body="request_type" type="string">Either `"opener"` (pre-chat slot) or `"sponsored_message"` (in-conversation slot). Mirrors `imp[].tagid` and lets you key creative rules without parsing the tag string.</ParamField>
      </Expandable>
    </ParamField>

    ## `site` / `app`

    Exactly one of `site` or `app` is present per request. `app` ships when the publisher's inventory is in-app (`ios_app` / `android_app`); otherwise `site`. The `publisher` subblock is identical in both.

    <ParamField body="site.id" type="string" required>
      Opaque publisher slot id. Resolved in priority: (1) per-DSP `external_site_id` override if your DSP requires a fixed id (set at onboarding), (2) the chatbot's `public_id` (UUID), (3) stringified internal publisher id (legacy fallback). Stable for a given chatbot.
    </ParamField>

    <ParamField body="site.domain" type="string">
      Hostname where the chatbot is rendered (lowercased, scheme stripped). Derived from the publisher's per-request URL. Omitted when the publisher doesn't provide a URL.
    </ParamField>

    <ParamField body="site.page" type="string">Full page URL where the chatbot is rendered. Omitted when not provided.</ParamField>
    <ParamField body="site.privacypolicy" type="integer" required>Always `1`. The Thrad network operates under a privacy policy.</ParamField>

    <ParamField body="site.publisher.id" type="string" required>
      Opaque publisher organisation id. Resolved in priority: (1) per-DSP `external_site_id` override, (2) the organisation's `public_id` (UUID), (3) stringified internal publisher id. Equals `site.id` when override (1) is in play; differs otherwise.
    </ParamField>

    <ParamField body="site.publisher.name" type="string">Organisation name, **lowercased**. Adtech allow/denylists key on string equality — we normalise upstream so you don't have to. Omitted when unset.</ParamField>
    <ParamField body="site.publisher.domain" type="string">Organisation domain, **lowercased**, scheme + path stripped. Omitted when unset. Different concept from `site.domain` — this is the publisher organisation's domain (sellers.json target once Wave 4 ships); `site.domain` is where the ad runs.</ParamField>

    <ParamField body="site.cat" type="string[]">
      IAB Content Taxonomy 3.0 Tier 1 Unique ID. Always paired with `site.cattax: 7`. IDs are mostly numeric (e.g. `"1"` Automotive, `"483"` Sports, `"596"` Technology & Computing) but six are short alphanumeric extension IDs (`"JLBCU7"` Entertainment, `"8VZQHL"` Events, `"SPSHQ5"` Genres, `"1KXCLD"` Holidays, `"v9i3On"` Sensitive Topics). Sub-tiers are not currently signalled.
    </ParamField>

    <ParamField body="site.cattax" type="integer">Always `7` (IAB Content Taxonomy 3.0). Present alongside `site.cat`.</ParamField>
    <ParamField body="site.search" type="string">LLM-rephrased conversational query. Primary intent signal.</ParamField>
    <ParamField body="site.keywords" type="string">Comma-separated topical keywords.</ParamField>

    ### `app` block (in-app inventory only)

    Same `id` / `publisher` / `cat` / `cattax` / `search` / `keywords` semantics as `site`. Additionally:

    <ParamField body="app.bundle" type="string">iOS bundle ID (`com.example.app`) or Android package name. Required for in-app inventory.</ParamField>
    <ParamField body="app.storeurl" type="string">App Store / Play Store listing URL.</ParamField>
    <ParamField body="app.domain" type="string">App marketing site domain — reuses the publisher organisation's domain (same value as `app.publisher.domain`).</ParamField>

    ## `device`

    <ParamField body="device.ua" type="string" required>End-user User-Agent.</ParamField>
    <ParamField body="device.ip" type="string">End-user IPv4. Omitted when the user is on IPv6 (see `device.ipv6`).</ParamField>
    <ParamField body="device.ipv6" type="string">End-user IPv6. Sent instead of `device.ip` when applicable.</ParamField>
    <ParamField body="device.devicetype" type="integer" required>`2`=desktop, `4`=phone, `5`=tablet, `0` for unknown.</ParamField>
    <ParamField body="device.js" type="integer" required>Always `1`. Thrad's chatbot widget is JS-rendered — `noscript` fallbacks are never needed.</ParamField>

    <ParamField body="device.language" type="string">
      ISO 639-1 alpha-2. Priority: (1) conversation language auto-detected from `messages` (best for in-chat ads), (2) Accept-Language header parsed from the publisher request (fallback for openers and sparse conversations). Field is omitted entirely when neither signal is available.
    </ParamField>

    <ParamField body="device.geo.country" type="string">ISO-3 country code.</ParamField>
    <ParamField body="device.geo.region" type="string">Region/state, when available.</ParamField>
    <ParamField body="device.geo.city" type="string">City, when available.</ParamField>
    <ParamField body="device.geo.type" type="integer">Always `2` (IP-based) when the `geo` block is present. Thrad does not collect GPS-grade geolocation.</ParamField>
    <ParamField body="device.os" type="string">OS string, UA-parsed.</ParamField>
    <ParamField body="device.ifa" type="string">Advertising id (IDFA/GAID). In-app only.</ParamField>
    <ParamField body="device.dnt" type="integer">`1` if Do Not Track.</ParamField>
    <ParamField body="device.lmt" type="integer">`1` if Limit Ad Tracking.</ParamField>

    ## `user`

    <ParamField body="user.id" type="string" required>Anonymous Thrad user id.</ParamField>
    <ParamField body="user.buyeruid" type="string">DSP cookie-match value, when available via cookie sync.</ParamField>
    <ParamField body="user.keywords" type="string">Mirrors `imp[].ext.thrad.keywords`.</ParamField>
    <ParamField body="user.ext.consent" type="string">IAB TCF v2.x consent string. Present when `regs.gdpr=1`.</ParamField>

    ## `regs`

    <ParamField body="regs.gdpr" type="integer" required>
      `1` when the user's country falls under GDPR / UK GDPR / EEA / Swiss FADP (based on Thrad SSP's IP-geo lookup), `0` otherwise. **`user.ext.consent` is not yet wired** — full TCF v2 forwarding lives on the Wave 4 roadmap. EU-compliant DSPs that require a consent string on `gdpr=1` should refuse to bid on those requests (the correct behavior); we explicitly declare `1` so eligibility logic isn't ambiguous.
    </ParamField>

    <ParamField body="regs.us_privacy" type="string">CCPA string, when supplied by the publisher. Not yet wired — Wave 4.</ParamField>
    <ParamField body="regs.gpp" type="string">GPP string, when supplied by the publisher. Not yet wired — Wave 4.</ParamField>
    <ParamField body="regs.coppa" type="integer">`1` if user is flagged under-13. Not yet wired — Wave 4.</ParamField>

    ## `source`

    <ParamField body="source.fd" type="integer" required>Always `0`. SSP is the entity making the auction decision.</ParamField>
    <ParamField body="source.tid" type="string" required>Transaction id. Echoes `BidRequest.id`.</ParamField>

    <ParamField body="source.schain" type="object">
      IAB SupplyChain object (sellers.json chain) — oRTB 2.6 location (root `source`, not `source.ext`). Sent on every request where the publisher's organisation `public_id` is available. Single-node chain today: `asi` is Thrad's host (matches our `sellers.json`), `sid` is the publisher organisation `public_id`, `hp: 1`. Verify the `asi` value resolves to a `seller_id` entry at `https://<asi>/sellers.json` if you do supply-chain validation.
    </ParamField>

    ## `bcat` / `badv`

    <ParamField body="bcat" type="string[]">Blocked IAB categories. Only present when set.</ParamField>
    <ParamField body="badv" type="string[]">Blocked advertiser domains. Only present when set.</ParamField>

    <RequestExample>
      ```json BidRequest theme={null}
      {
        "id": "auction_987654321",
        "at": 1,
        "tmax": 2000,
        "cur": ["USD"],
        "test": 0,
        "site": {
          "id": "pub_42",
          "domain": "yourchatbot.com",
          "page": "https://yourchatbot.com/chat",
          "cat": ["596"],
          "cattax": 7,
          "search": "best crm tools for 10 person startup",
          "keywords": "crm, software, startup, b2b",
          "publisher": { "id": "pub_42", "name": "Your Chatbot", "domain": "yourchatbot.com" }
        },
        "device": {
          "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
          "ip": "192.168.1.1",
          "devicetype": 2,
          "os": "OSX",
          "geo": { "country": "USA", "region": "CA", "city": "San Francisco", "type": 2 },
          "language": "en"
        },
        "user": {
          "id": "hashed_user_id_xyz987",
          "ext": { "consent": "<TCF v2 string when GDPR applies>" }
        },
        "regs": { "gdpr": 0 },
        "source": {
          "fd": 0,
          "tid": "auction_987654321",
          "schain": { "complete": 1, "ver": "1.0", "nodes": [
            { "asi": "thrad.ai", "sid": "<org-public-id>", "hp": 1 }
          ]}
        },
        "imp": [{
          "id": "1",
          "tagid": "sponsored_message_image_v1",
          "secure": 1,
          "bidfloor": 4.00,
          "bidfloorcur": "USD",
          "native": {
            "request": "{\"ver\":\"1.2\",\"context\":1,\"contextsubtype\":10,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"data\":{\"type\":2,\"len\":200}},{\"id\":3,\"required\":1,\"data\":{\"type\":1,\"len\":50}},{\"id\":4,\"required\":0,\"img\":{\"type\":3,\"wmin\":400,\"hmin\":400,\"mimes\":[\"image/jpeg\",\"image/png\",\"image/webp\"]}},{\"id\":5,\"required\":0,\"img\":{\"type\":1,\"wmin\":100,\"hmin\":100,\"mimes\":[\"image/jpeg\",\"image/png\",\"image/webp\"]}},{\"id\":6,\"required\":1,\"data\":{\"type\":12,\"len\":15}}],\"eventtrackers\":[{\"event\":1,\"methods\":[1]}],\"privacy\":1}"
          },
          "ext": {
            "thrad": {
              "chat_id": "chat_456",
              "n_ads_before": 2,
              "prefetched": true,
              "request_type": "sponsored_message"
            }
          }
        }]
      }
      ```
    </RequestExample>
  </Tab>

  <Tab title="BidResponse">
    ## Envelope

    <ResponseField name="id" type="string" required>Must echo `BidRequest.id`.</ResponseField>
    <ResponseField name="cur" type="string" required>Must be `"USD"`.</ResponseField>

    <ResponseField name="seatbid" type="array" required>
      Multiple `seatbid` entries and multiple `bid[]` per seatbid are accepted. The SSP picks the **highest-priced bid across all seatbids** as your DSP's representative for the cross-DSP auction. Ties are broken by first encountered. Bids with `bid.cur` set to anything other than `"USD"` are dropped before pricing. Non-winning bids in the same response still get an `lurl` notification — see [Notifications](#notifications) below.
    </ResponseField>

    ## `seatbid[]`

    <ResponseField name="seat" type="string">
      Buyer seat id. Identifies which buyer at your DSP placed the bid (e.g. agency, advertiser, or trading desk). Surfaced in Thrad analytics for per-seat revenue and quality slicing — recommended whenever your DSP fronts multiple buyers. Optional; omit or send empty if you have no seat concept.
    </ResponseField>

    ## `seatbid[].bid[]`

    <ResponseField name="id" type="string" required>Your unique bid id. Correlated with `nurl`, `lurl`, `burl`.</ResponseField>

    <ResponseField name="impid" type="string" required>
      Must echo `"1"` (Thrad sends a single impression per request). Non-`"1"` values are accepted today but logged for follow-up; strict envelope validation (v2) will hard-reject.
    </ResponseField>

    <ResponseField name="price" type="float" required>CPM USD. Must be ≥ `imp.bidfloor`. 2 decimal places — bids with more precision are rounded using `ROUND_HALF_UP` (see [Pricing & Commission](/dsp/guides/ortb/overview#pricing-commission)).</ResponseField>

    <ResponseField name="adm" type="string" required>
      JSON-encoded Native 1.2 ad markup. Both wrapper styles are accepted — the SSP auto-detects which one your DSP emits:

      **Spec-correct Native 1.2 (unwrapped, recommended):**

      ```json theme={null}
      {
        "ver": "1.2",
        "assets": [
          { "id": 1, "title": { "text": "Best CRM for startups" } },
          { "id": 2, "data": { "value": "Try HubSpot free for 30 days." } },
          { "id": 3, "data": { "value": "HubSpot" } },
          { "id": 4, "img": { "url": "https://your-dsp.com/img/hero.jpg", "w": 1200, "h": 627 } },
          { "id": 5, "img": { "url": "https://your-dsp.com/img/logo.png", "w": 100, "h": 100 } },
          { "id": 6, "data": { "value": "Try Free" } }
        ],
        "link": { "url": "https://your-dsp.com/click/abc123?price=${AUCTION_PRICE}" },
        "eventtrackers": [
          { "event": 1, "method": 1, "url": "https://your-dsp.com/imp/abc123" }
        ]
      }
      ```

      **Legacy Native 1.0/1.1 (wrapped, also accepted):**

      ```json theme={null}
      { "native": { "ver": "1.2", "assets": [ ... ], "link": { ... }, "eventtrackers": [ ... ] } }
      ```

      Native 1.2 §5.1 deprecated the outer `native` wrapper, but a meaningful share of DSPs still emit it. The SSP peels the wrapper when present and processes either shape identically. If `adm.ver` is set to anything other than `"1.2"`, a warning is logged and the bid still flows.

      **Impression trackers.** The SSP prefers Native 1.2 `eventtrackers[event=1, method=1]`. When `eventtrackers` is missing or empty, it falls back to the legacy `native.imptrackers[]` array (Native 1.0/1.1 compat) — bare URL strings, treated as `event=1` + `method=1`.

      Asset ids must match those requested. Missing required assets (1, 2, 3, 6) reject the bid.

      | Asset id                     | Publisher field                              |
      | ---------------------------- | -------------------------------------------- |
      | 1 (title)                    | `headline`                                   |
      | 2 (data.desc)                | `description`                                |
      | 3 (data.sponsoredBy)         | `advertiser`                                 |
      | 4 (img.main)                 | `image_url`                                  |
      | 5 (img.icon)                 | `logo_url`                                   |
      | 6 (data.ctatext)             | `cta_text`                                   |
      | `link.url`                   | `url` (wrapped through Thrad click redirect) |
      | `eventtrackers[event=1].url` | `view_url`                                   |
    </ResponseField>

    <ResponseField name="adomain" type="string[]" required>Advertiser domain(s). Used for brand-safety. Required and must be non-empty — bids with empty `adomain` log a warning today and are hard-rejected under strict envelope validation (v2).</ResponseField>
    <ResponseField name="nurl" type="string">Win notification URL. Fired once on win, before delivery. Include `${AUCTION_PRICE}`. **Must be HTTPS** — non-HTTPS URLs are dropped server-side (the bid still wins, only the notification is skipped).</ResponseField>
    <ResponseField name="lurl" type="string">Loss notification URL. Fired once on loss. Include `${AUCTION_LOSS}`. **Must be HTTPS** — non-HTTPS URLs are dropped.</ResponseField>
    <ResponseField name="burl" type="string">Billing URL. Fired once after publisher confirms impression rendered. Use for spend recognition. **Must be HTTPS** — non-HTTPS URLs are dropped.</ResponseField>
    <ResponseField name="cid" type="string">Campaign id. Recorded on the impression for analytics.</ResponseField>
    <ResponseField name="crid" type="string">Creative id. Recorded on the impression for analytics.</ResponseField>
    <ResponseField name="cat" type="string[]">IAB category for the creative. First entry is recorded on the impression for analytics.</ResponseField>

    ## Server validation

    Limits and checks the SSP applies to your response. Today most are **log-only** (the bid still flows) so DSPs get visibility before strict mode lands; strict envelope validation (v2) flips the listed checks to hard rejects.

    | Check                                              | Behaviour today                                   | Notes                                                           |
    | -------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------- |
    | Response body size                                 | Hard cap **1 MB** (rejected before JSON parse)    | Real oRTB responses are 1–10 KB; the cap is generous            |
    | `seatbid[]` count                                  | Hard cap **10** — extras are truncated with a log | Real responses send 1–3                                         |
    | `bid[]` per seatbid                                | Hard cap **20** — extras are truncated with a log | Real responses send 1–5                                         |
    | `BidResponse.id` echoes `BidRequest.id`            | Log-only                                          | Will hard-reject under strict mode                              |
    | `cur == "USD"` (envelope)                          | Log-only                                          | Per-bid `bid.cur != "USD"` is dropped today                     |
    | `bid.impid == "1"`                                 | Log-only                                          | Will hard-reject under strict mode                              |
    | `bid.adomain` non-empty                            | Log-only                                          | Will hard-reject under strict mode                              |
    | `adm.ver == "1.2"`                                 | Log-only                                          | Other versions accepted but flagged                             |
    | `nurl` / `lurl` / `burl` are HTTPS                 | URL dropped, bid still wins                       | Non-HTTPS leaks auction data in cleartext                       |
    | Asset shape matches request (id → outer/inner)     | Log-only                                          | Surfaces drift before it degrades to `ortb_no_renderable_asset` |
    | Image asset dimensions ≥ requested `wmin` / `hmin` | Log-only                                          | Declared dims only — fetched audit lands later                  |
    | Text asset length ≤ requested `len`                | Log-only                                          | Title limit is per-publisher (`max_headline_chars`)             |

    ## Notifications

    All three are fire-and-forget GETs (\~2 s timeout, no retry). Errors are logged and swallowed — no part of the auction or render path waits on them.

    | URL    | When fired                                          | Use for                       |
    | ------ | --------------------------------------------------- | ----------------------------- |
    | `nurl` | Auction win, after creative is committed for render | Bid bookkeeping, pacing       |
    | `lurl` | Auction loss (cross-DSP **and** in-response)        | Bidder tuning, price learning |
    | `burl` | Publisher impression confirmation                   | Spend recognition, billing    |

    ### Loss notifications (`lurl`) for multi-bid responses

    The SSP fires `lurl` in two situations and your endpoint should be able to handle both:

    * **Cross-DSP loser** — your DSP's representative bid (highest-priced bid in your response) lost the cross-DSP auction to another DSP. Reason code `102` ("lost to higher bid").
    * **In-response loser** — a non-winning bid inside your own response (you sent multiple bids in one or more seatbids; only the highest-priced one became your representative). Each in-response loser gets its own `lurl` fired with reason code `102`. This happens regardless of whether your representative bid won the cross-DSP auction — i.e. even if your DSP is the overall winner, your other bids in the same response still receive a loss notice.

    Bids dropped before pricing (non-USD `bid.cur`, missing `price`) do **not** receive an `lurl`.

    **Loss reason codes (`${AUCTION_LOSS}`):**

    | Code | Meaning                                       | Status                   |
    | ---- | --------------------------------------------- | ------------------------ |
    | 102  | Lost to higher bid (cross-DSP or in-response) | **Fired today**          |
    | 100  | Bid below floor                               | Coming soon              |
    | 3    | Invalid bid response                          | Coming soon              |
    | 7    | Invalid `adm`                                 | Coming soon              |
    | 6    | Advertiser domain blocked                     | Reserved (not yet wired) |
    | 1    | Internal SSP error                            | Reserved (not yet wired) |

    <Note>
      Today the SSP only fires `lurl` with reason `102`. Other rejection cases (below-floor, invalid response, invalid `adm`) drop the bid silently. Wiring those rejections to fire `lurl` with the appropriate code is on the v2 roadmap — your endpoint should be ready to receive them, but should also handle long stretches with only `102` in the wild.
    </Note>

    **Price macros** — substituted in `nurl`, `lurl`, `burl`, `adm`, and `adm.link.url`:

    | Macro                 | Value                                |
    | --------------------- | ------------------------------------ |
    | `${AUCTION_PRICE}`    | Clearing price (CPM USD, 2 decimals) |
    | `${AUCTION_ID}`       | `BidRequest.id`                      |
    | `${AUCTION_BID_ID}`   | `bid.id`                             |
    | `${AUCTION_IMP_ID}`   | Always `"1"`                         |
    | `${AUCTION_CURRENCY}` | `"USD"`                              |
    | `${AUCTION_LOSS}`     | Loss reason code                     |

    ## No-bid

    | Option                      | When to use                            |
    | --------------------------- | -------------------------------------- |
    | `204 No Content`            | Default. Lowest overhead.              |
    | `200` + `nbr`               | When reason code helps your analytics. |
    | `200` + empty `seatbid: []` | Legacy fallback.                       |

    Common `nbr` codes: `7` blocked publisher, `8` unmatched user, `9` reader cap, `10` DSP bid cap.

    <ResponseExample>
      ```json 200 - Bid (single) theme={null}
      {
        "id": "auction_987654321",
        "cur": "USD",
        "seatbid": [{
          "seat": "your-dsp",
          "bid": [{
            "id": "bid_abc123",
            "impid": "1",
            "price": 7.50,
            "adm": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"text\":\"Best CRM for startups\"}},{\"id\":2,\"data\":{\"value\":\"Try HubSpot free for 30 days.\"}},{\"id\":3,\"data\":{\"value\":\"HubSpot\"}},{\"id\":4,\"img\":{\"url\":\"https://your-dsp.com/img/hero.jpg\",\"w\":1200,\"h\":627}},{\"id\":5,\"img\":{\"url\":\"https://your-dsp.com/img/logo.png\",\"w\":100,\"h\":100}},{\"id\":6,\"data\":{\"value\":\"Try Free\"}}],\"link\":{\"url\":\"https://your-dsp.com/click/abc123?price=${AUCTION_PRICE}\"},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"https://your-dsp.com/imp/abc123\"}]}}",
            "adomain": ["hubspot.com"],
            "crid": "creative_42",
            "nurl": "https://your-dsp.com/win?bid=${AUCTION_BID_ID}&price=${AUCTION_PRICE}",
            "lurl": "https://your-dsp.com/loss?bid=${AUCTION_BID_ID}&reason=${AUCTION_LOSS}",
            "burl": "https://your-dsp.com/billed?bid=${AUCTION_BID_ID}&price=${AUCTION_PRICE}"
          }]
        }]
      }
      ```

      ```json 200 - Multi-bid (multiple seats) theme={null}
      {
        "id": "auction_987654321",
        "cur": "USD",
        "seatbid": [
          {
            "seat": "buyer_acme",
            "bid": [
              {
                "id": "bid_acme_1",
                "impid": "1",
                "price": 7.50,
                "adm": "...",
                "adomain": ["acme.com"],
                "crid": "creative_acme_42",
                "nurl": "https://your-dsp.com/win?bid=${AUCTION_BID_ID}&price=${AUCTION_PRICE}",
                "lurl": "https://your-dsp.com/loss?bid=${AUCTION_BID_ID}&reason=${AUCTION_LOSS}",
                "burl": "https://your-dsp.com/billed?bid=${AUCTION_BID_ID}&price=${AUCTION_PRICE}"
              },
              {
                "id": "bid_acme_2",
                "impid": "1",
                "price": 6.80,
                "adm": "...",
                "adomain": ["acme.com"],
                "crid": "creative_acme_43",
                "lurl": "https://your-dsp.com/loss?bid=${AUCTION_BID_ID}&reason=${AUCTION_LOSS}"
              }
            ]
          },
          {
            "seat": "buyer_globex",
            "bid": [{
              "id": "bid_globex_1",
              "impid": "1",
              "price": 5.25,
              "adm": "...",
              "adomain": ["globex.com"],
              "crid": "creative_globex_7",
              "lurl": "https://your-dsp.com/loss?bid=${AUCTION_BID_ID}&reason=${AUCTION_LOSS}"
            }]
          }
        ]
      }
      ```

      In the multi-bid example: `bid_acme_1` (\$7.50) is your DSP's representative for the cross-DSP auction. `bid_acme_2` and `bid_globex_1` are in-response losers — both will receive an `lurl` GET with reason `102` regardless of whether `bid_acme_1` wins or loses overall.

      ```json 204 - No Bid (recommended) theme={null}
      ```

      ```json 200 - No Bid with reason theme={null}
      {
        "id": "auction_987654321",
        "nbr": 102
      }
      ```
    </ResponseExample>
  </Tab>
</Tabs>

***

<Note>
  *Last updated: 2026-06-14 — `source.schain`, `regs.gdpr` (country-based), headers, and pubid-in-URL auth reflect production behavior since the oRTB v1 launch.*
</Note>
