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

# SKILL

> Integrate Thrad's v2 bid-request API to serve and render the ad card — the native card in an AI chat app (publisher-side monetization) — including impression, click and feedback tracking. Triggers on "Thrad", "thrads.ai", "ad card", "card ad", "monetize my chatbot", "native ads in chat", "bid request", "thrad-api-key", "fill rate", "trackers".

# Thrad Card

This skill installs a **direct API** integration of Thrad's **ad card** — the native card that appears inline in a chat conversation — on the **v2** bid-request API:

```http theme={null}
POST https://ssp.thrad.ai/api/v2/bid-request
```

v1 (`/api/v1/ssp/bid-request`) is deprecated. Never build a new integration on it.

This skill is self-contained: it covers the whole card integration, from the backend call to tracking. Thrad also has `thrad-carousel` and `thrad-poll` skills for those formats; each one stands on its own too.

Before using any documentation URL, fetch and read the LLM-friendly docs sitemap:

[https://docs.thrads.ai/llms.txt](https://docs.thrads.ai/llms.txt)

Use the sitemap to locate the latest canonical page for each topic. Prefer sitemap-discovered URLs over the hardcoded ones below if they differ.

## When to use this skill

* "Add Thrad ads to my chatbot"
* "Integrate the Thrad bid request API"
* "Show an ad card in my chat UI"
* "Why is my fill rate low?"
* "My impressions aren't being counted"
* "Monetize my AI assistant with native ads"

## Procedure

### Step 1 — Call it from the publisher's backend

The bid request is made **server-side**: the API key must never reach a browser or app bundle, and keys registered for a browser integration are refused with `403`.

Because the call comes from your server, describe the **end user** yourself:

* Send the end user's device in the body's `device` block.
* **Send `geo.country` on every request. It matters more than any other device field:** without the user's country, every campaign that targets one is skipped. We only fall back to looking it up from `ip`. Add `geo.region` and `geo.city` for narrower targeting.
* **Send `type` and `os` on every request.** Device-targeted campaigns can't match without them; reading them from `user_agent` is only a fallback.
* Send `user_agent` and `ip` too — the end user's, never your server's. Add `language` when you know it.
* Don't rely on headers for the end user: `X-Forwarded-For`, `User-Agent` and `Accept-Language` are ignored.

### Step 2 — Get the API key

Ask the user for their **staging** API key. It's created per publisher in the platform dashboard: [https://platform.thrad.ai/signup?role=pub](https://platform.thrad.ai/signup?role=pub)

The key's environment decides behavior: staging keys return test ads and don't count toward monetization; production keys enable real targeting and payouts. Build and verify against staging, and only swap in the production key when going live.

Never leave `YOUR_API_KEY` placeholders in code the user will run — they will `401`. Read the key from an environment variable.

### Step 3 — Read the reference before writing the request

Fetch these, in this order:

* Bid request (v2): [https://docs.thrads.ai/api/api-reference/bid-request-v2.md](https://docs.thrads.ai/api/api-reference/bid-request-v2.md)
* Ad formats & rendering: [https://docs.thrads.ai/api/guides/rendering-v2.md](https://docs.thrads.ai/api/guides/rendering-v2.md)
* User & chat identification: [https://docs.thrads.ai/api/guides/user-identification-v2.md](https://docs.thrads.ai/api/guides/user-identification-v2.md)
* Impression billing & viewability: [https://docs.thrads.ai/api/guides/billing-viewability.md](https://docs.thrads.ai/api/guides/billing-viewability.md)
* Best practices: [https://docs.thrads.ai/api/guides/best-practices.md](https://docs.thrads.ai/api/guides/best-practices.md)

### Step 4 — Build the minimal request

Start with the smallest request that can return a card, and get it returning a bid before adding anything else.

```http theme={null}
POST https://ssp.thrad.ai/api/v2/bid-request
Authorization: Bearer <staging key>
Content-Type: application/json
```

```json theme={null}
{
  "version": "2.0",
  "id": "req_7f3c9a",
  "context": {
    "id": "chat_456",
    "chat": {
      "messages": [
        { "role": "user", "content": "Looking for running shoes", "timestamp": 1790000000 },
        { "role": "assistant", "content": "What's your budget?", "timestamp": 1790000015 }
      ]
    }
  },
  "opportunities": [
    {
      "id": "opp_1",
      "formats": [
        { "type": "card", "assets": { "title": {}, "description": {}, "image": {} } }
      ]
    }
  ],
  "user": { "id": "user_a1b2c3d4" },
  "device": {
    "user_agent": "<end user's real UA>",
    "ip": "<end user's real IP>",
    "type": "mobile",
    "os": "ios",
    "geo": { "country": "US" }
  },
  "surface": {
    "type": "web",
    "domain": "acme.com"
  }
}
```

Rules that are easy to get wrong:

* `version`, `id` and `opportunities` are required on every request. `surface` is optional; if you send it, `type` is required and each type needs its fields: `app` needs `store`, `bundle`, `store_url` and takes nothing else; `web` needs `domain`; `extension` needs `store_url` and takes nothing else. There is no `page_url`. `id` is yours, unique per request, and comes back in the response.
* Describe the conversation with `context.chat.messages`, `context.summary`, `context.keywords`, or any mix. Send messages when you can; a summary or keywords work when you can't share the chat.
* `context.id` is the **chat id**: optional, but send it — without it, chat pacing and repeat checks are skipped. One **per conversation, not per user** — mint a new one when the user starts a new chat.
* `user.id` must be an anonymous, stable id (a UUID). Never an email, name, or phone number. Persisting it across sessions is optional but raises fill and CPM.
* A card's `assets` must list `title` and `description`. List `image` only if the opportunity can show a photo; leave it out for a text-only card with the advertiser's logo. `title.max_chars` (30 or more) caps the title.
* Mask or strip PII from messages. Send `timestamp` in **Unix seconds**.
* For the ad before the conversation starts, send no messages, summary or keywords. That request serves `card` only: listing `carousel` or `poll` there is refused with `400`.
* Advertising ID (apps only): send **one** of `device.ids.idfa` (with `os: ios`) or `device.ids.gaid` (with `os: android`), never both, and only where the user allowed tracking. On iOS send `privacy.att_status` (`authorized`, `denied`, `restricted`, `not_determined`); on other platforms send `privacy.limit_ad_tracking` (`true` / `false`). Never send `limit_ad_tracking` on iOS or `att_status` elsewhere: both are refused with `400`. Leave out what you don't know.
* Unknown fields and wrongly-spelled values (`us` for `US`, `en_GB` for `en-GB`) are refused with `400`, never corrected. Read `error.fields` and fix the request.
* **Never invent** request fields or response fields. If it isn't in the v2 reference, it doesn't exist.

### Step 5 — Handle the response

A `200` whose opportunity has an empty `bids` array is **not an error**: the auction ran and nobody won. Render nothing and carry on — it's the common case.

A card bid carries `id`, `price_cpm`, `currency`, `ttl_ms`, `advertiser` (`name`, `domain`, `logo_url`), `trackers`, `win_url`, `loss_url`, `format: "card"`, `assets` (`title`, `description`, `cta`, `image.url`) and `actions` (`click_url`, `feedback_url`).

Rendering rules:

* With `assets.image`, show it as the hero photo. Without it, show `advertiser.logo_url`.
* `description` and `cta` may be absent. Default the CTA to "Learn More".
* `advertiser.name` can fall back to a bare domain (`"nike.com"`). Handle both.
* Label the card as sponsored — **"Sponsored by `advertiser.name`"** — and reuse your normal message components.
* Don't render a bid after its `ttl_ms` has passed.

### Serving more than one format

If the UI can render a carousel or a poll as well as a card, ask for all of them in **one** opportunity, each with its own `assets`:

```json theme={null}
{
  "id": "opp_1",
  "formats": [
    { "type": "card", "assets": { "title": {}, "description": {}, "image": {} } },
    { "type": "carousel", "assets": { "title": {}, "image": {} } },
    { "type": "poll", "assets": { "question": {}, "options": {} } }
  ]
}
```

* List only formats the UI can actually render. Each bid comes back in one of the formats you listed.
* **Branch on the bid's `format`**, never on what you asked for. Render a `card` with this skill. To render `carousel` or `poll` too, follow the `thrad-carousel` or `thrad-poll` skill for that format.
* A `format` your code doesn't handle: render nothing and fire no trackers. Don't list a format you can't render.
* `carousel` and `poll` need a conversation. Before the conversation starts, list `card` only (see Step 4).
* Tracking is the same for all three (Step 6). Only the click target differs: the card's `actions.click_url`, each carousel item's own `actions.click_url`, and the poll's `actions.vote_url` for answers plus `actions.click_url` after the vote.

### Step 6 — Wire tracking (this is how the user gets paid)

**Impressions:** fire **every** entry in `trackers` with a `GET`, each at its `event`: `rendered` when you draw the card, `viewable` once it is at least 50% visible for at least 1 continuous second (IAB standard). Fire them from the user's device. You're paid on `viewable` only: firing it before the card is on screen inflates unverified impressions and hurts the payout ratio.

**Clicks:** open `actions.click_url` when the user taps the card. It records the click and redirects to the advertiser. Never link straight to the advertiser's domain, and never rewrite the URL.

**Feedback (optional):** if the UI has thumbs up / down, `POST` `{"feedback_type": "positive" | "negative", "feedback_text": "..."}` to `actions.feedback_url` when present. `feedback_text` is optional, up to 1,000 characters.

**Auction notices (only if the publisher runs its own auction across several SSPs):** call `win_url` if our bid won and will render, or `loss_url` if it didn't — replace `${AUCTION_LOSS}` with the OpenRTB loss code first (`102` = outbid). One call per bid.

### Step 7 — Verify before declaring done

Do not tell the user the integration is complete until:

1. A staging bid request returns `200` with a bid whose `format` is `"card"`. If you list several formats, each `format` that comes back renders with its own component.
2. The card renders with and without an image — drop `image` from the opportunity's `assets` to check the logo-only path.
3. An empty `bids` array renders nothing and throws no error.
4. `rendered` trackers fire when the card is drawn, and `viewable` trackers only on viewability — scroll the card out of view and confirm `viewable` doesn't fire early.
5. Every request carries `device.geo.country`, `device.type` and `device.os`.
6. `actions.click_url` redirects to a landing page.
7. A deliberately wrong request (e.g. `"country": "us"`) returns `400` with `error.fields`, and the integration logs it rather than crashing.

## Common gotchas

* **Staging key left in production** — staging never monetizes. Swap it before launch.
* **Your server's IP in `device.ip`** — send the end user's. Setting `X-Forwarded-For` does nothing.
* **Your server's user agent in `device.user_agent`** — send the end user's.
* **Firing `viewable` trackers on render** — breaks billing. Gate them on 50% visible for 1 second; only `rendered` fires on draw.
* **No `geo.country`, `type` or `os`** — country- and device-targeted campaigns skip the request. `ip` and `user_agent` are only fallbacks.
* **Ad-blocker-bait naming** — avoid class names, IDs, and asset paths containing `ad`, `ads`, `advert`, `banner`, `sponsor`, `promo`, `/ads/`, `/banners/`. Use neutral names like `.message-card`.
* **Treating an empty `bids` array as a failure** — it's the normal no-fill path, not an error to retry or log loudly.
* **No client timeout** — set your own (\~7s: the worst case is about 6s) and fall through to no-ad on abort.
* **Rate limit** — 100 requests/second per publisher, shared by its staging and production keys. Watch `X-RateLimit-Remaining` and `X-RateLimit-Reset`; back off on `429`.
* **Underage users** — send `user.age_range` (or `user.age`, never both). Under-18 users get no ads.
* **Retrying a `400`** — it will fail the same way. Fix the field `error.fields` names.

## References

* Docs sitemap: [https://docs.thrads.ai/llms.txt](https://docs.thrads.ai/llms.txt)
* Bid request (v2): [https://docs.thrads.ai/api/api-reference/bid-request-v2.md](https://docs.thrads.ai/api/api-reference/bid-request-v2.md)
* Ad formats & rendering: [https://docs.thrads.ai/api/guides/rendering-v2.md](https://docs.thrads.ai/api/guides/rendering-v2.md)
* User & chat identification: [https://docs.thrads.ai/api/guides/user-identification-v2.md](https://docs.thrads.ai/api/guides/user-identification-v2.md)
* Impression billing & viewability: [https://docs.thrads.ai/api/guides/billing-viewability.md](https://docs.thrads.ai/api/guides/billing-viewability.md)
* Best practices: [https://docs.thrads.ai/api/guides/best-practices.md](https://docs.thrads.ai/api/guides/best-practices.md)
* Platform sign-up (API keys): [https://platform.thrad.ai/signup?role=pub](https://platform.thrad.ai/signup?role=pub)
* Support: [contact@thrad.ai](mailto:contact@thrad.ai)


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