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; the typed TypeScript client in client/src/lib/api-types.ts is regenerated from the live schema. If you’re building anything against Looptro (a custom integration, a dashboard, a Slack bot beyond the built-in one), 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

EnvironmentBase URL
Hostedhttps://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 — the mint endpoint is session-only so a leaked PAT can’t 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, plus-delta, sailboat, daki), 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:

typeCarries
post_created, post_edited, post_movedfull PostDetail
post_deletedpost_id
post_grouped, post_group_renamedpost_id / group_id / title
vote_toggledpost_id, new authoritative count
action_item_created / _edited / _deletedfull ActionItemDetail or action_item_id
retro_settings_changed, retro_title_changed, retro_timer_changed, retro_columns_changedthe changed fields
presence_joined, presence_left, presence_snapshot, participant_evictedviewer presence
ready_changed, ready_resetthe 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" }
StatusWhen
400Malformed request body or missing required field
401No valid session cookie or token
403Authenticated but not authorized for this resource (also: PAT missing required scope)
404Resource doesn’t exist or isn’t visible to this caller
409Conflict (e.g. duplicate slug, accepting an already-consumed invite, deleting a template referenced by an active cycle)
422Validation error (e.g. passphrase too short)
429Rate 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.

EndpointSustainedBurst
POST /api/auth/signup1 every 12 minutes5
POST /api/auth/login1 every 6 seconds10
POST /api/invites/accept1 every 3 minutes20
POST /api/retros/{id}/participants1 every 3 minutes20
POST /api/teams/{id}/agent-tokens1 every 12 minutes5
OAuth start / callback1 every 6s / 12s10 / 5
Everything else (global)10 per second100

Behind shared egress (corporate NAT, office VPN), the same office hits one bucket. If a 10-person engineering 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.

OpenAPI

GET /openapi.json on any Looptro instance returns the full OpenAPI 3 document. Use it with openapi-typescript or any code generator to mint a typed client in your language of choice. The TypeScript client in this repo is generated this way; see client/src/lib/api-types.ts.

What’s not in the public API yet

  • Webhook delivery for retro events. Tracked in the Realtime Collaboration project.
  • Pagination cursors as opaque tokens (the v1 surface mostly returns small bounded lists).
  • Bulk endpoints for action item sync. The MCP server already exposes single-item primitives; bulk is added when a real workflow needs it.