Polls Feature - Frontend Integration Guide
Overview
Polls are attached to videos. A video can have one active poll at a time. Polls are managed by admins and displayed to users inside the video player/page.
How polls appear in the video response
When you fetch a video (details endpoint), the response includes:
{
"id": "KxLr9mN2oP",
"name": "My Video",
"pollId": "aBcDeFgHiJ",
"poll": {
"id": "aBcDeFgHiJ",
"title": "Who will win?",
"image": "https://cdn.example.com/poll-image.jpg",
"startsAt": "2026-05-08T10:00:00Z",
"endsAt": "2026-05-10T10:00:00Z",
"status": 2,
"showResults": true,
"videoId": "KxLr9mN2oP",
"totalVotes": 150,
"hasVoted": true,
"userVotedOptionId": "xYzAbCdEfG",
"options": [
{ "id": "xYzAbCdEfG", "text": "Team A", "displayOrder": 1, "voteCount": 90 },
{ "id": "hIjKlMnOpQ", "text": "Team B", "displayOrder": 2, "voteCount": 60 }
]
}
}
pollId— the active poll ID (null if no active poll)poll— full poll object (null if no active poll)hasVoted— whether the current user has voteduserVotedOptionId— which option the user picked (null if not voted)
Result visibility
- If
showResultsistrue— vote counts are always visible - If
showResultsisfalseand user has not voted —totalVotesand allvoteCountvalues are0 - If
showResultsisfalseand user has voted — vote counts are visible
Poll status values
| Value | Name | Description |
|---|---|---|
| 1 | Draft | Not visible to users |
| 2 | Active | Visible and accepting votes |
| 3 | Ended | Closed, no more votes |
Only active polls (status: 2) appear in the video response.
Client/Mobile Endpoints
Submit a vote
POST /client/v1/polls/{pollId}/vote
POST /mobile/v1/polls/{pollId}/vote
Headers: pictv-token, pictv-user (authenticated)
Request body:
{
"optionId": "xYzAbCdEfG"
}
Success response (200): Returns the updated PollDto with the user's vote applied.
{
"id": "aBcDeFgHiJ",
"title": "Who will win?",
"image": "https://cdn.example.com/poll-image.jpg",
"startsAt": "2026-05-08T10:00:00Z",
"endsAt": "2026-05-10T10:00:00Z",
"status": 2,
"showResults": true,
"videoId": "KxLr9mN2oP",
"totalVotes": 151,
"hasVoted": true,
"userVotedOptionId": "xYzAbCdEfG",
"options": [
{ "id": "xYzAbCdEfG", "text": "Team A", "displayOrder": 1, "voteCount": 91 },
{ "id": "hIjKlMnOpQ", "text": "Team B", "displayOrder": 2, "voteCount": 60 }
]
}
Error responses:
| Status | Reason |
|---|---|
| 400 | Already voted on this poll |
| 400 | Poll is not within its active time range |
| 404 | Poll not found or not active |
| 404 | Option not found |
Important rules
- A user can only vote once per poll
- Votes can only be submitted when
statusisActive(2) and current time is betweenstartsAtandendsAt - After voting, the response includes updated vote counts
Frontend Implementation Guide
1. Display the poll
When loading a video, check if poll is not null in the video response:
if (video.poll) {
// Show poll UI
}
2. Show options
Render the options sorted by displayOrder. Each option has:
id— needed for the vote requesttext— the option labelvoteCount— number of votes (may be 0 if results are hidden)
3. Handle vote state
Check poll.hasVoted:
- Not voted: Show voting buttons, allow user to select an option and submit
- Already voted: Highlight the user's choice using
poll.userVotedOptionId, disable voting buttons
4. Show/hide results
const showResults = poll.showResults || poll.hasVoted;
If showResults is false and user hasn't voted, all counts are 0 — just show the options without percentages/bars.
5. Submit vote
On option click:
POST /client/v1/polls/{poll.id}/vote
Body: { "optionId": "<selected option id>" }
On success, replace the poll state with the response data. The response already has hasVoted: true and updated counts.
6. Calculate percentages
const percentage = poll.totalVotes > 0
? Math.round((option.voteCount / poll.totalVotes) * 100)
: 0;
7. Poll image
poll.image is optional. If present, display it as a header/banner for the poll.
8. Time validation
The API enforces time validation server-side, but for UX you can hide the vote button if:
const now = new Date();
const isWithinRange = now >= new Date(poll.startsAt) && now <= new Date(poll.endsAt);
const canVote = poll.status === 2 && isWithinRange && !poll.hasVoted;
Admin Endpoints
Base path: /admin/v1/polls
| Method | Path | Description |
|---|---|---|
| POST | / |
Create a poll |
| GET | / |
List polls (query params: q, status, page, pageSize) |
| GET | /{id} |
Get poll details |
| PUT | /{id} |
Update a poll |
| POST | /{id}/activate |
Activate a poll (Draft -> Active) |
| POST | /{id}/end |
End a poll (Active -> Ended) |
| DELETE | /{id} |
Delete a poll |
Create poll
POST /admin/v1/polls
{
"videoId": "KxLr9mN2oP",
"title": "Who will win?",
"image": "https://cdn.example.com/poll-image.jpg",
"startsAt": "2026-05-08T10:00:00Z",
"endsAt": "2026-05-10T10:00:00Z",
"showResults": true,
"showOnEmbed": true,
"options": [
{ "text": "Team A", "displayOrder": 1 },
{ "text": "Team B", "displayOrder": 2 }
]
}
- Minimum 2 options required
- Poll is created in
Draftstatus showResults— whether users can see vote counts before votingshowOnEmbed— whether poll appears on the video embed
Update poll
PUT /admin/v1/polls/{id}
Same body as create, plus:
id— poll ID (in body)options[].id— include the existing option ID to update it; omit to add new; missing IDs get deleted
{
"id": "aBcDeFgHiJ",
"videoId": "KxLr9mN2oP",
"title": "Updated title",
"image": null,
"startsAt": "2026-05-08T10:00:00Z",
"endsAt": "2026-05-12T10:00:00Z",
"showResults": false,
"showOnEmbed": true,
"options": [
{ "id": "xYzAbCdEfG", "text": "Team A (updated)", "displayOrder": 1 },
{ "text": "Team C (new)", "displayOrder": 3 }
]
}
Activate poll
POST /admin/v1/polls/{id}/activate
- Changes status from Draft to Active
- Fails if the video already has another active poll
- Fails if poll has fewer than 2 options
End poll
POST /admin/v1/polls/{id}/end
Changes status to Ended. No more votes accepted.
List polls
GET /admin/v1/polls?q=search&status=2&page=1&pageSize=10
Status filter values: 1 (Draft), 2 (Active), 3 (Ended)
Admin poll response
Includes video details and audit fields:
{
"id": "aBcDeFgHiJ",
"title": "Who will win?",
"image": "https://cdn.example.com/poll-image.jpg",
"startsAt": "2026-05-08T10:00:00Z",
"endsAt": "2026-05-10T10:00:00Z",
"status": 2,
"showResults": true,
"showOnEmbed": true,
"videoId": "KxLr9mN2oP",
"video": {
"id": "KxLr9mN2oP",
"name": "My Video",
"thumbnail": "https://cdn.example.com/thumb.jpg"
},
"totalVotes": 150,
"createdAt": "2026-05-07T12:00:00Z",
"updatedAt": "2026-05-08T09:00:00Z",
"createdBy": "admin-user-id",
"updatedBy": "admin-user-id",
"options": [
{ "id": "xYzAbCdEfG", "text": "Team A", "displayOrder": 1, "voteCount": 90 },
{ "id": "hIjKlMnOpQ", "text": "Team B", "displayOrder": 2, "voteCount": 60 }
]
}
Admin Poll Lifecycle
Draft --> Active --> Ended
(activate) (end)
- Create poll (status: Draft)
- Configure options, title, image, dates
- Activate when ready (only one active poll per video)
- End when finished