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.
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.
# 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-limitsPaso 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.
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:
{
"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).
# 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:
¿Qué tan sano se sintió este sprint? (1 = duro, 5 = genial)
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.
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:
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