Naar inhoud springen

June 18, 2026 · 10 min read

How we put our weekly retro on autopilot with the PollsLive API

A developer's walkthrough: create a retro poll, open a live session, then pull results and a CSV, all from a small Node script on a cron. Real /api/v1 endpoints, real payloads.

By the PollsLive team·DevelopersHow-toTeams & meetings

I'm a platform engineer, and every Friday I used to spend ten minutes hand-building the same retro deck. Same five questions, different week. Classic thing-a-script-should-do. So I moved it onto the PollsLive API and a cron job: the poll now creates itself, a live session opens automatically, and after standup a CSV lands in our Slack. Here's exactly how, with the real endpoints and payloads.

Auth: one Bearer token

I generated a workspace API key in Studio → Developer API (they look like `plv_live_…`) and dropped it in the script's environment. Every request is just a Bearer token, no OAuth dance for server-to-server use.

auth
# Base URL for all calls
export POLLSLIVE_API=https://pollslive.com/api/v1
export POLLSLIVE_KEY=plv_live_xxxxxxxxxxxxxxxxxxxx

# Every request carries the key as a Bearer token:
#   Authorization: Bearer $POLLSLIVE_KEY
# Limits and HTTP 429 handling: https://pollslive.com/docs/developers/rate-limits

Step 1: Create the retro poll

A poll is a typed deck: `content.questions` is an array of slides, each with a `kind` discriminator. For the retro I use a scale, an open_ended rendered as a cloud, and a multiple_choice to vote on the fix. `POST /polls` with `renderingMode: "LIVE"` gives me a draft back.

POST /polls
curl -sS -X POST "$POLLSLIVE_API/polls" \
  -H "Authorization: Bearer $POLLSLIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Weekly retro - week of Jun 16",
    "renderingMode": "LIVE",
    "content": {
      "questions": [
        {
          "id": "health",
          "kind": "scale",
          "text": "How healthy did this sprint feel?",
          "min": 1, "max": 5,
          "minLabel": "Rough", "maxLabel": "Great"
        },
        {
          "id": "blockers",
          "kind": "open_ended",
          "text": "What slowed us down this sprint?",
          "render": "cloud",
          "maxLength": 120
        },
        {
          "id": "fix",
          "kind": "multiple_choice",
          "type": "single",
          "text": "What should we fix first next sprint?",
          "options": [
            { "id": "ci",       "label": "Stabilise the CI pipeline" },
            { "id": "tickets",  "label": "Tighter ticket acceptance criteria" },
            { "id": "wip",      "label": "Cap WIP and reviews SLA" },
            { "id": "meetings", "label": "Trim the meeting load" }
          ]
        }
      ]
    }
  }'

The `201` response wraps the poll in a `data` envelope. I keep `data.id` for the rest of the flow, and `data.accessPin` / `data.shareUrl` for the join details:

201 Created
{
  "data": {
    "id": "poll_9aZ2kP",
    "slug": "weekly-retro-jun-16",
    "title": "Weekly retro - week of Jun 16",
    "renderingMode": "LIVE",
    "isPublished": false,
    "accessPin": "6098",
    "shareUrl": "https://pollslive.com/p/weekly-retro-jun-16",
    "createdAt": "2026-06-16T08:00:11.204Z"
  }
}

Step 2: Publish, then open a live session

Publishing snapshots the content so it's safe to present. A `PATCH /polls/{id}` with `publish: true` does it; then `POST /sessions` spins up the presenter session and returns the join PIN plus a one-time `hostToken` (the host-authority secret: it's only returned on create, so I store it immediately).

PATCH /polls/{id} → POST /sessions
# Publish the draft
curl -sS -X PATCH "$POLLSLIVE_API/polls/poll_9aZ2kP" \
  -H "Authorization: Bearer $POLLSLIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "publish": true }'

# Open a presenter-paced live session
curl -sS -X POST "$POLLSLIVE_API/sessions" \
  -H "Authorization: Bearer $POLLSLIVE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "pollId": "poll_9aZ2kP", "pace": "PRESENTER" }'
# → { "data": { "id": "ses_4dF1", "pin": "6098",
#       "joinUrl": "https://pollslive.com/play/6098",
#       "hostUrl": "https://pollslive.com/host/ses_4dF1",
#       "hostToken": "hst_…", "status": "LOBBY" } }

My cron posts the `joinUrl` and PIN into the team channel a minute before standup. The team scans, and the slides I built in code are what they see. Here's the live result of the `health` slide once everyone's answered:

Live scale
Join at the PIN 6098

How healthy did this sprint feel? (1 = rough, 5 = great)

5 - Great11% · 1
4 - Good22% · 2
3 - OK22% · 2
2 - Rough33% · 3
1 - Bad11% · 1
The slide I defined in JSON, rendered live, no manual deck-building.

Step 3: Pull results and a CSV after standup

When the session ends, `GET /polls/{id}/results` returns the tallies in a `slides[]` array, one entry per slide, with per-kind aggregates (`optionCounts` for choice slides, scale averages, open text, etc.). That's the part to read after a live session: the top-level `totalVotes` and `voteCounts` count async votes only, so they stay at zero and empty for a poll that has only run live.

GET /polls/{id}/results
curl -sS "$POLLSLIVE_API/polls/poll_9aZ2kP/results" \
  -H "Authorization: Bearer $POLLSLIVE_KEY"

{
  "data": {
    "pollId": "poll_9aZ2kP",
    "slug": "weekly-retro-jun-16",
    "title": "Weekly retro - week of Jun 16",
    "isPublished": true,
    "closed": false,
    "totalVotes": 0,
    "voteCounts": {},
    "slides": [
      { "slideId": "health", "kind": "scale", "totalResponses": 9,
        "average": 2.888888888888889,
        "distribution": { "1": 1, "2": 3, "3": 2, "4": 2, "5": 1 } },
      { "slideId": "blockers", "kind": "open_ended", "totalResponses": 2,
        "responses": [
          { "text": "flaky CI", "displayName": null,
            "createdAt": "2026-06-19T09:04:12.000Z" },
          { "text": "late reviews", "displayName": null,
            "createdAt": "2026-06-19T09:04:31.000Z" }
        ] },
      { "slideId": "fix", "kind": "multiple_choice", "totalResponses": 9,
        "optionCounts": { "ci": 4, "tickets": 3, "wip": 2, "meetings": 0 } }
    ]
  }
}

For the archive, `GET /polls/{id}/export` streams an RFC-4180 CSV (slide number, question, type, answer label, count/value). I save it and attach it to the Slack message. The whole post-standup step is one small function:

summarise.mjs (Node 18+)
const API = process.env.POLLSLIVE_API;
const KEY = process.env.POLLSLIVE_KEY;
const auth = { Authorization: `Bearer ${KEY}` };

export async function summariseRetro(pollId) {
  const res = await fetch(`${API}/polls/${pollId}/results`, { headers: auth });
  if (res.status === 429) throw new Error("rate_limited"); // back off + retry
  if (!res.ok) throw new Error(`results ${res.status}`);
  const { data } = await res.json();

  // Live answers are in slides[], not in the top-level voteCounts
  const slide = (id) => data.slides.find((s) => s.slideId === id);
  const fix = slide("fix");
  const winner = Object.entries(fix?.optionCounts ?? {})
    .filter(([, count]) => count > 0)
    .sort((a, b) => b[1] - a[1])[0];

  // Grab the CSV for the archive
  const csv = await fetch(`${API}/polls/${pollId}/export`, { headers: auth })
    .then((r) => r.text());

  return {
    health: slide("health")?.average,
    topFix: winner?.[0],
    voters: fix?.totalResponses ?? 0,
    csv,
  };
}

That's the whole thing. The retro deck builds itself, the session opens on schedule, and the summary + CSV post automatically. I got my Friday ten minutes back, and the format is identical every week, which is exactly what you want from a retro.

The nicest part: the slides I describe in JSON are the same slides the team sees and votes on. No drift between 'what the script made' and 'what we ran'.

Full endpoint reference and schemas are in the interactive API reference, and the narrative quickstart is in the developer guide. Next on my list: webhooks, so the Slack summary fires the instant the session ends instead of on a timer, which is exactly what the webhooks write-up covers.

Tip

Use a cron job to clone last week's retro deck and publish a fresh link automatically.

No-code alternative: recurring polls without rebuilding.

Build on PollsLive: create polls, drive live sessions, pull results, and receive webhooks from your own code.

Read the developer docs

We gebruiken essentiële cookies om je ingelogd te houden en dubbele stemmen te voorkomen. Met je toestemming gebruiken we ook Cloudflare Web Analytics om paginaweergaven te tellen en de Whop Pixel om te zien welke bezoeken tot een aankoop leiden. Cookie Policy