Saltar al contenido

June 18, 2026 · 10 min read

Cómo pusimos nuestra retro semanal en piloto automático con la API de PollsLive

El recorrido de una desarrolladora: crea una encuesta de retro, abre una sesión en vivo y luego descarga resultados y un CSV, todo desde un pequeño script de Node en un cron. Endpoints /api/v1 reales, payloads reales.

By the PollsLive team·DevelopersHow-toTeams & meetings

Soy ingeniera de plataforma y, cada viernes, solía pasar diez minutos armando a mano la misma plantilla de retro. Las mismas cinco preguntas, distinta semana. El clásico caso de algo-que-debería-hacer-un-script. Así que lo trasladé a la API de PollsLive y a un cron: la encuesta ahora se crea sola, se abre automáticamente una sesión en vivo y, después del standup, un CSV aterriza en nuestro Slack. Aquí va exactamente cómo, con los endpoints y payloads reales.

Autenticación: un único token Bearer

Generé una API key del workspace en Studio → API para desarrolladores (tienen pinta de `plv_live_…`) y la metí en el entorno del script. Cada petición es simplemente un token Bearer, sin baile de OAuth para usos servidor a servidor.

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

Paso 1: Crear la encuesta de retro

Una encuesta es una plantilla tipada: `content.questions` es un array de diapositivas, cada una con un discriminador `kind`. Para la retro uso una scale, una open_ended renderizada como nube y una multiple_choice para votar el arreglo. `POST /polls` con `renderingMode: "LIVE"` me devuelve un borrador.

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" }
          ]
        }
      ]
    }
  }'

La respuesta `201` envuelve la encuesta en un sobre `data`: me quedo con `data.id` para el resto del flujo, y con `data.accessPin` / `data.shareUrl` para los datos de unión:

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"
  }
}

Paso 2: Publicar y luego abrir una sesión en vivo

Publicar toma una instantánea del contenido para que sea seguro presentarlo. Un `PATCH /polls/{id}` con `publish: true` lo hace; luego `POST /sessions` levanta la sesión de presentador y devuelve el PIN de unión más un `hostToken` de un solo uso (el secreto de autoridad del host, solo se devuelve al crear, así que lo guardo de inmediato).

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" } }

Mi cron publica la `joinUrl` y el PIN en el canal del equipo un minuto antes del standup. El equipo escanea, y las diapositivas que armé en código son lo que ven: aquí está el resultado en vivo de la diapositiva `health` una vez que todos respondieron:

Live scale
Join at the PIN 6098

¿Qué tan sano se sintió este sprint? (1 = duro, 5 = genial)

5 - Genial11% · 1
4 - Bien22% · 2
3 - Regular22% · 2
2 - Duro33% · 3
1 - Malo11% · 1
La diapositiva que definí en JSON, renderizada en vivo, sin armar plantillas a mano.

Paso 3: Descargar resultados y un CSV tras el standup

Cuando la sesión termina, `GET /polls/{id}/results` devuelve los recuentos en un array `slides[]`, una entrada por diapositiva, con agregados por tipo (`optionCounts` para las diapositivas de elección, promedios de escala, texto abierto, etc.). Esa es la parte que debes leer tras una sesión en vivo: `totalVotes` y `voteCounts` en el nivel superior solo cuentan votos asíncronos, así que se quedan en cero y vacío en una encuesta que solo se ha usado en vivo.

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 } }
    ]
  }
}

Para el archivo, `GET /polls/{id}/export` transmite un CSV RFC-4180 (número de diapositiva, pregunta, tipo, etiqueta de respuesta, recuento/valor). Lo guardo y lo adjunto al mensaje de Slack. Todo el paso post-standup es una pequeña función:

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,
  };
}

Eso es todo. La plantilla de retro se construye sola, la sesión se abre a su hora, y el resumen + CSV se publican automáticamente. Recuperé mis diez minutos del viernes, y el formato es idéntico cada semana, que es exactamente lo que quieres de una retro.

Lo mejor: las diapositivas que describo en JSON son las mismas que el equipo ve y vota. Sin desvíos entre 'lo que armó el script' y 'lo que ejecutamos'.

La referencia completa de endpoints y los esquemas están en la referencia de la API interactiva, y el quickstart narrativo está en la guía para desarrolladores. Lo siguiente en mi lista: webhooks, para que el resumen de Slack se dispare en el instante en que termina la sesión en lugar de por temporizador, que es exactamente lo que cubre el artículo sobre webhooks.

Tip

Usa un cron job para clonar el deck de la retro de la semana pasada y publicar un enlace nuevo automáticamente.

Alternativa sin código: encuestas recurrentes sin reconstruirlas.

Construye sobre PollsLive: crea encuestas, dirige sesiones en vivo, descarga resultados y recibe webhooks desde tu propio código.

Lee la documentación para desarrolladores

Usamos cookies esenciales para mantener tu sesión iniciada y evitar votos duplicados. Con tu permiso, también usamos Cloudflare Web Analytics para contar las visitas a las páginas y el Whop Pixel para ver qué visitas terminan en una compra. Política de cookies