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

# Poll Tracking (v1)

> Submit sponsored poll votes and retrieve signed aggregate results for direct API integrations.

Sponsored poll bid responses include signed `vote_url` and `results_url` values. Use those URLs exactly as returned by the bid response — the signed token authorizes access to a rendered poll instance and keeps clients from voting against or reading raw poll IDs.

> [!WARNING]
>
> Voting records poll engagement only. It does not redirect to the advertiser. If the poll response includes `cta_url`, show that CTA after voting and use it for advertiser click-throughs.

## Vote

```http theme={null}
POST /api/v1/tracking/sponsored-polls/vote?token=...
```

Submit a user's selected poll option. The vote is recorded and the endpoint returns the current lifetime aggregate results for the poll.

### Request

**token** `string` *required*

Signed token from the poll creative's `vote_url`. It is already present in the `vote_url` query string — POST to that URL exactly as returned. The token must be in the query string; it is not read from the request body.

**option\_id** `string` *required*

Selected option identifier from the poll creative's `options[].id`.

```json Vote request body (POST to the returned vote_url) theme={null}
{
  "option_id": "0"
}
```

### Response

The response is wrapped in a `status` / `data` envelope. `data` holds the current aggregate results.

```json 200 - Vote recorded theme={null}
{
  "status": "success",
  "data": {
    "sponsored_poll_id": 123,
    "question": "What kind of playlist are you making?",
    "total_votes": 42,
    "options": [
      { "id": "0", "text": "Wedding", "votes": 21, "percentage": 50 },
      { "id": "1", "text": "Study", "votes": 14, "percentage": 33.3 },
      { "id": "2", "text": "Workout", "votes": 7, "percentage": 16.7 }
    ]
  }
}
```

```json 400 - Invalid option theme={null}
{
  "detail": "Invalid poll option"
}
```

**status** `string`

Always `"success"` for a recorded (or deduplicated) vote.

**data** `object`

Current aggregate poll results.

**data properties**

**sponsored\_poll\_id** `integer`

Poll identifier.

**question** `string`

Poll question text.

**total\_votes** `integer`

Lifetime vote count for the poll.

**options** `array`

Lifetime aggregate result rows for each option.

**options properties**

**id** `string`

Option identifier.

**text** `string`

Option label.

**votes** `integer`

Lifetime vote count for this option.

**percentage** `number`

Percentage of total votes for this option.

### Duplicate Votes

Each signed poll token records at most one vote for its bid/impression. A duplicate vote attempt does **not** increment the aggregate counts and is **not** an error — the endpoint returns `200` with the current aggregate results, exactly like a first vote. There is no separate duplicate-vote status code, so clients do not need special handling; the returned `data` is always safe to render.

## Results

```http theme={null}
GET /api/v1/tracking/sponsored-polls/results?token=...
```

Retrieve current aggregate results without recording a vote. Use this endpoint to refresh percentages after rendering, or when the user returns to an already-voted poll.

### Query Parameters

**token** `string` *required*

Signed token from the poll creative's `results_url`.

### Response

Same `status` / `data` envelope as the vote endpoint.

```json 200 - Results theme={null}
{
  "status": "success",
  "data": {
    "sponsored_poll_id": 123,
    "question": "What kind of playlist are you making?",
    "total_votes": 42,
    "options": [
      { "id": "0", "text": "Wedding", "votes": 21, "percentage": 50 },
      { "id": "1", "text": "Study", "votes": 14, "percentage": 33.3 },
      { "id": "2", "text": "Workout", "votes": 7, "percentage": 16.7 }
    ]
  }
}
```

**status** `string`

Always `"success"`.

**data** `object`

Current aggregate poll results — `sponsored_poll_id`, `question`, `total_votes`, and an `options` array of `id` / `text` / `votes` / `percentage` rows.

## Error Handling

Errors use the standard FastAPI shape — a single `detail` string:

```json theme={null}
{
  "detail": "Invalid poll option"
}
```

| Status Code | Scenario | `detail` | Client Behavior |
| - | - | - | - |
| `400 Bad Request` | `option_id` does not match one of the poll options | `Invalid poll option` | Keep the poll visible and let the user choose a valid option |
| `404 Not Found` | Token is malformed, expired, or not valid for this poll | `Link expired or not found` | Do not retry with the same token; request a fresh poll creative |
| `422 Unprocessable Entity` | Required `token` query parameter or `option_id` body field is missing | FastAPI validation detail | Fix the request; this indicates a client integration bug |
| `500 Internal Server Error` | Tracking service failed unexpectedly | `Internal Server Error` | Continue without blocking the chat experience |

> [!NOTE]
>
> The `422` response body uses FastAPI's validation format (`detail` is an array of field errors), not a single string.

## Rendering Flow

1. Render the poll question and option buttons from the bid response.
2. Fire the impression pixel — extract the `view_url` query parameter from `cta_url` (or use the top-level `view_url` field when the poll has no `cta_url`) and request it when the poll becomes visible.
3. When the user selects an option, POST `option_id` to `vote_url`.
4. Replace the option buttons with the returned percentages and total vote count (read from the `data` object).
5. If `cta_url` is present, reveal the advertiser CTA after the vote.
6. Use `results_url` to refresh percentages without recording another vote.


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