> ## 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 poll ad via the v2 bid-request API — request poll inventory, render the question and options, submit votes to the signed vote_url, and show aggregate results. Triggers on "Thrad", "thrads.ai", "poll ad", "ad poll", "poll ad in chat", "vote_url", "interactive ad unit".

# Thrad Poll

This skill installs a **direct API** integration of Thrad's poll ad — an interactive advertiser poll rendered inline in a chat conversation — on the **v2** bid-request API. The format is `poll` in the request and in the bid's `format`. Unlike the card, it has its own signed vote URL:

```http theme={null}
POST https://ssp.thrad.ai/api/v2/bid-request      # request the poll
POST <actions.vote_url>                                  # submit a vote
```

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

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 poll ad to my chat"
* "Request poll inventory from Thrad"
* "How do I submit a poll vote / show poll results?"
* "My poll vote returns 404"

## The two things to get right

**Polls are conversation-only.** The request must describe a conversation (messages, a summary or keywords). A poll with none of them is refused with `400`.

**Option taps are votes, not clicks.** They go to `actions.vote_url`. Don't wrap the poll in an advertiser link, and don't send option taps to `actions.click_url`. Advertiser traffic only goes through the post-vote CTA.

## 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 (Poll section): [https://docs.thrads.ai/api/guides/rendering-v2.md](https://docs.thrads.ai/api/guides/rendering-v2.md)
* Poll voting: [https://docs.thrads.ai/api/api-reference/poll-voting.md](https://docs.thrads.ai/api/api-reference/poll-voting.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 `poll` format to the opportunity. Its `assets` must list both `question` and `options`.

```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": "poll", "assets": { "question": {}, "options": {} } }
      ]
    }
  ],
  "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 `poll` 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 — Read the poll bid

A poll bid carries `id`, `price_cpm`, `currency`, `ttl_ms`, `advertiser` (`name`, `domain`, `logo_url`), `trackers`, `win_url`, `loss_url`, `format: "poll"`, `assets` and `actions`.

* `assets.question` — the question.
* `assets.options` — two or more `{ id, text }`. Show the `text`, vote with the `id`.
* `assets.cta` — the label for the advertiser link shown after the vote. Present only with `actions.click_url`.
* `actions.vote_url` — always present.
* `actions.click_url` — the post-vote advertiser link. May be absent: handle it.
* `actions.feedback_url` — optional, for thumbs up / down (see below).
* An empty `bids` array is the normal no-fill path, not an error.

### Step 6 — Fire the impression

Fire every entry in `trackers` with a `GET`, each at its `event`, from the user's device: `rendered` when you draw the poll, `viewable` once it is at least 50% visible for at least 1 continuous second. Only `viewable` is paid: firing it before the poll is on screen costs the publisher money.

### Step 7 — Render, vote, show results

Render the options as buttons. On tap, `POST` the option's `id` to `actions.vote_url` exactly as returned — the signed token lives in its query string.

```javascript theme={null}
async function submitPollVote(voteUrl, optionId) {
  const response = await fetch(voteUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ option_id: optionId })
  });

  if (!response.ok) throw new Error("Poll vote failed");

  // Vote responses are wrapped in { status, data } — the results live in data.
  const payload = await response.json();
  return payload.data;
}
```

Replace the option buttons with the percentages and total from `data`. Then, if `actions.click_url` is present, show `assets.cta` (default "Learn More") linking to it — it records the click and redirects to the advertiser.

### Feedback and auction notices

**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 8 — Handle errors without breaking the chat

* **Duplicate votes are not an error.** A token records one vote; voting again returns `200` with the current results.
* **`404`** — the token is expired or not valid for this poll. Don't retry with it.
* **Anything else** — fail quietly and keep the chat working. Never block the conversation on a poll call.

### Step 9 — Verify before declaring done

Do not tell the user the integration is complete until:

1. A staging request listing `poll` returns a bid with `format: "poll"`. If staging returns no poll, check with [contact@thrad.ai](mailto:contact@thrad.ai) before building further.
2. The mixed-format path works — a `card` bid renders as a card, not a broken poll.
3. The question and every option render, and the poll is **not** wrapped in an advertiser link.
4. A vote returns `200`, and the UI swaps the buttons for results read from `data`.
5. Voting again with the same token returns `200` and doesn't double-count.
6. `rendered` trackers fire when the poll is drawn, `viewable` trackers only on viewability.
7. When present, the post-vote CTA appears and `actions.click_url` redirects to the advertiser.
8. Every request carries `device.geo.country`, `device.type` and `device.os`.
9. A deliberately wrong request (e.g. `"country": "us"`) returns `400` with `error.fields`, and the integration logs it rather than crashing.

## Common gotchas

* **Polls before the conversation starts** — refused; polls need a conversation.
* **Wrapping the poll in a click link** — option taps go to `vote_url`; advertiser traffic only via the post-vote `click_url`.
* **Reading results off the raw vote response** — they're wrapped in `{ status, data }`. Read `data`.
* **Rebuilding the vote URL** — `POST` to `vote_url` verbatim. Don't vote against raw ids or move the token into the body.
* **Assuming `click_url` and `cta` are always there** — polls without a click-through omit both.
* **Ad-blocker-bait naming** — avoid class names, IDs, and asset paths containing `ad`, `ads`, `advert`, `banner`, `sponsor`, `promo`.
* **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.
* **Never invent** poll fields or endpoints. If it isn't in the Thrad docs, it doesn't exist.

## 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)
* Poll voting: [https://docs.thrads.ai/api/api-reference/poll-voting.md](https://docs.thrads.ai/api/api-reference/poll-voting.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.