> ## 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 carousel ad format via the v2 bid-request API — a strip of 3–5 product cards in an AI chat app, with per-item click tracking and one impression for the whole strip. Triggers on "Thrad", "thrads.ai", "carousel ad", "product carousel", "carousel in chat".

# Thrad Carousel

This skill installs a **direct API** integration of Thrad's **carousel** — a horizontally scrollable strip of 3–5 product cards from a single advertiser, shown inline in a chat conversation. Each item is a product (title, image, optional price) and its own click target; the strip shares one brand logo and one sponsored label.

It uses the **v2** bid-request API. The format is `carousel` in the request and in the bid's `format`. It is **conversation-only**: never ask for it before the conversation starts (a request with no messages, summary or keywords).

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

This skill is self-contained: it covers the whole integration, from the backend call to tracking.

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 a product carousel ad to my chatbot"
* "Show multiple products in one Thrad ad"
* "Integrate the carousel ad format"
* "Render Thrad carousel items / track carousel clicks"

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

* Ad formats & rendering (Carousel section): [https://docs.thrads.ai/api/guides/rendering-v2.md](https://docs.thrads.ai/api/guides/rendering-v2.md)
* 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)
* 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 request

Add a `carousel` format to the opportunity. Its `assets` must list both `title` and `image` — an item can't render without either.

```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": "carousel", "assets": { "title": {}, "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"
  }
}
```

If the UI can also render other formats, list them in the same opportunity, each with its own `assets`. **Branch on the bid's `format`**, never on what you asked for. Render a `carousel` with this skill; a `format` your code doesn't handle: render nothing and fire no trackers.

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. **This format needs a conversation:** with none of them, the request is refused with `400`.
* `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.
* Mask or strip PII from messages. Send `timestamp` in **Unix seconds**.
* 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

An empty `bids` array is the normal no-fill path — render nothing, no error.

A carousel bid carries `id`, `price_cpm`, `currency`, `ttl_ms`, `advertiser` (`name`, `domain`, `logo_url`), `trackers`, `win_url`, `loss_url`, `format: "carousel"`, `assets.items` and `actions.feedback_url`. Each item: `title`, `image.url`, optional `price` (`amount` as a string, plus `currency`), and `actions.click_url`.

Rendering rules:

* Render items **left to right in the order given**. You may drop trailing items if space demands; **never reorder**.
* Show `advertiser.logo_url` and **"Sponsored by `advertiser.name`"** in the strip's header.
* Show an item's price with its currency when present; leave the price line out when absent.
* Items are always currently servable products — render what you receive, don't cache creatives across requests.
* Don't render a bid after its `ttl_ms` has passed.

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

* **Impression:** fire every entry in `trackers` **once per strip**, each at its `event` — one impression for the whole strip, however many items or however far it's scrolled. `rendered` fires when you draw the strip; `viewable` once it is at least 50% visible for at least 1 continuous second. Fire from the user's device.
* **Clicks:** each item opens **its own `actions.click_url`** — it records which item was clicked and redirects to the product page. There is no click for the strip as a whole. Never link straight to the product URL or add your own tracking to it. Several clicks in one impression are fine; each counts.

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

1. A staging request listing `carousel` returns a bid with `format: "carousel"` and 3–5 `assets.items`.
2. The strip renders in order; a narrow viewport drops the tail rather than reordering.
3. An item without `price` renders without a price line.
4. An empty `bids` array renders nothing and throws no error.
5. The trackers fire once per strip: `rendered` when drawn, `viewable` only on viewability.
6. Each item's `click_url` redirects to its product page.
7. Every request carries `device.geo.country`, `device.type` and `device.os`.
8. A deliberately wrong request (e.g. `"country": "us"`) returns `400` with `error.fields`, and the integration logs it rather than crashing.

## Common gotchas

* **Carousel before the conversation starts** — refused with `400`; the carousel needs a conversation.
* **Leaving `title` or `image` out of the carousel's `assets`** — refused with `400`; both are required.
* **Firing the trackers per item** — one impression per strip.
* **Reordering or re-sorting items** — the order is part of the creative. Drop from the tail if you must; never reorder.
* **Ad-blocker-bait naming** — use neutral class names (`.product-strip`, not `.ad-carousel`).
* **Staging key left in production** — staging never monetizes. Swap it before launch.
* **Your server's IP or user agent in `device`** — send the end user's. Setting `X-Forwarded-For` does nothing.
* **No `geo.country`, `type` or `os`** — country- and device-targeted campaigns skip the request. `ip` and `user_agent` are only fallbacks.
* **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)
* Ad formats & rendering: [https://docs.thrads.ai/api/guides/rendering-v2.md](https://docs.thrads.ai/api/guides/rendering-v2.md)
* 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)
* Impression billing & viewability: [https://docs.thrads.ai/api/guides/billing-viewability.md](https://docs.thrads.ai/api/guides/billing-viewability.md)
* User & chat identification: [https://docs.thrads.ai/api/guides/user-identification-v2.md](https://docs.thrads.ai/api/guides/user-identification-v2.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.