REST API: build on top of Looptro

Looptro exposes a typed REST API for retros, posts, votes, action items, and team management. Same surface the web client uses, described by OpenAPI.

Updated

The Looptro API is the same surface the web client uses. Every endpoint is described by OpenAPI: fetch the full document at GET /openapi.json and generate a typed client in your language with openapi-typescript or any OpenAPI code generator. If you’re building anything against Looptro (a custom integration, a dashboard, a Slack bot), this is where to start.

For AI agents specifically, the MCP server is usually the right surface (capability-scoped tokens, structured tool calls, audit trail). The REST API is for everything else.

Base URL

Environment Base URL
Hosted https://app.looptro.dev/api
Dev (worktree) http://localhost:$SERVER_PORT/api (default 8589)

All endpoints below are relative to that base.

Authentication

Two paths in.

HttpOnly cookie sessions. Same flow the web client uses. Sign up or log in, the server sets looptro_access (15-minute JWT) + looptro_refresh (30-day rotation) cookies, both HttpOnly + Secure + SameSite=Strict. Every subsequent request goes through with credentials: 'include' (or -b $JAR in curl). Refresh tokens rotate on every call to POST /api/auth/refresh.

Personal access tokens. Capability-scoped, team-scoped, revocable. Mint via POST /api/teams/{team_id}/agent-tokens (from the cookie session, because the mint endpoint is session-only so a leaked PAT cannot bootstrap sibling tokens). The plaintext is returned once in the wire form rtl_pat_<id>_<secret>. Send it as Authorization: Bearer <token>. Same authz checks as cookie sessions; the difference is that every PAT-driven action is audit-logged with actor_type='agent' plus the token id. See the MCP docs for the OAuth-based discovery + device-code flow agents can use to mint tokens without manual setup.

There is no API-key flow for human users beyond the cookie path. If you need scripted access, use a PAT (even if your “agent” is a cron job).

Quickstart

The commands below walk through signup → create team → create retro → add a post. Run them in order; cookies and IDs carry forward.

JAR=$(mktemp)
BASE=https://app.looptro.dev/api

1. Sign up

curl -s -c $JAR -b $JAR -X POST $BASE/auth/signup \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "you@example.com",
    "passphrase": "correct-horse-battery-staple",
    "display_name": "You"
  }'

201 Created with the new user record. Cookies land in $JAR. Passphrases must be ≥12 characters.

2. Create a team

TEAM=$(curl -s -b $JAR -X POST $BASE/teams \
  -H 'Content-Type: application/json' \
  -d '{"name":"Engineering","slug":"engineering"}' \
  | jq -r .id)

201 Created. The creator becomes the team owner.

3. Create a retro

RETRO=$(curl -s -b $JAR -X POST $BASE/teams/$TEAM/retros \
  -H 'Content-Type: application/json' \
  -d '{"title":"Sprint 42","template":"start-stop-continue"}' \
  | jq -r .id)

201 Created. Pass exactly one of template (built-in preset: start-stop-continue, mad-sad-glad, 4ls, sailboat), team_template_id (UUID of a team-curated template), or columns (inline column list for ad-hoc retros). The retro starts in active state and reflect phase; pass "state":"scheduled" to set it up day-before.

4. Add a post

COLUMN=$(curl -s -b $JAR $BASE/retros/$RETRO | jq -r '.columns[0].id')

curl -s -b $JAR -X POST $BASE/retros/$RETRO/posts \
  -H 'Content-Type: application/json' \
  -d "{\"column_id\":\"$COLUMN\",\"body\":\"Pair-program on the auth refactor\"}"

201 Created. The retro broadcasts the new post over its WebSocket to anyone with the board open.

Live updates

GET /api/retros/{id}/ws upgrades to a WebSocket. Same authz (cookie or PAT with retros:read) as the REST endpoints. The server pushes JSON events as state changes. Each event has a type discriminator (snake_case) plus the relevant payload. Sample events:

type Carries
post_created, post_edited, post_moved full PostDetail
post_deleted post_id
post_grouped, post_group_renamed post_id / group_id / title
vote_toggled post_id, new authoritative count
action_item_created / _edited / _deleted full ActionItemDetail or action_item_id
retro_settings_changed, retro_title_changed, retro_timer_changed, retro_columns_changed the changed fields
presence_joined, presence_left, presence_snapshot, participant_evicted viewer presence
ready_changed, ready_reset the per-actor “I’m done” toggle

Treat events as cache-invalidation prompts and refetch the affected resource; the REST endpoint stays the source of truth. The full event union is in the OpenAPI doc under RetroEvent.

Errors

All errors are JSON with a detail field.

{ "detail": "team not found" }
Status When
400 Malformed request body or missing required field
401 No valid session cookie or token
403 Authenticated but not authorized for this resource (also: PAT missing required scope)
404 Resource doesn’t exist or isn’t visible to this caller
409 Conflict (e.g. duplicate slug, accepting an already-consumed invite, deleting a template referenced by an active cycle)
422 Validation error (e.g. passphrase too short)
429 Rate limit exceeded; retry after the Retry-After header

500-class errors collapse to {"detail": "internal error"} with the underlying detail in the server’s structured log. If you see one, please email security@looptro.dev with the timestamp and request id.

Rate limits

Per-IP throttles on the endpoints most worth abusing.

Endpoint Sustained Burst
POST /api/auth/signup 1 every 12 minutes 5
POST /api/auth/login 1 every 6 seconds 10
POST /api/invites/accept 1 every 3 minutes 20
POST /api/retros/{id}/participants 1 every 3 minutes 20
POST /api/teams/{id}/agent-tokens 1 every 12 minutes 5
OAuth start / callback 1 every 6s / 12s 10 / 5
Everything else (global) 10 per second 100

Behind shared egress (corporate NAT, office VPN), the same office hits one bucket. If a 10-person team needs to log in simultaneously after a deploy, the burst absorbs it. If you’re integrating from a server-side process, set a sane backoff on 429 with Retry-After.